diff --git a/BUILD_OSX.md b/BUILD_OSX.md
index c8f19710ca..44f27d3d02 100644
--- a/BUILD_OSX.md
+++ b/BUILD_OSX.md
@@ -3,9 +3,10 @@ Please read the [general build guide](BUILD.md) for information on dependencies
 ###Homebrew
 [Homebrew](http://brew.sh/) is an excellent package manager for OS X. It makes install of all High Fidelity dependencies very simple.
 
-    brew install cmake openssl qt5
+    brew tap homebrew/versions
+    brew install cmake openssl qt55
 
-We no longer require install of qt5 via our [homebrew formulas repository](https://github.com/highfidelity/homebrew-formulas). Versions of Qt that are 5.5.x and above provide a mechanism to disable the wireless scanning we previously had a custom patch for.
+We no longer require install of qt5 via our [homebrew formulas repository](https://github.com/highfidelity/homebrew-formulas). Versions of Qt that are 5.5.x provide a mechanism to disable the wireless scanning we previously had a custom patch for.
 
 ###OpenSSL and Qt
 
diff --git a/assignment-client/src/AssignmentClientMonitor.cpp b/assignment-client/src/AssignmentClientMonitor.cpp
index 322fe6e57e..8ba253d549 100644
--- a/assignment-client/src/AssignmentClientMonitor.cpp
+++ b/assignment-client/src/AssignmentClientMonitor.cpp
@@ -286,8 +286,8 @@ void AssignmentClientMonitor::handleChildStatusPacket(QSharedPointer<ReceivedMes
 
             if (!senderID.isNull()) {
                 // We don't have this node yet - we should add it
-                matchingNode = DependencyManager::get<LimitedNodeList>()->addOrUpdateNode
-                    (senderID, NodeType::Unassigned, senderSockAddr, senderSockAddr, false, false);
+                matchingNode = DependencyManager::get<LimitedNodeList>()->addOrUpdateNode(senderID, NodeType::Unassigned,
+                                                                                          senderSockAddr, senderSockAddr);
 
                 auto childData = std::unique_ptr<AssignmentClientChildData>
                     { new AssignmentClientChildData(Assignment::Type::AllTypes) };
diff --git a/assignment-client/src/assets/AssetServer.cpp b/assignment-client/src/assets/AssetServer.cpp
index 1fb0674e7d..7f43b86328 100644
--- a/assignment-client/src/assets/AssetServer.cpp
+++ b/assignment-client/src/assets/AssetServer.cpp
@@ -235,7 +235,7 @@ void AssetServer::handleGetAllMappingOperation(ReceivedMessage& message, SharedN
 }
 
 void AssetServer::handleSetMappingOperation(ReceivedMessage& message, SharedNodePointer senderNode, NLPacketList& replyPacket) {
-    if (senderNode->getCanRez()) {
+    if (senderNode->getCanWriteToAssetServer()) {
         QString assetPath = message.readString();
 
         auto assetHash = message.read(SHA256_HASH_LENGTH).toHex();
@@ -251,7 +251,7 @@ void AssetServer::handleSetMappingOperation(ReceivedMessage& message, SharedNode
 }
 
 void AssetServer::handleDeleteMappingsOperation(ReceivedMessage& message, SharedNodePointer senderNode, NLPacketList& replyPacket) {
-    if (senderNode->getCanRez()) {
+    if (senderNode->getCanWriteToAssetServer()) {
         int numberOfDeletedMappings { 0 };
         message.readPrimitive(&numberOfDeletedMappings);
 
@@ -272,7 +272,7 @@ void AssetServer::handleDeleteMappingsOperation(ReceivedMessage& message, Shared
 }
 
 void AssetServer::handleRenameMappingOperation(ReceivedMessage& message, SharedNodePointer senderNode, NLPacketList& replyPacket) {
-    if (senderNode->getCanRez()) {
+    if (senderNode->getCanWriteToAssetServer()) {
         QString oldPath = message.readString();
         QString newPath = message.readString();
 
@@ -337,7 +337,7 @@ void AssetServer::handleAssetGet(QSharedPointer<ReceivedMessage> message, Shared
 
 void AssetServer::handleAssetUpload(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode) {
 
-    if (senderNode->getCanRez()) {
+    if (senderNode->getCanWriteToAssetServer()) {
         qDebug() << "Starting an UploadAssetTask for upload from" << uuidStringWithoutCurlyBraces(senderNode->getUUID());
 
         auto task = new UploadAssetTask(message, senderNode, _filesDirectory);
diff --git a/assignment-client/src/entities/EntityServer.cpp b/assignment-client/src/entities/EntityServer.cpp
index 0555f95c65..7594d5dd2c 100644
--- a/assignment-client/src/entities/EntityServer.cpp
+++ b/assignment-client/src/entities/EntityServer.cpp
@@ -268,6 +268,14 @@ void EntityServer::readAdditionalConfiguration(const QJsonObject& settingsSectio
     qDebug("wantTerseEditLogging=%s", debug::valueOf(wantTerseEditLogging));
 
     EntityTreePointer tree = std::static_pointer_cast<EntityTree>(_tree);
+
+    int maxTmpEntityLifetime;
+    if (readOptionInt("maxTmpLifetime", settingsSectionObject, maxTmpEntityLifetime)) {
+        tree->setEntityMaxTmpLifetime(maxTmpEntityLifetime);
+    } else {
+        tree->setEntityMaxTmpLifetime(EntityTree::DEFAULT_MAX_TMP_ENTITY_LIFETIME);
+    }
+
     tree->setWantEditLogging(wantEditLogging);
     tree->setWantTerseEditLogging(wantTerseEditLogging);
 }
diff --git a/domain-server/resources/describe-settings.json b/domain-server/resources/describe-settings.json
index ba00392cd7..bad24dd3a1 100644
--- a/domain-server/resources/describe-settings.json
+++ b/domain-server/resources/describe-settings.json
@@ -1,5 +1,5 @@
 {
-  "version": 1.3,
+  "version": 1.4,
   "settings": [
     {
       "name": "metaverse",
@@ -56,6 +56,7 @@
           "label": "Paths",
           "help": "Clients can enter a path to reach an exact viewpoint in your domain.<br/>Add rows to the table below to map a path to a viewpoint.<br/>The index path ( / ) is where clients will enter if they do not enter an explicit path.",
           "type": "table",
+          "can_add_new_rows": true,
           "key": {
             "name": "path",
             "label": "Path",
@@ -157,27 +158,6 @@
           "help": "Password used for basic HTTP authentication. Leave this blank if you do not want to change it.",
           "value-hidden": true
         },
-        {
-          "name": "restricted_access",
-          "type": "checkbox",
-          "label": "Restricted Access",
-          "default": false,
-          "help": "Only users listed in \"Allowed Users\" can enter your domain."
-        },
-        {
-          "name": "allowed_users",
-          "type": "table",
-          "label": "Allowed Users",
-          "help": "You can always connect from the domain-server machine.",
-          "numbered": false,
-          "columns": [
-            {
-              "name": "username",
-              "label": "Username",
-              "can_set": true
-            }
-          ]
-        },
         {
           "name": "maximum_user_capacity",
           "label": "Maximum User Capacity",
@@ -187,25 +167,141 @@
           "advanced": false
         },
         {
-          "name": "allowed_editors",
+          "name": "standard_permissions",
           "type": "table",
-          "label": "Allowed Editors",
-          "help": "List the High Fidelity names for people you want to be able lock or unlock entities in this domain.<br/>An empty list means everyone.",
-          "numbered": false,
+          "label": "Domain-Wide User Permissions",
+          "help": "Indicate which users or groups can have which <a data-toggle='tooltip' data-html=true title='<p><strong>Domain-Wide User Permissions</strong></p><ul><li><strong>Connect</strong><br />Sets whether a user can connect to the the domain.</li><li><strong>Lock / Unlock</strong><br />Sets whether a user change the &ldquo;locked&rdquo; property of an entity (either from on to off or off to on).</li><li><strong>Rez</strong><br />Sets whether a user can create new entities.</li><li><strong>Rez Temporary</strong><br />Sets whether a user can create new entities with a finite lifetime.</li><li><strong>Write Assets</strong><br />Sets whether a user can make changes to the domain&rsquo;s asset-server assets.</li><li><strong>Ignore Max Capacity</strong><br />Sets whether a user can connect even if the domain has reached or exceeded its maximum allowed agents.</li></ul><p>Note that permissions assigned to a specific user will supersede any parameter-level permissions that might otherwise apply to that user. Additionally, if more than one parameter is applicable to a given user, the permissions given to that user will be the sum of all applicable parameters. For example, let&rsquo;s say only localhost users can connect and only logged in users can lock and unlock entities. If a user is both logged in and on localhost then they will be able to both connect and lock/unlock entities.</p>'>domain-wide permissions</a>.",
+          "caption": "Standard Permissions",
+          "can_add_new_rows": false,
+
+          "groups": [
+            {
+              "label": "User / Group",
+              "span": 1
+            },
+            {
+              "label": "Permissions <a data-toggle='tooltip' data-html='true' title='<p><strong>Domain-Wide User Permissions</strong></p><ul><li><strong>Connect</strong><br />Sets whether a user can connect to the the domain.</li><li><strong>Lock / Unlock</strong><br />Sets whether a user change the &ldquo;locked&rdquo; property of an entity (either from on to off or off to on).</li><li><strong>Rez</strong><br />Sets whether a user can create new entities.</li><li><strong>Rez Temporary</strong><br />Sets whether a user can create new entities with a finite lifetime.</li><li><strong>Write Assets</strong><br />Sets whether a user can make changes to the domain&rsquo;s asset-server assets.</li><li><strong>Ignore Max Capacity</strong><br />Sets whether a user can connect even if the domain has reached or exceeded its maximum allowed agents.</li></ul><p>Note that permissions assigned to a specific user will supersede any parameter-level permissions that might otherwise apply to that user. Additionally, if more than one parameter is applicable to a given user, the permissions given to that user will be the sum of all applicable parameters. For example, let&rsquo;s say only localhost users can connect and only logged in users can lock and unlock entities. If a user is both logged in and on localhost then they will be able to both connect and lock/unlock entities.</p>'>?</a>",
+              "span": 6
+            }
+          ],
+
           "columns": [
             {
-              "name": "username",
-              "label": "Username",
-              "can_set": true
+              "name": "permissions_id",
+              "label": ""
+            },
+            {
+              "name": "id_can_connect",
+              "label": "Connect",
+              "type": "checkbox",
+              "editable": true,
+              "default": true
+            },
+            {
+              "name": "id_can_adjust_locks",
+              "label": "Lock / Unlock",
+              "type": "checkbox",
+              "editable": true,
+              "default": false
+            },
+            {
+              "name": "id_can_rez",
+              "label": "Rez",
+              "type": "checkbox",
+              "editable": true,
+              "default": false
+            },
+            {
+              "name": "id_can_rez_tmp",
+              "label": "Rez Temporary",
+              "type": "checkbox",
+              "editable": true,
+              "default": false
+            },
+            {
+              "name": "id_can_write_to_asset_server",
+              "label": "Write Assets",
+              "type": "checkbox",
+              "editable": true,
+              "default": false
+            },
+            {
+              "name": "id_can_connect_past_max_capacity",
+              "label": "Ignore Max Capacity",
+              "type": "checkbox",
+              "editable": true,
+              "default": false
             }
-          ]
+          ],
+
+          "non-deletable-row-key": "permissions_id",
+          "non-deletable-row-values": ["localhost", "anonymous", "logged-in"]
         },
         {
-          "name": "editors_are_rezzers",
-          "type": "checkbox",
-          "label": "Only Editors Can Create Entities",
-          "help": "Only users listed in \"Allowed Editors\" can create new entites.",
-          "default": false
+          "name": "permissions",
+          "type": "table",
+          "caption": "Permissions for Specific Users",
+          "can_add_new_rows": true,
+
+          "groups": [
+            {
+              "label": "User / Group",
+              "span": 1
+            },
+            {
+              "label": "Permissions <a data-toggle='tooltip' data-html='true' title='<p><strong>Domain-Wide User Permissions</strong></p><ul><li><strong>Connect</strong><br />Sets whether a user can connect to the the domain.</li><li><strong>Lock / Unlock</strong><br />Sets whether a user change the &ldquo;locked&rdquo; property of an entity (either from on to off or off to on).</li><li><strong>Rez</strong><br />Sets whether a user can create new entities.</li><li><strong>Rez Temporary</strong><br />Sets whether a user can create new entities with a finite lifetime.</li><li><strong>Write Assets</strong><br />Sets whether a user can make changes to the domain&rsquo;s asset-server assets.</li><li><strong>Ignore Max Capacity</strong><br />Sets whether a user can connect even if the domain has reached or exceeded its maximum allowed agents.</li></ul><p>Note that permissions assigned to a specific user will supersede any parameter-level permissions that might otherwise apply to that user. Additionally, if more than one parameter is applicable to a given user, the permissions given to that user will be the sum of all applicable parameters. For example, let&rsquo;s say only localhost users can connect and only logged in users can lock and unlock entities. If a user is both logged in and on localhost then they will be able to both connect and lock/unlock entities.</p>'>?</a>",
+              "span": 6
+            }
+          ],
+
+          "columns": [
+            {
+              "name": "permissions_id",
+              "label": ""
+            },
+            {
+              "name": "id_can_connect",
+              "label": "Connect",
+              "type": "checkbox",
+              "editable": true,
+              "default": true
+            },
+            {
+              "name": "id_can_adjust_locks",
+              "label": "Lock / Unlock",
+              "type": "checkbox",
+              "editable": true,
+              "default": false
+            },
+            {
+              "name": "id_can_rez",
+              "label": "Rez",
+              "type": "checkbox",
+              "editable": true,
+              "default": false
+            },
+            {
+              "name": "id_can_rez_tmp",
+              "label": "Rez Temporary",
+              "type": "checkbox",
+              "editable": true,
+              "default": false
+            },
+            {
+              "name": "id_can_write_to_asset_server",
+              "label": "Write Assets",
+              "type": "checkbox",
+              "editable": true,
+              "default": false
+            },
+            {
+              "name": "id_can_connect_past_max_capacity",
+              "label": "Ignore Max Capacity",
+              "type": "checkbox",
+              "editable": true,
+              "default": false
+            }
+          ]
         }
       ]
     },
@@ -218,6 +314,8 @@
           "type": "table",
           "label": "Persistent Scripts",
           "help": "Add the URLs for scripts that you would like to ensure are always running in your domain.",
+          "can_add_new_rows": true,
+
           "columns": [
             {
               "name": "url",
@@ -302,6 +400,8 @@
           "label": "Zones",
           "help": "In this table you can define a set of zones in which you can specify various audio properties.",
           "numbered": false,
+          "can_add_new_rows": true,
+
           "key": {
             "name": "name",
             "label": "Name",
@@ -353,6 +453,8 @@
           "help": "In this table you can set custom attenuation coefficients between audio zones",
           "numbered": true,
           "can_order": true,
+          "can_add_new_rows": true,
+
           "columns": [
             {
               "name": "source",
@@ -380,6 +482,8 @@
           "label": "Reverb Settings",
           "help": "In this table you can set reverb levels for audio zones.  For a medium-sized (e.g., 100 square meter) meeting room, try a decay time of around 1.5 seconds and a wet/dry mix of 25%. For an airplane hangar or cathedral, try a decay time of 4 seconds and a wet/dry mix of 50%.",
           "numbered": true,
+          "can_add_new_rows": true,
+
           "columns": [
             {
               "name": "zone",
@@ -479,6 +583,14 @@
       "label": "Entity Server Settings",
       "assignment-types": [6],
       "settings": [
+        {
+          "name": "maxTmpLifetime",
+          "label": "Maximum Lifetime of Temporary Entities",
+          "help": "The maximum number of seconds for the lifetime of an entity which will be considered \"temporary\".",
+          "placeholder": "3600",
+          "default": "3600",
+          "advanced": true
+        },
         {
           "name": "persistFilePath",
           "label": "Entities File Path",
@@ -501,6 +613,8 @@
           "label": "Backup Rules",
           "help": "In this table you can define a set of rules for how frequently to backup copies of your entites content file.",
           "numbered": false,
+          "can_add_new_rows": true,
+
           "default":  [
                           {"Name":"Half Hourly Rolling","backupInterval":1800,"format":".backup.halfhourly.%N","maxBackupVersions":5},
                           {"Name":"Daily Rolling","backupInterval":86400,"format":".backup.daily.%N","maxBackupVersions":7},
diff --git a/domain-server/resources/web/css/style.css b/domain-server/resources/web/css/style.css
index efb9e907c5..2862feed87 100644
--- a/domain-server/resources/web/css/style.css
+++ b/domain-server/resources/web/css/style.css
@@ -20,6 +20,17 @@ body {
   top: 40px;
 }
 
+.table .value-row td, .table .inputs td {
+    vertical-align: middle;
+}
+
+.table .table-checkbox {
+    /* Fix IE sizing checkboxes to fill table cell */
+    width: auto;
+    margin-left: auto;
+    margin-right: auto;
+}
+
 .glyphicon-remove {
   font-size: 24px;
 }
@@ -107,6 +118,58 @@ table {
   word-wrap: break-word;
 }
 
+caption {
+    color: #333;
+    font-weight: 700;
+    padding-top: 0;
+}
+
+table > tbody > .headers > td {
+    vertical-align: middle;
+}
+
+table .headers + .headers td {
+    font-size: 13px;
+    color: #222;
+}
+
+table[name="security.standard_permissions"] .headers td + td, table[name="security.permissions"] .headers td + td {
+    text-align: center;
+}
+
+.tooltip.top .tooltip-arrow {
+    border-top-color: #fff;
+    border-width: 10px 10px 0;
+    margin-bottom: -5px;
+}
+
+.tooltip-inner {
+    padding: 20px 20px 10px 20px;
+    font-size: 14px;
+    text-align: left;
+    color: #333;
+    background-color: #fff;
+    box-shadow: 0 3px 8px 8px #e8e8e8;
+}
+
+.tooltip.in {
+    opacity: 1;
+}
+
+.tooltip-inner ul {
+    padding-left: 0;
+    margin-bottom: 15px;
+}
+
+.tooltip-inner li {
+    list-style-type: none;
+    margin-bottom: 5px;
+}
+
+#security .tooltip-inner {
+    max-width: 520px;
+}
+
 #xs-advanced-container {
   margin-bottom: 20px;
 }
diff --git a/domain-server/resources/web/settings/js/settings.js b/domain-server/resources/web/settings/js/settings.js
index e17a886e10..aecc48b31f 100644
--- a/domain-server/resources/web/settings/js/settings.js
+++ b/domain-server/resources/web/settings/js/settings.js
@@ -232,6 +232,17 @@ $(document).ready(function(){
     badgeSidebarForDifferences($(this));
   });
 
+  // Bootstrap switch in table
+  $('#' + Settings.FORM_ID).on('change', 'input.table-checkbox', function () {
+    // Bootstrap switches in table: set the changed data attribute for all rows in table.
+    var row = $(this).closest('tr');
+    if (row.hasClass("value-row")) {  // Don't set attribute on input row switches prior to it being added to table.
+      row.find('td.' + Settings.DATA_COL_CLASS + ' input').attr('data-changed', true);
+      updateDataChangedForSiblingRows(row, true);
+      badgeSidebarForDifferences($(this));
+    }
+  });
+
   $('.advanced-toggle').click(function(){
     Settings.showAdvanced = !Settings.showAdvanced
     var advancedSelector = $('.' + Settings.ADVANCED_CLASS)
@@ -841,6 +852,8 @@ function reloadSettings(callback) {
     // setup any bootstrap switches
     $('.toggle-checkbox').bootstrapSwitch();
 
+    $('[data-toggle="tooltip"]').tooltip();
+
     // add tooltip to locked settings
     $('label.locked').tooltip({
       placement: 'right',
@@ -875,6 +888,7 @@ function saveSettings() {
     }
   }
 
+  console.log("----- SAVING ------");
   console.log(formJSON);
 
   // re-enable all inputs
@@ -908,10 +922,33 @@ function makeTable(setting, keypath, setting_value, isLocked) {
     html += "<span class='help-block'>" + setting.help + "</span>"
   }
 
+  var nonDeletableRowKey = setting["non-deletable-row-key"];
+  var nonDeletableRowValues = setting["non-deletable-row-values"];
+
   html += "<table class='table table-bordered " + (isLocked ? "locked-table" : "") + "' data-short-name='" + setting.name
     + "' name='" + keypath + "' id='" + (typeof setting.html_id !== 'undefined' ? setting.html_id : keypath)
     + "' data-setting-type='" + (isArray ? 'array' : 'hash') + "'>";
 
+  if (setting.caption) {
+    html += "<caption>" + setting.caption + "</caption>"
+  }
+
+  // Column groups
+  if (setting.groups) {
+    html += "<tr class='headers'>"
+    _.each(setting.groups, function (group) {
+        html += "<td colspan='" + group.span  + "'><strong>" + group.label + "</strong></td>"
+    })
+    if (!isLocked && !setting.read_only) {
+        if (setting.can_order) {
+            html += "<td class='" + Settings.REORDER_BUTTONS_CLASSES +
+                    "'><a href='javascript:void(0);' class='glyphicon glyphicon-sort'></a></td>";
+        }
+        html += "<td class='" + Settings.ADD_DEL_BUTTONS_CLASSES + "'></td></tr>"
+    }
+    html += "</tr>"
+  }
+
   // Column names
   html += "<tr class='headers'>"
 
@@ -950,6 +987,8 @@ function makeTable(setting, keypath, setting_value, isLocked) {
           html += "<td class='key'>" + rowIndexOrName + "</td>"
       }
 
+      var isNonDeletableRow = false;
+
       _.each(setting.columns, function(col) {
 
         if (isArray) {
@@ -961,16 +1000,19 @@ function makeTable(setting, keypath, setting_value, isLocked) {
           colName = keypath + "." + rowIndexOrName + "." + col.name;
         }
 
-        // setup the td for this column
-        html += "<td class='" + Settings.DATA_COL_CLASS + "' name='" + colName + "'>";
+        isNonDeletableRow = isNonDeletableRow
+          || (nonDeletableRowKey === col.name && nonDeletableRowValues.indexOf(colValue) !== -1);
 
-        // add the actual value to the td so it is displayed
-        html += colValue;
+        if (isArray && col.type === "checkbox" && col.editable) {
+          html += "<td class='" + Settings.DATA_COL_CLASS + "'name='" + col.name + "'>"
+                  + "<input type='checkbox' class='form-control table-checkbox' "
+                  + "name='" + colName + "'" + (colValue ? " checked" : "") + " /></td>";
+        } else {
+          // Use a hidden input so that the values are posted.
+          html += "<td class='" + Settings.DATA_COL_CLASS + "' name='" + colName + "'>"
+                  + colValue + "<input type='hidden' name='" + colName + "' value='" + colValue + "'/></td>";
+        }
 
-        // for values to be posted properly we add a hidden input to this td
-        html += "<input type='hidden' name='" + colName + "' value='" + colValue + "'/>";
-
-        html += "</td>";
       })
 
       if (!isLocked && !setting.read_only) {
@@ -979,8 +1021,12 @@ function makeTable(setting, keypath, setting_value, isLocked) {
                   "'><a href='javascript:void(0);' class='" + Settings.MOVE_UP_SPAN_CLASSES + "'></a>"
                   + "<a href='javascript:void(0);' class='" + Settings.MOVE_DOWN_SPAN_CLASSES + "'></a></td>"
         }
-        html += "<td class='" + Settings.ADD_DEL_BUTTONS_CLASSES +
-                "'><a href='javascript:void(0);' class='" + Settings.DEL_ROW_SPAN_CLASSES + "'></a></td>"
+        if (isNonDeletableRow) {
+          html += "<td></td>";
+        } else {
+          html += "<td class='" + Settings.ADD_DEL_BUTTONS_CLASSES
+                  + "'><a href='javascript:void(0);' class='" + Settings.DEL_ROW_SPAN_CLASSES + "'></a></td>";
+        }
       }
 
       html += "</tr>"
@@ -990,7 +1036,7 @@ function makeTable(setting, keypath, setting_value, isLocked) {
   }
 
   // populate inputs in the table for new values
-  if (!isLocked && !setting.read_only) {
+  if (!isLocked && !setting.read_only && setting.can_add_new_rows) {
      html += makeTableInputs(setting)
   }
   html += "</table>"
@@ -1012,17 +1058,23 @@ function makeTableInputs(setting) {
   }
 
   _.each(setting.columns, function(col) {
-    html += "<td class='" + Settings.DATA_COL_CLASS + "'name='" + col.name + "'>\
-             <input type='text' class='form-control' placeholder='" + (col.placeholder ? col.placeholder : "") + "'\
-             value='" + (col.default ? col.default : "") + "' data-default='" + (col.default ? col.default : "") + "'>\
-             </td>"
+    if (col.type === "checkbox") {
+      html += "<td class='" + Settings.DATA_COL_CLASS + "'name='" + col.name + "'>"
+              + "<input type='checkbox' class='form-control table-checkbox' "
+              + "name='" + col.name + "'" + (col.default ? " checked" : "") + "/></td>";
+    } else {
+      html += "<td class='" + Settings.DATA_COL_CLASS + "'name='" + col.name + "'>\
+               <input type='text' class='form-control' placeholder='" + (col.placeholder ? col.placeholder : "") + "'\
+               value='" + (col.default ? col.default : "") + "' data-default='" + (col.default ? col.default : "") + "'>\
+               </td>"
+    }
   })
 
   if (setting.can_order) {
     html += "<td class='" + Settings.REORDER_BUTTONS_CLASSES + "'></td>"
   }
-    html += "<td class='" + Settings.ADD_DEL_BUTTONS_CLASSES +
-            "'><a href='javascript:void(0);' class='glyphicon glyphicon-plus " + Settings.ADD_ROW_BUTTON_CLASS + "'></a></td>"
+  html += "<td class='" + Settings.ADD_DEL_BUTTONS_CLASSES +
+    "'><a href='javascript:void(0);' class='glyphicon glyphicon-plus " + Settings.ADD_ROW_BUTTON_CLASS + "'></a></td>"
   html += "</tr>"
 
   return html
@@ -1127,11 +1179,11 @@ function addTableRow(add_glyphicon) {
       } else {
         $(element).html(1)
       }
-  } else if ($(element).hasClass(Settings.REORDER_BUTTONS_CLASS)) {
-    $(element).html("<td class='" + Settings.REORDER_BUTTONS_CLASSES + "'><a href='javascript:void(0);'"
-        + " class='" + Settings.MOVE_UP_SPAN_CLASSES + "'></a><a href='javascript:void(0);' class='"
-        + Settings.MOVE_DOWN_SPAN_CLASSES + "'></span></td>")
-  } else if ($(element).hasClass(Settings.ADD_DEL_BUTTONS_CLASS)) {
+    } else if ($(element).hasClass(Settings.REORDER_BUTTONS_CLASS)) {
+      $(element).html("<td class='" + Settings.REORDER_BUTTONS_CLASSES + "'><a href='javascript:void(0);'"
+                      + " class='" + Settings.MOVE_UP_SPAN_CLASSES + "'></a><a href='javascript:void(0);' class='"
+                      + Settings.MOVE_DOWN_SPAN_CLASSES + "'></span></td>")
+    } else if ($(element).hasClass(Settings.ADD_DEL_BUTTONS_CLASS)) {
       // Change buttons
       var anchor = $(element).children("a")
       anchor.removeClass(Settings.ADD_ROW_SPAN_CLASSES)
@@ -1142,8 +1194,20 @@ function addTableRow(add_glyphicon) {
       input.remove()
     } else if ($(element).hasClass(Settings.DATA_COL_CLASS)) {
       // Hide inputs
-      var input = $(element).children("input")
-      input.attr("type", "hidden")
+      var input = $(element).find("input")
+      var isCheckbox = false;
+      if (input.hasClass("table-checkbox")) {
+        input = $(input).parent();
+        isCheckbox = true;
+      }
+
+      var val = input.val();
+      if (isCheckbox) {
+        val = $(input).find("input").is(':checked');
+        // don't hide the checkbox
+      } else {
+        input.attr("type", "hidden")
+      }
 
       if (isArray) {
         var row_index = row.siblings('.' + Settings.DATA_ROW_CLASS).length
@@ -1152,14 +1216,22 @@ function addTableRow(add_glyphicon) {
         // are there multiple columns or just one?
         // with multiple we have an array of Objects, with one we have an array of whatever the value type is
         var num_columns = row.children('.' + Settings.DATA_COL_CLASS).length
-        input.attr("name", setting_name + "[" + row_index + "]" + (num_columns > 1 ? "." + key : ""))
+
+        if (isCheckbox) {
+          $(input).find("input").attr("name", setting_name + "[" + row_index + "]" + (num_columns > 1 ? "." + key : ""))
+        } else {
+          input.attr("name", setting_name + "[" + row_index + "]" + (num_columns > 1 ? "." + key : ""))
+        }
       } else {
         input.attr("name", full_name + "." + $(element).attr("name"))
       }
 
-      input.attr("data-changed", "true")
-
-      $(element).append(input.val())
+      if (isCheckbox) {
+        $(input).find("input").attr("data-changed", "true");
+      } else {
+        input.attr("data-changed", "true");
+        $(element).append(val);
+      }
     } else {
       console.log("Unknown table element")
     }
diff --git a/domain-server/src/DomainGatekeeper.cpp b/domain-server/src/DomainGatekeeper.cpp
index b940d46849..c4a7d1a425 100644
--- a/domain-server/src/DomainGatekeeper.cpp
+++ b/domain-server/src/DomainGatekeeper.cpp
@@ -26,7 +26,7 @@ using SharedAssignmentPointer = QSharedPointer<Assignment>;
 DomainGatekeeper::DomainGatekeeper(DomainServer* server) :
     _server(server)
 {
-    
+
 }
 
 void DomainGatekeeper::addPendingAssignedNode(const QUuid& nodeUUID, const QUuid& assignmentUUID,
@@ -38,7 +38,7 @@ void DomainGatekeeper::addPendingAssignedNode(const QUuid& nodeUUID, const QUuid
 
 QUuid DomainGatekeeper::assignmentUUIDForPendingAssignment(const QUuid& tempUUID) {
     auto it = _pendingAssignedNodes.find(tempUUID);
-    
+
     if (it != _pendingAssignedNodes.end()) {
         return it->second.getAssignmentUUID();
     } else {
@@ -62,59 +62,56 @@ void DomainGatekeeper::processConnectRequestPacket(QSharedPointer<ReceivedMessag
 
     QByteArray myProtocolVersion = protocolVersionsSignature();
     if (nodeConnection.protocolVersion != myProtocolVersion) {
-        QString protocolVersionError = "Protocol version mismatch - Domain version:" + QCoreApplication::applicationVersion();
-        qDebug() << "Protocol Version mismatch - denying connection.";
-        sendConnectionDeniedPacket(protocolVersionError, message->getSenderSockAddr(), 
-                DomainHandler::ConnectionRefusedReason::ProtocolMismatch);
+        sendProtocolMismatchConnectionDenial(message->getSenderSockAddr());
         return;
     }
-    
+
     if (nodeConnection.localSockAddr.isNull() || nodeConnection.publicSockAddr.isNull()) {
         qDebug() << "Unexpected data received for node local socket or public socket. Will not allow connection.";
         return;
     }
-    
+
     static const NodeSet VALID_NODE_TYPES {
         NodeType::AudioMixer, NodeType::AvatarMixer, NodeType::AssetServer, NodeType::EntityServer, NodeType::Agent, NodeType::MessagesMixer
     };
-    
+
     if (!VALID_NODE_TYPES.contains(nodeConnection.nodeType)) {
         qDebug() << "Received an invalid node type with connect request. Will not allow connection from"
             << nodeConnection.senderSockAddr << ": " << nodeConnection.nodeType;
         return;
     }
-    
+
     // check if this connect request matches an assignment in the queue
     auto pendingAssignment = _pendingAssignedNodes.find(nodeConnection.connectUUID);
-    
+
     SharedNodePointer node;
-    
+
     if (pendingAssignment != _pendingAssignedNodes.end()) {
         node = processAssignmentConnectRequest(nodeConnection, pendingAssignment->second);
     } else if (!STATICALLY_ASSIGNED_NODES.contains(nodeConnection.nodeType)) {
         QString username;
         QByteArray usernameSignature;
-        
+
         if (message->getBytesLeftToRead() > 0) {
             // read username from packet
             packetStream >> username;
-            
+
             if (message->getBytesLeftToRead() > 0) {
                 // read user signature from packet
                 packetStream >> usernameSignature;
             }
         }
-        
+
         node = processAgentConnectRequest(nodeConnection, username, usernameSignature);
     }
-    
+
     if (node) {
         // set the sending sock addr and node interest set on this node
         DomainServerNodeData* nodeData = reinterpret_cast<DomainServerNodeData*>(node->getLinkedData());
         nodeData->setSendingSockAddr(message->getSenderSockAddr());
         nodeData->setNodeInterestSet(nodeConnection.interestList.toSet());
         nodeData->setPlaceName(nodeConnection.placeName);
-        
+
         // signal that we just connected a node so the DomainServer can get it a list
         // and broadcast its presence right away
         emit connectedNode(node);
@@ -123,18 +120,72 @@ void DomainGatekeeper::processConnectRequestPacket(QSharedPointer<ReceivedMessag
     }
 }
 
+void DomainGatekeeper::updateNodePermissions() {
+    // If the permissions were changed on the domain-server webpage (and nothing else was), a restart isn't required --
+    // we reprocess the permissions map and update the nodes here.  The node list is frequently sent out to all
+    // the connected nodes, so these changes are propagated to other nodes.
+
+    QList<SharedNodePointer> nodesToKill;
+
+    auto limitedNodeList = DependencyManager::get<LimitedNodeList>();
+    limitedNodeList->eachNode([this, limitedNodeList, &nodesToKill](const SharedNodePointer& node){
+        QString username = node->getPermissions().getUserName();
+        NodePermissions userPerms(username);
+
+        if (node->getPermissions().isAssignment) {
+            // this node is an assignment-client
+            userPerms.isAssignment = true;
+            userPerms.canAdjustLocks = true;
+            userPerms.canRezPermanentEntities = true;
+            userPerms.canRezTemporaryEntities = true;
+        } else {
+            // this node is an agent
+            userPerms.setAll(false);
+
+            const QHostAddress& addr = node->getLocalSocket().getAddress();
+            bool isLocalUser = (addr == limitedNodeList->getLocalSockAddr().getAddress() ||
+                                addr == QHostAddress::LocalHost);
+            if (isLocalUser) {
+                userPerms |= _server->_settingsManager.getStandardPermissionsForName(NodePermissions::standardNameLocalhost);
+            }
+
+            if (username.isEmpty()) {
+                userPerms |= _server->_settingsManager.getStandardPermissionsForName(NodePermissions::standardNameAnonymous);
+            } else {
+                if (_server->_settingsManager.havePermissionsForName(username)) {
+                    userPerms = _server->_settingsManager.getPermissionsForName(username);
+                } else {
+                    userPerms |= _server->_settingsManager.getStandardPermissionsForName(NodePermissions::standardNameLoggedIn);
+                }
+            }
+        }
+
+        node->setPermissions(userPerms);
+
+        if (!userPerms.canConnectToDomain) {
+            qDebug() << "node" << node->getUUID() << "no longer has permission to connect.";
+            // hang up on this node
+            nodesToKill << node;
+        }
+    });
+
+    foreach (auto node, nodesToKill) {
+        emit killNode(node);
+    }
+}
+
 SharedNodePointer DomainGatekeeper::processAssignmentConnectRequest(const NodeConnectionData& nodeConnection,
                                                                     const PendingAssignedNodeData& pendingAssignment) {
-    
+
     // make sure this matches an assignment the DS told us we sent out
     auto it = _pendingAssignedNodes.find(nodeConnection.connectUUID);
-    
+
     SharedAssignmentPointer matchingQueuedAssignment = SharedAssignmentPointer();
-    
+
     if (it != _pendingAssignedNodes.end()) {
         // find the matching queued static assignment in DS queue
         matchingQueuedAssignment = _server->dequeueMatchingAssignment(it->second.getAssignmentUUID(), nodeConnection.nodeType);
-        
+
         if (matchingQueuedAssignment) {
             qDebug() << "Assignment deployed with" << uuidStringWithoutCurlyBraces(nodeConnection.connectUUID)
                 << "matches unfulfilled assignment"
@@ -149,124 +200,99 @@ SharedNodePointer DomainGatekeeper::processAssignmentConnectRequest(const NodeCo
         qDebug() << "No assignment was deployed with UUID" << uuidStringWithoutCurlyBraces(nodeConnection.connectUUID);
         return SharedNodePointer();
     }
-    
+
     // add the new node
     SharedNodePointer newNode = addVerifiedNodeFromConnectRequest(nodeConnection);
-    
+
     DomainServerNodeData* nodeData = reinterpret_cast<DomainServerNodeData*>(newNode->getLinkedData());
-    
+
     // set assignment related data on the linked data for this node
     nodeData->setAssignmentUUID(matchingQueuedAssignment->getUUID());
     nodeData->setWalletUUID(it->second.getWalletUUID());
     nodeData->setNodeVersion(it->second.getNodeVersion());
     nodeData->setWasAssigned(true);
-    
+
     // cleanup the PendingAssignedNodeData for this assignment now that it's connecting
     _pendingAssignedNodes.erase(it);
-    
+
     // always allow assignment clients to create and destroy entities
-    newNode->setIsAllowedEditor(true);
-    newNode->setCanRez(true);
-    
+    NodePermissions userPerms;
+    userPerms.isAssignment = true;
+    userPerms.canAdjustLocks = true;
+    userPerms.canRezPermanentEntities = true;
+    userPerms.canRezTemporaryEntities = true;
+    newNode->setPermissions(userPerms);
     return newNode;
 }
 
 const QString MAXIMUM_USER_CAPACITY = "security.maximum_user_capacity";
-const QString ALLOWED_EDITORS_SETTINGS_KEYPATH = "security.allowed_editors";
-const QString EDITORS_ARE_REZZERS_KEYPATH = "security.editors_are_rezzers";
 
 SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnectionData& nodeConnection,
                                                                const QString& username,
                                                                const QByteArray& usernameSignature) {
-    
+
     auto limitedNodeList = DependencyManager::get<LimitedNodeList>();
-    
-    bool isRestrictingAccess =
-        _server->_settingsManager.valueOrDefaultValueForKeyPath(RESTRICTED_ACCESS_SETTINGS_KEYPATH).toBool();
-    
-    // check if this user is on our local machine - if this is true they are always allowed to connect
+
+    // start with empty permissions
+    NodePermissions userPerms(username);
+    userPerms.setAll(false);
+
+    // check if this user is on our local machine - if this is true set permissions to those for a "localhost" connection
     QHostAddress senderHostAddress = nodeConnection.senderSockAddr.getAddress();
     bool isLocalUser =
         (senderHostAddress == limitedNodeList->getLocalSockAddr().getAddress() || senderHostAddress == QHostAddress::LocalHost);
-    
-    // if we're using restricted access and this user is not local make sure we got a user signature
-    if (isRestrictingAccess && !isLocalUser) {
-        if (!username.isEmpty()) {
-            if (usernameSignature.isEmpty()) {
-                // if user didn't include usernameSignature in connect request, send a connectionToken packet
-                sendConnectionTokenPacket(username, nodeConnection.senderSockAddr);
-                
-                // ask for their public key right now to make sure we have it
-                requestUserPublicKey(username);
-                
-                return SharedNodePointer();
-            }
-        }
+    if (isLocalUser) {
+        userPerms |= _server->_settingsManager.getStandardPermissionsForName(NodePermissions::standardNameLocalhost);
+        qDebug() << "user-permissions: is local user, so:" << userPerms;
     }
-    
-    bool verifiedUsername = false;
-    
-    // if we do not have a local user we need to subject them to our verification and capacity checks
-    if (!isLocalUser) {
-        
-        // check if we need to look at the username signature
-        if (isRestrictingAccess) {
-            if (isVerifiedAllowedUser(username, usernameSignature, nodeConnection.senderSockAddr)) {
-                // we verified the user via their username and signature - set the verifiedUsername
-                // so we don't re-decrypt their sig if we're trying to exempt them from max capacity check (due to
-                // being in the allowed editors list)
-                verifiedUsername = true;
-            } else {
-                // failed to verify user - return a null shared ptr
-                return SharedNodePointer();
-            }
-        }
-        
-        if (!isWithinMaxCapacity(username, usernameSignature, verifiedUsername, nodeConnection.senderSockAddr)) {
-            // we can't allow this user to connect because we are at max capacity (and they either aren't an allowed editor
-            // or couldn't be verified as one)
+
+    if (!username.isEmpty() && usernameSignature.isEmpty()) {
+        // user is attempting to prove their identity to us, but we don't have enough information
+        sendConnectionTokenPacket(username, nodeConnection.senderSockAddr);
+        // ask for their public key right now to make sure we have it
+        requestUserPublicKey(username);
+        if (!userPerms.canConnectToDomain) {
             return SharedNodePointer();
         }
     }
-    
-    // if this user is in the editors list (or if the editors list is empty) set the user's node's isAllowedEditor to true
-    const QVariant* allowedEditorsVariant =
-        valueForKeyPath(_server->_settingsManager.getSettingsMap(), ALLOWED_EDITORS_SETTINGS_KEYPATH);
-    QStringList allowedEditors = allowedEditorsVariant ? allowedEditorsVariant->toStringList() : QStringList();
-    
-    // if the allowed editors list is empty then everyone can adjust locks
-    bool isAllowedEditor = allowedEditors.empty();
-    
-    if (allowedEditors.contains(username, Qt::CaseInsensitive)) {
-        // we have a non-empty allowed editors list - check if this user is verified to be in it
-        if (!verifiedUsername) {
-            if (!verifyUserSignature(username, usernameSignature, HifiSockAddr())) {
-                // failed to verify a user that is in the allowed editors list
-                
-                // TODO: fix public key refresh in interface/metaverse and force this check
-                qDebug() << "Could not verify user" << username << "as allowed editor. In the interim this user"
-                    << "will be given edit rights to avoid a thrasing of public key requests and connect requests.";
-            }
-            
-            isAllowedEditor = true;
+
+    if (username.isEmpty()) {
+        // they didn't tell us who they are
+        userPerms |= _server->_settingsManager.getStandardPermissionsForName(NodePermissions::standardNameAnonymous);
+        qDebug() << "user-permissions: no username, so:" << userPerms;
+    } else if (verifyUserSignature(username, usernameSignature, nodeConnection.senderSockAddr)) {
+        // they are sent us a username and the signature verifies it
+        if (_server->_settingsManager.havePermissionsForName(username)) {
+            // we have specific permissions for this user.
+            userPerms = _server->_settingsManager.getPermissionsForName(username);
+            qDebug() << "user-permissions: specific user matches, so:" << userPerms;
         } else {
-            // already verified this user and they are in the allowed editors list
-            isAllowedEditor = true;
+            // they are logged into metaverse, but we don't have specific permissions for them.
+            userPerms |= _server->_settingsManager.getStandardPermissionsForName(NodePermissions::standardNameLoggedIn);
+            qDebug() << "user-permissions: user is logged in, so:" << userPerms;
+        }
+        userPerms.setUserName(username);
+    } else {
+        // they sent us a username, but it didn't check out
+        requestUserPublicKey(username);
+        if (!userPerms.canConnectToDomain) {
+            return SharedNodePointer();
         }
     }
-    
-    // check if only editors should be able to rez entities
-    const QVariant* editorsAreRezzersVariant =
-        valueForKeyPath(_server->_settingsManager.getSettingsMap(), EDITORS_ARE_REZZERS_KEYPATH);
-    
-    bool onlyEditorsAreRezzers = false;
-    if (editorsAreRezzersVariant) {
-        onlyEditorsAreRezzers = editorsAreRezzersVariant->toBool();
+
+    qDebug() << "user-permissions: final:" << userPerms;
+
+    if (!userPerms.canConnectToDomain) {
+        sendConnectionDeniedPacket("You lack the required permissions to connect to this domain.",
+                                   nodeConnection.senderSockAddr, DomainHandler::ConnectionRefusedReason::TooManyUsers);
+        return SharedNodePointer();
     }
-    
-    bool canRez = true;
-    if (onlyEditorsAreRezzers) {
-        canRez = isAllowedEditor;
+
+    if (!userPerms.canConnectPastMaxCapacity && !isWithinMaxCapacity()) {
+        // we can't allow this user to connect because we are at max capacity
+        sendConnectionDeniedPacket("Too many connected users.", nodeConnection.senderSockAddr,
+                                   DomainHandler::ConnectionRefusedReason::TooManyUsers);
+        return SharedNodePointer();
     }
 
     QUuid hintNodeID;
@@ -285,24 +311,23 @@ SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnect
 
         return true;
     });
-    
+
     // add the connecting node (or re-use the matched one from eachNodeBreakable above)
     SharedNodePointer newNode = addVerifiedNodeFromConnectRequest(nodeConnection, hintNodeID);
-    
+
     // set the edit rights for this user
-    newNode->setIsAllowedEditor(isAllowedEditor);
-    newNode->setCanRez(canRez);
+    newNode->setPermissions(userPerms);
 
     // grab the linked data for our new node so we can set the username
     DomainServerNodeData* nodeData = reinterpret_cast<DomainServerNodeData*>(newNode->getLinkedData());
-    
+
     // if we have a username from the connect request, set it on the DomainServerNodeData
     nodeData->setUsername(username);
-    
+
     // also add an interpolation to DomainServerNodeData so that servers can get username in stats
     nodeData->addOverrideForKey(USERNAME_UUID_REPLACEMENT_STATS_KEY,
                                 uuidStringWithoutCurlyBraces(newNode->getUUID()), username);
-    
+
     return newNode;
 }
 
@@ -310,11 +335,11 @@ SharedNodePointer DomainGatekeeper::addVerifiedNodeFromConnectRequest(const Node
                                                                       QUuid nodeID) {
     HifiSockAddr discoveredSocket = nodeConnection.senderSockAddr;
     SharedNetworkPeer connectedPeer = _icePeers.value(nodeConnection.connectUUID);
-    
+
     if (connectedPeer) {
         //  this user negotiated a connection with us via ICE, so re-use their ICE client ID
         nodeID = nodeConnection.connectUUID;
-        
+
         if (connectedPeer->getActiveSocket()) {
             // set their discovered socket to whatever the activated socket on the network peer object was
             discoveredSocket = *connectedPeer->getActiveSocket();
@@ -325,15 +350,15 @@ SharedNodePointer DomainGatekeeper::addVerifiedNodeFromConnectRequest(const Node
             nodeID = QUuid::createUuid();
         }
     }
-    
+
     auto limitedNodeList = DependencyManager::get<LimitedNodeList>();
-    
+
     SharedNodePointer newNode = limitedNodeList->addOrUpdateNode(nodeID, nodeConnection.nodeType,
                                                                  nodeConnection.publicSockAddr, nodeConnection.localSockAddr);
-    
+
     // So that we can send messages to this node at will - we need to activate the correct socket on this node now
     newNode->activateMatchingOrNewSymmetricSocket(discoveredSocket);
-    
+
     return newNode;
 }
 
@@ -343,21 +368,21 @@ bool DomainGatekeeper::verifyUserSignature(const QString& username,
 
     // it's possible this user can be allowed to connect, but we need to check their username signature
     QByteArray publicKeyArray = _userPublicKeys.value(username);
-    
+
     const QUuid& connectionToken = _connectionTokenHash.value(username.toLower());
-    
+
     if (!publicKeyArray.isEmpty() && !connectionToken.isNull()) {
         // if we do have a public key for the user, check for a signature match
-        
+
         const unsigned char* publicKeyData = reinterpret_cast<const unsigned char*>(publicKeyArray.constData());
-        
+
         // first load up the public key into an RSA struct
         RSA* rsaPublicKey = d2i_RSA_PUBKEY(NULL, &publicKeyData, publicKeyArray.size());
-        
+
         QByteArray lowercaseUsername = username.toLower().toUtf8();
         QByteArray usernameWithToken = QCryptographicHash::hash(lowercaseUsername.append(connectionToken.toRfc4122()),
                                                                 QCryptographicHash::Sha256);
-        
+
         if (rsaPublicKey) {
             int decryptResult = RSA_verify(NID_sha256,
                                            reinterpret_cast<const unsigned char*>(usernameWithToken.constData()),
@@ -365,29 +390,29 @@ bool DomainGatekeeper::verifyUserSignature(const QString& username,
                                            reinterpret_cast<const unsigned char*>(usernameSignature.constData()),
                                            usernameSignature.size(),
                                            rsaPublicKey);
-            
+
             if (decryptResult == 1) {
-                qDebug() << "Username signature matches for" << username << "- allowing connection.";
-                
+                qDebug() << "Username signature matches for" << username;
+
                 // free up the public key and remove connection token before we return
                 RSA_free(rsaPublicKey);
                 _connectionTokenHash.remove(username);
-                
+
                 return true;
-                
+
             } else {
                 if (!senderSockAddr.isNull()) {
                     qDebug() << "Error decrypting username signature for " << username << "- denying connection.";
                     sendConnectionDeniedPacket("Error decrypting username signature.", senderSockAddr,
                         DomainHandler::ConnectionRefusedReason::LoginError);
                 }
-                
+
                 // free up the public key, we don't need it anymore
                 RSA_free(rsaPublicKey);
             }
-            
+
         } else {
-            
+
             // we can't let this user in since we couldn't convert their public key to an RSA key we could use
             if (!senderSockAddr.isNull()) {
                 qDebug() << "Couldn't convert data to RSA key for" << username << "- denying connection.";
@@ -402,86 +427,35 @@ bool DomainGatekeeper::verifyUserSignature(const QString& username,
                 DomainHandler::ConnectionRefusedReason::LoginError);
         }
     }
-    
+
     requestUserPublicKey(username); // no joy.  maybe next time?
     return false;
 }
 
-bool DomainGatekeeper::isVerifiedAllowedUser(const QString& username, const QByteArray& usernameSignature,
-                                             const HifiSockAddr& senderSockAddr) {
-    
-    if (username.isEmpty()) {
-        qDebug() << "Connect request denied - no username provided.";
-        
-        sendConnectionDeniedPacket("No username provided", senderSockAddr,
-            DomainHandler::ConnectionRefusedReason::LoginError);
-        
-        return false;
-    }
-    
-    QStringList allowedUsers =
-        _server->_settingsManager.valueOrDefaultValueForKeyPath(ALLOWED_USERS_SETTINGS_KEYPATH).toStringList();
-    
-    if (allowedUsers.contains(username, Qt::CaseInsensitive)) {
-        if (!verifyUserSignature(username, usernameSignature, senderSockAddr)) {
-            return false;
-        }
-    } else {
-        qDebug() << "Connect request denied for user" << username << "- not in allowed users list.";
-        sendConnectionDeniedPacket("User not on whitelist.", senderSockAddr,
-            DomainHandler::ConnectionRefusedReason::NotAuthorized);
-        
-        return false;
-    }
-    
-    return true;
-}
-
-bool DomainGatekeeper::isWithinMaxCapacity(const QString& username, const QByteArray& usernameSignature,
-                                           bool& verifiedUsername,
-                                           const HifiSockAddr& senderSockAddr) {
+bool DomainGatekeeper::isWithinMaxCapacity() {
     // find out what our maximum capacity is
-    const QVariant* maximumUserCapacityVariant = valueForKeyPath(_server->_settingsManager.getSettingsMap(), MAXIMUM_USER_CAPACITY);
+    const QVariant* maximumUserCapacityVariant =
+        valueForKeyPath(_server->_settingsManager.getSettingsMap(), MAXIMUM_USER_CAPACITY);
     unsigned int maximumUserCapacity = maximumUserCapacityVariant ? maximumUserCapacityVariant->toUInt() : 0;
 
     if (maximumUserCapacity > 0) {
         unsigned int connectedUsers = _server->countConnectedUsers();
 
         if (connectedUsers >= maximumUserCapacity) {
-            // too many users, deny the new connection unless this user is an allowed editor
-            
-            const QVariant* allowedEditorsVariant =
-                valueForKeyPath(_server->_settingsManager.getSettingsMap(), ALLOWED_EDITORS_SETTINGS_KEYPATH);
-            
-            QStringList allowedEditors = allowedEditorsVariant ? allowedEditorsVariant->toStringList() : QStringList();
-            if (allowedEditors.contains(username)) {
-                if (verifiedUsername || verifyUserSignature(username, usernameSignature, senderSockAddr)) {
-                    verifiedUsername = true;
-                    qDebug() << "Above maximum capacity -" << connectedUsers << "/" << maximumUserCapacity <<
-                        "but user" << username << "is in allowed editors list so will be allowed to connect.";
-                    return true;
-                }
-            }
-            
-            // deny connection from this user
             qDebug() << connectedUsers << "/" << maximumUserCapacity << "users connected, denying new connection.";
-            sendConnectionDeniedPacket("Too many connected users.", senderSockAddr,
-                DomainHandler::ConnectionRefusedReason::TooManyUsers);
-            
             return false;
         }
-        
+
         qDebug() << connectedUsers << "/" << maximumUserCapacity << "users connected, allowing new connection.";
     }
-    
+
     return true;
 }
 
 
 void DomainGatekeeper::preloadAllowedUserPublicKeys() {
-    const QVariant* allowedUsersVariant = valueForKeyPath(_server->_settingsManager.getSettingsMap(), ALLOWED_USERS_SETTINGS_KEYPATH);
-    QStringList allowedUsers = allowedUsersVariant ? allowedUsersVariant->toStringList() : QStringList();
-    
+    QStringList allowedUsers = _server->_settingsManager.getAllNames();
+
     if (allowedUsers.size() > 0) {
         // in the future we may need to limit how many requests here - for now assume that lists of allowed users are not
         // going to create > 100 requests
@@ -492,15 +466,20 @@ void DomainGatekeeper::preloadAllowedUserPublicKeys() {
 }
 
 void DomainGatekeeper::requestUserPublicKey(const QString& username) {
+    // don't request public keys for the standard psuedo-account-names
+    if (NodePermissions::standardNames.contains(username, Qt::CaseInsensitive)) {
+        return;
+    }
+
     // even if we have a public key for them right now, request a new one in case it has just changed
     JSONCallbackParameters callbackParams;
     callbackParams.jsonCallbackReceiver = this;
     callbackParams.jsonCallbackMethod = "publicKeyJSONCallback";
-    
+
     const QString USER_PUBLIC_KEY_PATH = "api/v1/users/%1/public_key";
-    
+
     qDebug() << "Requesting public key for user" << username;
-    
+
     DependencyManager::get<AccountManager>()->sendRequest(USER_PUBLIC_KEY_PATH.arg(username),
                                               AccountManagerAuth::None,
                                               QNetworkAccessManager::GetOperation, callbackParams);
@@ -508,38 +487,47 @@ void DomainGatekeeper::requestUserPublicKey(const QString& username) {
 
 void DomainGatekeeper::publicKeyJSONCallback(QNetworkReply& requestReply) {
     QJsonObject jsonObject = QJsonDocument::fromJson(requestReply.readAll()).object();
-    
+
     if (jsonObject["status"].toString() == "success") {
         // figure out which user this is for
-        
+
         const QString PUBLIC_KEY_URL_REGEX_STRING = "api\\/v1\\/users\\/([A-Za-z0-9_\\.]+)\\/public_key";
         QRegExp usernameRegex(PUBLIC_KEY_URL_REGEX_STRING);
-        
+
         if (usernameRegex.indexIn(requestReply.url().toString()) != -1) {
             QString username = usernameRegex.cap(1);
-            
+
             qDebug() << "Storing a public key for user" << username;
-            
+
             // pull the public key as a QByteArray from this response
             const QString JSON_DATA_KEY = "data";
             const QString JSON_PUBLIC_KEY_KEY = "public_key";
-            
+
             _userPublicKeys[username] =
                 QByteArray::fromBase64(jsonObject[JSON_DATA_KEY].toObject()[JSON_PUBLIC_KEY_KEY].toString().toUtf8());
         }
     }
 }
 
-void DomainGatekeeper::sendConnectionDeniedPacket(const QString& reason, const HifiSockAddr& senderSockAddr, 
-            DomainHandler::ConnectionRefusedReason reasonCode) {
+void DomainGatekeeper::sendProtocolMismatchConnectionDenial(const HifiSockAddr& senderSockAddr) {
+    QString protocolVersionError = "Protocol version mismatch - Domain version: " + QCoreApplication::applicationVersion();
+
+    qDebug() << "Protocol Version mismatch - denying connection.";
+
+    sendConnectionDeniedPacket(protocolVersionError, senderSockAddr,
+                               DomainHandler::ConnectionRefusedReason::ProtocolMismatch);
+}
+
+void DomainGatekeeper::sendConnectionDeniedPacket(const QString& reason, const HifiSockAddr& senderSockAddr,
+                                                  DomainHandler::ConnectionRefusedReason reasonCode) {
     // this is an agent and we've decided we won't let them connect - send them a packet to deny connection
     QByteArray utfString = reason.toUtf8();
     quint16 payloadSize = utfString.size();
-   
+
     // setup the DomainConnectionDenied packet
-    auto connectionDeniedPacket = NLPacket::create(PacketType::DomainConnectionDenied, 
-                                                payloadSize + sizeof(payloadSize) + sizeof(uint8_t));
-    
+    auto connectionDeniedPacket = NLPacket::create(PacketType::DomainConnectionDenied,
+                                                   payloadSize + sizeof(payloadSize) + sizeof(uint8_t));
+
     // pack in the reason the connection was denied (the client displays this)
     if (payloadSize > 0) {
         uint8_t reasonCodeWire = (uint8_t)reasonCode;
@@ -547,7 +535,7 @@ void DomainGatekeeper::sendConnectionDeniedPacket(const QString& reason, const H
         connectionDeniedPacket->writePrimitive(payloadSize);
         connectionDeniedPacket->write(utfString);
     }
-    
+
     // send the packet off
     DependencyManager::get<LimitedNodeList>()->sendPacket(std::move(connectionDeniedPacket), senderSockAddr);
 }
@@ -555,20 +543,20 @@ void DomainGatekeeper::sendConnectionDeniedPacket(const QString& reason, const H
 void DomainGatekeeper::sendConnectionTokenPacket(const QString& username, const HifiSockAddr& senderSockAddr) {
     // get the existing connection token or create a new one
     QUuid& connectionToken = _connectionTokenHash[username.toLower()];
-    
+
     if (connectionToken.isNull()) {
         connectionToken = QUuid::createUuid();
     }
-    
+
     // setup a static connection token packet
     static auto connectionTokenPacket = NLPacket::create(PacketType::DomainServerConnectionToken, NUM_BYTES_RFC4122_UUID);
-    
+
     // reset the packet before each time we send
     connectionTokenPacket->reset();
-    
+
     // write the connection token
     connectionTokenPacket->write(connectionToken.toRfc4122());
-    
+
     // send off the packet unreliably
     DependencyManager::get<LimitedNodeList>()->sendUnreliablePacket(*connectionTokenPacket, senderSockAddr);
 }
@@ -576,33 +564,33 @@ void DomainGatekeeper::sendConnectionTokenPacket(const QString& username, const
 const int NUM_PEER_PINGS_BEFORE_DELETE = 2000 / UDP_PUNCH_PING_INTERVAL_MS;
 
 void DomainGatekeeper::pingPunchForConnectingPeer(const SharedNetworkPeer& peer) {
-    
+
     if (peer->getConnectionAttempts() >= NUM_PEER_PINGS_BEFORE_DELETE) {
         // we've reached the maximum number of ping attempts
         qDebug() << "Maximum number of ping attempts reached for peer with ID" << peer->getUUID();
         qDebug() << "Removing from list of connecting peers.";
-        
+
         _icePeers.remove(peer->getUUID());
     } else {
         auto limitedNodeList = DependencyManager::get<LimitedNodeList>();
-        
+
         // send the ping packet to the local and public sockets for this node
         auto localPingPacket = limitedNodeList->constructICEPingPacket(PingType::Local, limitedNodeList->getSessionUUID());
         limitedNodeList->sendPacket(std::move(localPingPacket), peer->getLocalSocket());
-        
+
         auto publicPingPacket = limitedNodeList->constructICEPingPacket(PingType::Public, limitedNodeList->getSessionUUID());
         limitedNodeList->sendPacket(std::move(publicPingPacket), peer->getPublicSocket());
-        
+
         peer->incrementConnectionAttempts();
     }
 }
 
 void DomainGatekeeper::handlePeerPingTimeout() {
     NetworkPeer* senderPeer = qobject_cast<NetworkPeer*>(sender());
-    
+
     if (senderPeer) {
         SharedNetworkPeer sharedPeer = _icePeers.value(senderPeer->getUUID());
-        
+
         if (sharedPeer && !sharedPeer->getActiveSocket()) {
             pingPunchForConnectingPeer(sharedPeer);
         }
@@ -613,24 +601,24 @@ void DomainGatekeeper::processICEPeerInformationPacket(QSharedPointer<ReceivedMe
     // loop through the packet and pull out network peers
     // any peer we don't have we add to the hash, otherwise we update
     QDataStream iceResponseStream(message->getMessage());
-    
+
     NetworkPeer* receivedPeer = new NetworkPeer;
     iceResponseStream >> *receivedPeer;
-    
+
     if (!_icePeers.contains(receivedPeer->getUUID())) {
         qDebug() << "New peer requesting ICE connection being added to hash -" << *receivedPeer;
         SharedNetworkPeer newPeer = SharedNetworkPeer(receivedPeer);
         _icePeers[receivedPeer->getUUID()] = newPeer;
-        
+
         // make sure we know when we should ping this peer
         connect(newPeer.data(), &NetworkPeer::pingTimerTimeout, this, &DomainGatekeeper::handlePeerPingTimeout);
-        
+
         // immediately ping the new peer, and start a timer to continue pinging it until we connect to it
         newPeer->startPingTimer();
-        
+
         qDebug() << "Sending ping packets to establish connectivity with ICE peer with ID"
             << newPeer->getUUID();
-        
+
         pingPunchForConnectingPeer(newPeer);
     } else {
         delete receivedPeer;
@@ -640,18 +628,18 @@ void DomainGatekeeper::processICEPeerInformationPacket(QSharedPointer<ReceivedMe
 void DomainGatekeeper::processICEPingPacket(QSharedPointer<ReceivedMessage> message) {
     auto limitedNodeList = DependencyManager::get<LimitedNodeList>();
     auto pingReplyPacket = limitedNodeList->constructICEPingReplyPacket(*message, limitedNodeList->getSessionUUID());
-    
+
     limitedNodeList->sendPacket(std::move(pingReplyPacket), message->getSenderSockAddr());
 }
 
 void DomainGatekeeper::processICEPingReplyPacket(QSharedPointer<ReceivedMessage> message) {
     QDataStream packetStream(message->getMessage());
-    
+
     QUuid nodeUUID;
     packetStream >> nodeUUID;
-    
+
     SharedNetworkPeer sendingPeer = _icePeers.value(nodeUUID);
-    
+
     if (sendingPeer) {
         // we had this NetworkPeer in our connecting list - add the right sock addr to our connected list
         sendingPeer->activateMatchingOrNewSymmetricSocket(message->getSenderSockAddr());
diff --git a/domain-server/src/DomainGatekeeper.h b/domain-server/src/DomainGatekeeper.h
index 09e3b04ed7..50bbf38543 100644
--- a/domain-server/src/DomainGatekeeper.h
+++ b/domain-server/src/DomainGatekeeper.h
@@ -42,6 +42,8 @@ public:
     void preloadAllowedUserPublicKeys();
     
     void removeICEPeer(const QUuid& peerUUID) { _icePeers.remove(peerUUID); }
+
+    static void sendProtocolMismatchConnectionDenial(const HifiSockAddr& senderSockAddr);
 public slots:
     void processConnectRequestPacket(QSharedPointer<ReceivedMessage> message);
     void processICEPingPacket(QSharedPointer<ReceivedMessage> message);
@@ -51,8 +53,12 @@ public slots:
     void publicKeyJSONCallback(QNetworkReply& requestReply);
     
 signals:
+    void killNode(SharedNodePointer node);
     void connectedNode(SharedNodePointer node);
-    
+
+public slots:
+    void updateNodePermissions();
+
 private slots:
     void handlePeerPingTimeout();
 private:
@@ -66,18 +72,14 @@ private:
     
     bool verifyUserSignature(const QString& username, const QByteArray& usernameSignature,
                              const HifiSockAddr& senderSockAddr);
-    bool isVerifiedAllowedUser(const QString& username, const QByteArray& usernameSignature,
-                               const HifiSockAddr& senderSockAddr);
-    bool isWithinMaxCapacity(const QString& username, const QByteArray& usernameSignature,
-                             bool& verifiedUsername,
-                             const HifiSockAddr& senderSockAddr);
+    bool isWithinMaxCapacity();
     
     bool shouldAllowConnectionFromNode(const QString& username, const QByteArray& usernameSignature,
                                        const HifiSockAddr& senderSockAddr);
     
     void sendConnectionTokenPacket(const QString& username, const HifiSockAddr& senderSockAddr);
-    void sendConnectionDeniedPacket(const QString& reason, const HifiSockAddr& senderSockAddr, 
-                    DomainHandler::ConnectionRefusedReason reasonCode = DomainHandler::ConnectionRefusedReason::Unknown);
+    static void sendConnectionDeniedPacket(const QString& reason, const HifiSockAddr& senderSockAddr,
+            DomainHandler::ConnectionRefusedReason reasonCode = DomainHandler::ConnectionRefusedReason::Unknown);
     
     void pingPunchForConnectingPeer(const SharedNetworkPeer& peer);
     
diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp
index 0f5498a575..7c596bb187 100644
--- a/domain-server/src/DomainServer.cpp
+++ b/domain-server/src/DomainServer.cpp
@@ -101,6 +101,13 @@ DomainServer::DomainServer(int argc, char* argv[]) :
     // make sure we hear about newly connected nodes from our gatekeeper
     connect(&_gatekeeper, &DomainGatekeeper::connectedNode, this, &DomainServer::handleConnectedNode);
 
+    // if a connected node loses connection privileges, hang up on it
+    connect(&_gatekeeper, &DomainGatekeeper::killNode, this, &DomainServer::handleKillNode);
+
+    // if permissions are updated, relay the changes to the Node datastructures
+    connect(&_settingsManager, &DomainServerSettingsManager::updateNodePermissions,
+            &_gatekeeper, &DomainGatekeeper::updateNodePermissions);
+
     if (optionallyReadX509KeyAndCertificate() && optionallySetupOAuth()) {
         // we either read a certificate and private key or were not passed one
         // and completed login or did not need to
@@ -318,16 +325,11 @@ bool DomainServer::packetVersionMatch(const udt::Packet& packet) {
 
     auto nodeList = DependencyManager::get<LimitedNodeList>();
 
-    // This implements a special case that handles OLD clients which don't know how to negotiate matching
-    // protocol versions. We know these clients will sent DomainConnectRequest with older versions. We also
-    // know these clients will show a warning dialog if they get an EntityData with a protocol version they
-    // don't understand, so we can send them an empty EntityData with our latest version and they will
-    // warn the user that the protocol is not compatible
-    if (headerType == PacketType::DomainConnectRequest &&
-        headerVersion < static_cast<PacketVersion>(DomainConnectRequestVersion::HasProtocolVersions)) {
-        auto packetWithBadVersion = NLPacket::create(PacketType::EntityData);
-        nodeList->sendPacket(std::move(packetWithBadVersion), packet.getSenderSockAddr());
-        return false;
+    // if this is a mismatching connect packet, we can't simply drop it on the floor
+    // send back a packet to the interface that tells them we refuse connection for a mismatch
+    if (headerType == PacketType::DomainConnectRequest
+        && headerVersion != versionForPacketType(PacketType::DomainConnectRequest)) {
+        DomainGatekeeper::sendProtocolMismatchConnectionDenial(packet.getSenderSockAddr());
     }
 
     // let the normal nodeList implementation handle all other packets.
@@ -800,8 +802,7 @@ void DomainServer::sendDomainListToNode(const SharedNodePointer& node, const Hif
     
     extendedHeaderStream << limitedNodeList->getSessionUUID();
     extendedHeaderStream << node->getUUID();
-    extendedHeaderStream << (quint8) node->isAllowedEditor();
-    extendedHeaderStream << (quint8) node->getCanRez();
+    extendedHeaderStream << node->getPermissions();
 
     auto domainListPackets = NLPacketList::create(PacketType::DomainList, extendedHeader);
 
@@ -1093,11 +1094,12 @@ void DomainServer::sendHeartbeatToMetaverse(const QString& networkAddress) {
     static const QString AUTOMATIC_NETWORKING_KEY = "automatic_networking";
     domainObject[AUTOMATIC_NETWORKING_KEY] = _automaticNetworkingSetting;
 
-    // Add a flag to indicate if this domain uses restricted access -
-    // for now that will exclude it from listings
-    static const QString RESTRICTED_ACCESS_FLAG = "restricted";
-    domainObject[RESTRICTED_ACCESS_FLAG] =
-        _settingsManager.valueOrDefaultValueForKeyPath(RESTRICTED_ACCESS_SETTINGS_KEYPATH).toBool();
+    // add a flag to indicate if this domain uses restricted access - for now that will exclude it from listings
+    const QString RESTRICTED_ACCESS_FLAG = "restricted";
+
+    // consider the domain to have restricted access if "anonymous" connections can't connect to the domain.
+    NodePermissions anonymousPermissions = _settingsManager.getPermissionsForName(NodePermissions::standardNameAnonymous);
+    domainObject[RESTRICTED_ACCESS_FLAG] = !anonymousPermissions.canConnectToDomain;
 
     // Add the metadata to the heartbeat
     static const QString DOMAIN_HEARTBEAT_KEY = "heartbeat";
@@ -2107,35 +2109,42 @@ void DomainServer::processPathQueryPacket(QSharedPointer<ReceivedMessage> messag
 void DomainServer::processNodeDisconnectRequestPacket(QSharedPointer<ReceivedMessage> message) {
     // This packet has been matched to a source node and they're asking not to be in the domain anymore
     auto limitedNodeList = DependencyManager::get<LimitedNodeList>();
-    
+
     const QUuid& nodeUUID = message->getSourceID();
-    
+
     qDebug() << "Received a disconnect request from node with UUID" << nodeUUID;
-    
+
     // we want to check what type this node was before going to kill it so that we can avoid sending the RemovedNode
     // packet to nodes that don't care about this type
     auto nodeToKill = limitedNodeList->nodeWithUUID(nodeUUID);
-    
+
     if (nodeToKill) {
-        auto nodeType = nodeToKill->getType();
-        limitedNodeList->killNodeWithUUID(nodeUUID);
-        
-        static auto removedNodePacket = NLPacket::create(PacketType::DomainServerRemovedNode, NUM_BYTES_RFC4122_UUID);
-        
-        removedNodePacket->reset();
-        removedNodePacket->write(nodeUUID.toRfc4122());
-    
-        // broadcast out the DomainServerRemovedNode message
-        limitedNodeList->eachMatchingNode([&nodeType](const SharedNodePointer& otherNode) -> bool {
-            // only send the removed node packet to nodes that care about the type of node this was
-            auto nodeLinkedData = dynamic_cast<DomainServerNodeData*>(otherNode->getLinkedData());
-            return (nodeLinkedData != nullptr) && nodeLinkedData->getNodeInterestSet().contains(nodeType);
-        }, [&limitedNodeList](const SharedNodePointer& otherNode){
-            limitedNodeList->sendUnreliablePacket(*removedNodePacket, *otherNode);
-        });
+        handleKillNode(nodeToKill);
     }
 }
 
+void DomainServer::handleKillNode(SharedNodePointer nodeToKill) {
+    auto nodeType = nodeToKill->getType();
+    auto limitedNodeList = DependencyManager::get<LimitedNodeList>();
+    const QUuid& nodeUUID = nodeToKill->getUUID();
+
+    limitedNodeList->killNodeWithUUID(nodeUUID);
+
+    static auto removedNodePacket = NLPacket::create(PacketType::DomainServerRemovedNode, NUM_BYTES_RFC4122_UUID);
+
+    removedNodePacket->reset();
+    removedNodePacket->write(nodeUUID.toRfc4122());
+
+    // broadcast out the DomainServerRemovedNode message
+    limitedNodeList->eachMatchingNode([&nodeType](const SharedNodePointer& otherNode) -> bool {
+        // only send the removed node packet to nodes that care about the type of node this was
+        auto nodeLinkedData = dynamic_cast<DomainServerNodeData*>(otherNode->getLinkedData());
+        return (nodeLinkedData != nullptr) && nodeLinkedData->getNodeInterestSet().contains(nodeType);
+    }, [&limitedNodeList](const SharedNodePointer& otherNode){
+        limitedNodeList->sendUnreliablePacket(*removedNodePacket, *otherNode);
+    });
+}
+
 void DomainServer::processICEServerHeartbeatDenialPacket(QSharedPointer<ReceivedMessage> message) {
     static const int NUM_HEARTBEAT_DENIALS_FOR_KEYPAIR_REGEN = 3;
 
diff --git a/domain-server/src/DomainServer.h b/domain-server/src/DomainServer.h
index 8b8409ff0a..bdcc36c1ac 100644
--- a/domain-server/src/DomainServer.h
+++ b/domain-server/src/DomainServer.h
@@ -114,6 +114,8 @@ private:
 
     unsigned int countConnectedUsers();
 
+    void handleKillNode(SharedNodePointer nodeToKill);
+
     void sendDomainListToNode(const SharedNodePointer& node, const HifiSockAddr& senderSockAddr);
 
     QUuid connectionSecretForNodes(const SharedNodePointer& nodeA, const SharedNodePointer& nodeB);
diff --git a/domain-server/src/DomainServerSettingsManager.cpp b/domain-server/src/DomainServerSettingsManager.cpp
index 0ca0cf8232..5790eb9178 100644
--- a/domain-server/src/DomainServerSettingsManager.cpp
+++ b/domain-server/src/DomainServerSettingsManager.cpp
@@ -9,6 +9,8 @@
 //  See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
 //
 
+#include <algorithm>
+
 #include <QtCore/QCoreApplication>
 #include <QtCore/QDir>
 #include <QtCore/QFile>
@@ -26,6 +28,9 @@
 
 #include "DomainServerSettingsManager.h"
 
+#define WANT_DEBUG 1
+
+
 const QString SETTINGS_DESCRIPTION_RELATIVE_PATH = "/resources/describe-settings.json";
 
 const QString DESCRIPTION_SETTINGS_KEY = "settings";
@@ -44,7 +49,8 @@ DomainServerSettingsManager::DomainServerSettingsManager() :
     QFile descriptionFile(QCoreApplication::applicationDirPath() + SETTINGS_DESCRIPTION_RELATIVE_PATH);
     descriptionFile.open(QIODevice::ReadOnly);
 
-    QJsonDocument descriptionDocument = QJsonDocument::fromJson(descriptionFile.readAll());
+    QJsonParseError parseError;
+    QJsonDocument descriptionDocument = QJsonDocument::fromJson(descriptionFile.readAll(), &parseError);
 
     if (descriptionDocument.isObject()) {
         QJsonObject descriptionObject = descriptionDocument.object();
@@ -63,8 +69,8 @@ DomainServerSettingsManager::DomainServerSettingsManager() :
     }
 
     static const QString MISSING_SETTINGS_DESC_MSG =
-        QString("Did not find settings decription in JSON at %1 - Unable to continue. domain-server will quit.")
-        .arg(SETTINGS_DESCRIPTION_RELATIVE_PATH);
+        QString("Did not find settings decription in JSON at %1 - Unable to continue. domain-server will quit.\n%2 at %3")
+        .arg(SETTINGS_DESCRIPTION_RELATIVE_PATH).arg(parseError.errorString()).arg(parseError.offset);
     static const int MISSING_SETTINGS_DESC_ERROR_CODE = 6;
 
     QMetaObject::invokeMethod(QCoreApplication::instance(), "queuedQuit", Qt::QueuedConnection,
@@ -88,7 +94,8 @@ void DomainServerSettingsManager::processSettingsRequestPacket(QSharedPointer<Re
 }
 
 void DomainServerSettingsManager::setupConfigMap(const QStringList& argumentList) {
-    _configMap.loadMasterAndUserConfig(argumentList);
+    _argumentList = argumentList;
+    _configMap.loadMasterAndUserConfig(_argumentList);
 
     // What settings version were we before and what are we using now?
     // Do we need to do any re-mapping?
@@ -97,6 +104,11 @@ void DomainServerSettingsManager::setupConfigMap(const QStringList& argumentList
     double oldVersion = appSettings.value(JSON_SETTINGS_VERSION_KEY, 0.0).toDouble();
 
     if (oldVersion != _descriptionVersion) {
+        const QString ALLOWED_USERS_SETTINGS_KEYPATH = "security.allowed_users";
+        const QString RESTRICTED_ACCESS_SETTINGS_KEYPATH = "security.restricted_access";
+        const QString ALLOWED_EDITORS_SETTINGS_KEYPATH = "security.allowed_editors";
+        const QString EDITORS_ARE_REZZERS_KEYPATH = "security.editors_are_rezzers";
+
         qDebug() << "Previous domain-server settings version was"
             << QString::number(oldVersion, 'g', 8) << "and the new version is"
             << QString::number(_descriptionVersion, 'g', 8) << "- checking if any re-mapping is required";
@@ -127,7 +139,7 @@ void DomainServerSettingsManager::setupConfigMap(const QStringList& argumentList
                 persistToFile();
 
                 // reload the master and user config so that the merged config is right
-                _configMap.loadMasterAndUserConfig(argumentList);
+                _configMap.loadMasterAndUserConfig(_argumentList);
             }
         }
 
@@ -163,7 +175,7 @@ void DomainServerSettingsManager::setupConfigMap(const QStringList& argumentList
                 persistToFile();
 
                 // reload the master and user config so that the merged config is right
-                _configMap.loadMasterAndUserConfig(argumentList);
+                _configMap.loadMasterAndUserConfig(_argumentList);
             }
 
         }
@@ -186,15 +198,216 @@ void DomainServerSettingsManager::setupConfigMap(const QStringList& argumentList
                 persistToFile();
 
                 // reload the master and user config so the merged config is correct
-                _configMap.loadMasterAndUserConfig(argumentList);
+                _configMap.loadMasterAndUserConfig(_argumentList);
             }
         }
+
+        if (oldVersion < 1.4) {
+            // This was prior to the permissions-grid in the domain-server settings page
+            bool isRestrictedAccess = valueOrDefaultValueForKeyPath(RESTRICTED_ACCESS_SETTINGS_KEYPATH).toBool();
+            QStringList allowedUsers = valueOrDefaultValueForKeyPath(ALLOWED_USERS_SETTINGS_KEYPATH).toStringList();
+            QStringList allowedEditors = valueOrDefaultValueForKeyPath(ALLOWED_EDITORS_SETTINGS_KEYPATH).toStringList();
+            bool onlyEditorsAreRezzers = valueOrDefaultValueForKeyPath(EDITORS_ARE_REZZERS_KEYPATH).toBool();
+
+            _standardAgentPermissions[NodePermissions::standardNameLocalhost].reset(
+                new NodePermissions(NodePermissions::standardNameLocalhost));
+            _standardAgentPermissions[NodePermissions::standardNameLocalhost]->setAll(true);
+            _standardAgentPermissions[NodePermissions::standardNameAnonymous].reset(
+                new NodePermissions(NodePermissions::standardNameAnonymous));
+            _standardAgentPermissions[NodePermissions::standardNameLoggedIn].reset(
+                new NodePermissions(NodePermissions::standardNameLoggedIn));
+
+            if (isRestrictedAccess) {
+                // only users in allow-users list can connect
+                _standardAgentPermissions[NodePermissions::standardNameAnonymous]->canConnectToDomain = false;
+                _standardAgentPermissions[NodePermissions::standardNameLoggedIn]->canConnectToDomain = false;
+            } // else anonymous and logged-in retain default of canConnectToDomain = true
+
+            foreach (QString allowedUser, allowedUsers) {
+                // even if isRestrictedAccess is false, we have to add explicit rows for these users.
+                // defaults to canConnectToDomain = true
+                _agentPermissions[allowedUser].reset(new NodePermissions(allowedUser));
+            }
+
+            foreach (QString allowedEditor, allowedEditors) {
+                if (!_agentPermissions.contains(allowedEditor)) {
+                    _agentPermissions[allowedEditor].reset(new NodePermissions(allowedEditor));
+                    if (isRestrictedAccess) {
+                        // they can change locks, but can't connect.
+                        _agentPermissions[allowedEditor]->canConnectToDomain = false;
+                    }
+                }
+                _agentPermissions[allowedEditor]->canAdjustLocks = true;
+            }
+
+            QList<QHash<QString, NodePermissionsPointer>> permissionsSets;
+            permissionsSets << _standardAgentPermissions << _agentPermissions;
+            foreach (auto permissionsSet, permissionsSets) {
+                foreach (QString userName, permissionsSet.keys()) {
+                    if (onlyEditorsAreRezzers) {
+                        permissionsSet[userName]->canRezPermanentEntities = permissionsSet[userName]->canAdjustLocks;
+                        permissionsSet[userName]->canRezTemporaryEntities = permissionsSet[userName]->canAdjustLocks;
+                    } else {
+                        permissionsSet[userName]->canRezPermanentEntities = true;
+                        permissionsSet[userName]->canRezTemporaryEntities = true;
+                    }
+                }
+            }
+
+            packPermissions();
+            _standardAgentPermissions.clear();
+            _agentPermissions.clear();
+        }
     }
 
+    unpackPermissions();
+
     // write the current description version to our settings
     appSettings.setValue(JSON_SETTINGS_VERSION_KEY, _descriptionVersion);
 }
 
+void DomainServerSettingsManager::packPermissionsForMap(QString mapName,
+                                                        QHash<QString, NodePermissionsPointer> agentPermissions,
+                                                        QString keyPath) {
+    QVariant* security = valueForKeyPath(_configMap.getUserConfig(), "security");
+    if (!security || !security->canConvert(QMetaType::QVariantMap)) {
+        security = valueForKeyPath(_configMap.getUserConfig(), "security", true);
+        (*security) = QVariantMap();
+    }
+
+    // save settings for anonymous / logged-in / localhost
+    QVariant* permissions = valueForKeyPath(_configMap.getUserConfig(), keyPath);
+    if (!permissions || !permissions->canConvert(QMetaType::QVariantList)) {
+        permissions = valueForKeyPath(_configMap.getUserConfig(), keyPath, true);
+        (*permissions) = QVariantList();
+    }
+
+    QVariantList* permissionsList = reinterpret_cast<QVariantList*>(permissions);
+    (*permissionsList).clear();
+    foreach (QString userName, agentPermissions.keys()) {
+        *permissionsList += agentPermissions[userName]->toVariant();
+    }
+}
+
+void DomainServerSettingsManager::packPermissions() {
+    // transfer details from _agentPermissions to _configMap
+    packPermissionsForMap("standard_permissions", _standardAgentPermissions, AGENT_STANDARD_PERMISSIONS_KEYPATH);
+
+    // save settings for specific users
+    packPermissionsForMap("permissions", _agentPermissions, AGENT_PERMISSIONS_KEYPATH);
+
+    persistToFile();
+    _configMap.loadMasterAndUserConfig(_argumentList);
+}
+
+void DomainServerSettingsManager::unpackPermissions() {
+    // transfer details from _configMap to _agentPermissions;
+
+    _standardAgentPermissions.clear();
+    _agentPermissions.clear();
+
+    bool foundLocalhost = false;
+    bool foundAnonymous = false;
+    bool foundLoggedIn = false;
+    bool needPack = false;
+
+    QVariant* standardPermissions = valueForKeyPath(_configMap.getUserConfig(), AGENT_STANDARD_PERMISSIONS_KEYPATH);
+    if (!standardPermissions || !standardPermissions->canConvert(QMetaType::QVariantList)) {
+        qDebug() << "failed to extract standard permissions from settings.";
+        standardPermissions = valueForKeyPath(_configMap.getUserConfig(), AGENT_STANDARD_PERMISSIONS_KEYPATH, true);
+        (*standardPermissions) = QVariantList();
+    }
+    QVariant* permissions = valueForKeyPath(_configMap.getUserConfig(), AGENT_PERMISSIONS_KEYPATH);
+    if (!permissions || !permissions->canConvert(QMetaType::QVariantList)) {
+        qDebug() << "failed to extract permissions from settings.";
+        permissions = valueForKeyPath(_configMap.getUserConfig(), AGENT_PERMISSIONS_KEYPATH, true);
+        (*permissions) = QVariantList();
+    }
+
+    QList<QVariant> standardPermissionsList = standardPermissions->toList();
+    foreach (QVariant permsHash, standardPermissionsList) {
+        NodePermissionsPointer perms { new NodePermissions(permsHash.toMap()) };
+        QString id = perms->getID();
+        foundLocalhost |= (id == NodePermissions::standardNameLocalhost);
+        foundAnonymous |= (id == NodePermissions::standardNameAnonymous);
+        foundLoggedIn |= (id == NodePermissions::standardNameLoggedIn);
+        if (_standardAgentPermissions.contains(id)) {
+            qDebug() << "duplicate name in standard permissions table: " << id;
+            _standardAgentPermissions[id] |= perms;
+            needPack = true;
+        } else {
+            _standardAgentPermissions[id] = perms;
+        }
+    }
+
+    QList<QVariant> permissionsList = permissions->toList();
+    foreach (QVariant permsHash, permissionsList) {
+        NodePermissionsPointer perms { new NodePermissions(permsHash.toMap()) };
+        QString id = perms->getID();
+        if (_agentPermissions.contains(id)) {
+            qDebug() << "duplicate name in permissions table: " << id;
+            _agentPermissions[id] |= perms;
+            needPack = true;
+        } else {
+            _agentPermissions[id] = perms;
+        }
+    }
+
+    // if any of the standard names are missing, add them
+    if (!foundLocalhost) {
+        NodePermissionsPointer perms { new NodePermissions(NodePermissions::standardNameLocalhost) };
+        perms->setAll(true);
+        _standardAgentPermissions[perms->getID()] = perms;
+        needPack = true;
+    }
+    if (!foundAnonymous) {
+        NodePermissionsPointer perms { new NodePermissions(NodePermissions::standardNameAnonymous) };
+        _standardAgentPermissions[perms->getID()] = perms;
+        needPack = true;
+    }
+    if (!foundLoggedIn) {
+        NodePermissionsPointer perms { new NodePermissions(NodePermissions::standardNameLoggedIn) };
+        _standardAgentPermissions[perms->getID()] = perms;
+        needPack = true;
+    }
+
+    if (needPack) {
+        packPermissions();
+    }
+
+    #ifdef WANT_DEBUG
+    qDebug() << "--------------- permissions ---------------------";
+    QList<QHash<QString, NodePermissionsPointer>> permissionsSets;
+    permissionsSets << _standardAgentPermissions << _agentPermissions;
+    foreach (auto permissionSet, permissionsSets) {
+        QHashIterator<QString, NodePermissionsPointer> i(permissionSet);
+        while (i.hasNext()) {
+            i.next();
+            NodePermissionsPointer perms = i.value();
+            qDebug() << i.key() << perms;
+        }
+    }
+    #endif
+}
+
+NodePermissions DomainServerSettingsManager::getStandardPermissionsForName(const QString& name) const {
+    if (_standardAgentPermissions.contains(name)) {
+        return *(_standardAgentPermissions[name].get());
+    }
+    NodePermissions nullPermissions;
+    nullPermissions.setAll(false);
+    return nullPermissions;
+}
+
+NodePermissions DomainServerSettingsManager::getPermissionsForName(const QString& name) const {
+    if (_agentPermissions.contains(name)) {
+        return *(_agentPermissions[name].get());
+    }
+    NodePermissions nullPermissions;
+    nullPermissions.setAll(false);
+    return nullPermissions;
+}
+
 QVariant DomainServerSettingsManager::valueOrDefaultValueForKeyPath(const QString& keyPath) {
     const QVariant* foundValue = valueForKeyPath(_configMap.getMergedConfig(), keyPath);
 
@@ -257,7 +470,7 @@ bool DomainServerSettingsManager::handleAuthenticatedHTTPRequest(HTTPConnection
         qDebug() << "DomainServerSettingsManager postedObject -" << postedObject;
 
         // we recurse one level deep below each group for the appropriate setting
-        recurseJSONObjectAndOverwriteSettings(postedObject);
+        bool restartRequired = recurseJSONObjectAndOverwriteSettings(postedObject);
 
         // store whatever the current _settingsMap is to file
         persistToFile();
@@ -267,8 +480,13 @@ bool DomainServerSettingsManager::handleAuthenticatedHTTPRequest(HTTPConnection
         connection->respond(HTTPConnection::StatusCode200, jsonSuccess.toUtf8(), "application/json");
 
         // defer a restart to the domain-server, this gives our HTTPConnection enough time to respond
-        const int DOMAIN_SERVER_RESTART_TIMER_MSECS = 1000;
-        QTimer::singleShot(DOMAIN_SERVER_RESTART_TIMER_MSECS, qApp, SLOT(restart()));
+        if (restartRequired) {
+            const int DOMAIN_SERVER_RESTART_TIMER_MSECS = 1000;
+            QTimer::singleShot(DOMAIN_SERVER_RESTART_TIMER_MSECS, qApp, SLOT(restart()));
+        } else {
+            unpackPermissions();
+            emit updateNodePermissions();
+        }
 
         return true;
     } else if (connection->requestOperation() == QNetworkAccessManager::GetOperation && url.path() == SETTINGS_PATH_JSON) {
@@ -282,7 +500,6 @@ bool DomainServerSettingsManager::handleAuthenticatedHTTPRequest(HTTPConnection
         rootObject[SETTINGS_RESPONSE_VALUE_KEY] = responseObjectForType("", true);
         rootObject[SETTINGS_RESPONSE_LOCKED_VALUES_KEY] = QJsonDocument::fromVariant(_configMap.getMasterConfig()).object();
 
-
         connection->respond(HTTPConnection::StatusCode200, QJsonDocument(rootObject).toJson(), "application/json");
     }
 
@@ -458,6 +675,8 @@ void DomainServerSettingsManager::updateSetting(const QString& key, const QJsonV
         // TODO: we still need to recurse here with the description in case values in the array have special types
         settingMap[key] = newValue.toArray().toVariantList();
     }
+
+    sortPermissions();
 }
 
 QJsonObject DomainServerSettingsManager::settingDescriptionFromGroup(const QJsonObject& groupObject, const QString& settingName) {
@@ -471,9 +690,10 @@ QJsonObject DomainServerSettingsManager::settingDescriptionFromGroup(const QJson
     return QJsonObject();
 }
 
-void DomainServerSettingsManager::recurseJSONObjectAndOverwriteSettings(const QJsonObject& postedObject) {
+bool DomainServerSettingsManager::recurseJSONObjectAndOverwriteSettings(const QJsonObject& postedObject) {
     auto& settingsVariant = _configMap.getUserConfig();
-    
+    bool needRestart = false;
+
     // Iterate on the setting groups
     foreach(const QString& rootKey, postedObject.keys()) {
         QJsonValue rootValue = postedObject[rootKey];
@@ -521,6 +741,9 @@ void DomainServerSettingsManager::recurseJSONObjectAndOverwriteSettings(const QJ
 
             if (!matchingDescriptionObject.isEmpty()) {
                 updateSetting(rootKey, rootValue, *thisMap, matchingDescriptionObject);
+                if (rootKey != "security") {
+                    needRestart = true;
+                }
             } else {
                 qDebug() << "Setting for root key" << rootKey << "does not exist - cannot update setting.";
             }
@@ -534,6 +757,9 @@ void DomainServerSettingsManager::recurseJSONObjectAndOverwriteSettings(const QJ
                 if (!matchingDescriptionObject.isEmpty()) {
                     QJsonValue settingValue = rootValue.toObject()[settingKey];
                     updateSetting(settingKey, settingValue, *thisMap, matchingDescriptionObject);
+                    if (rootKey != "security") {
+                        needRestart = true;
+                    }
                 } else {
                     qDebug() << "Could not find description for setting" << settingKey << "in group" << rootKey <<
                         "- cannot update setting.";
@@ -549,9 +775,42 @@ void DomainServerSettingsManager::recurseJSONObjectAndOverwriteSettings(const QJ
 
     // re-merge the user and master configs after a settings change
     _configMap.mergeMasterAndUserConfigs();
+
+    return needRestart;
+}
+
+// Compare two members of a permissions list
+bool permissionVariantLessThan(const QVariant &v1, const QVariant &v2) {
+    if (!v1.canConvert(QMetaType::QVariantMap) ||
+        !v2.canConvert(QMetaType::QVariantMap)) {
+        return v1.toString() < v2.toString();
+    }
+    QVariantMap m1 = v1.toMap();
+    QVariantMap m2 = v2.toMap();
+
+    if (!m1.contains("permissions_id") ||
+        !m2.contains("permissions_id")) {
+        return v1.toString() < v2.toString();
+    }
+    return m1["permissions_id"].toString() < m2["permissions_id"].toString();
+}
+
+void DomainServerSettingsManager::sortPermissions() {
+    // sort the permission-names
+    QVariant* standardPermissions = valueForKeyPath(_configMap.getUserConfig(), AGENT_STANDARD_PERMISSIONS_KEYPATH);
+    if (standardPermissions && standardPermissions->canConvert(QMetaType::QVariantList)) {
+        QList<QVariant>* standardPermissionsList = reinterpret_cast<QVariantList*>(standardPermissions);
+        std::sort((*standardPermissionsList).begin(), (*standardPermissionsList).end(), permissionVariantLessThan);
+    }
+    QVariant* permissions = valueForKeyPath(_configMap.getUserConfig(), AGENT_PERMISSIONS_KEYPATH);
+    if (permissions && permissions->canConvert(QMetaType::QVariantList)) {
+        QList<QVariant>* permissionsList = reinterpret_cast<QVariantList*>(permissions);
+        std::sort((*permissionsList).begin(), (*permissionsList).end(), permissionVariantLessThan);
+    }
 }
 
 void DomainServerSettingsManager::persistToFile() {
+    sortPermissions();
 
     // make sure we have the dir the settings file is supposed to live in
     QFileInfo settingsFileInfo(_configMap.getUserConfigFilename());
diff --git a/domain-server/src/DomainServerSettingsManager.h b/domain-server/src/DomainServerSettingsManager.h
index d6dd5070a9..446e9a2eed 100644
--- a/domain-server/src/DomainServerSettingsManager.h
+++ b/domain-server/src/DomainServerSettingsManager.h
@@ -19,14 +19,14 @@
 #include <HTTPManager.h>
 
 #include <ReceivedMessage.h>
+#include "NodePermissions.h"
 
 const QString SETTINGS_PATHS_KEY = "paths";
 
 const QString SETTINGS_PATH = "/settings";
 const QString SETTINGS_PATH_JSON = SETTINGS_PATH + ".json";
-
-const QString ALLOWED_USERS_SETTINGS_KEYPATH = "security.allowed_users";
-const QString RESTRICTED_ACCESS_SETTINGS_KEYPATH = "security.restricted_access";
+const QString AGENT_STANDARD_PERMISSIONS_KEYPATH = "security.standard_permissions";
+const QString AGENT_PERMISSIONS_KEYPATH = "security.permissions";
 
 class DomainServerSettingsManager : public QObject {
     Q_OBJECT
@@ -41,16 +41,29 @@ public:
     QVariantMap& getUserSettingsMap() { return _configMap.getUserConfig(); }
     QVariantMap& getSettingsMap() { return _configMap.getMergedConfig(); }
 
+    bool haveStandardPermissionsForName(const QString& name) const { return _standardAgentPermissions.contains(name); }
+    bool havePermissionsForName(const QString& name) const { return _agentPermissions.contains(name); }
+    NodePermissions getStandardPermissionsForName(const QString& name) const;
+    NodePermissions getPermissionsForName(const QString& name) const;
+    QStringList getAllNames() { return _agentPermissions.keys(); }
+
+signals:
+    void updateNodePermissions();
+
+
 private slots:
     void processSettingsRequestPacket(QSharedPointer<ReceivedMessage> message);
 
 private:
+    QStringList _argumentList;
+
     QJsonObject responseObjectForType(const QString& typeValue, bool isAuthenticated = false);
-    void recurseJSONObjectAndOverwriteSettings(const QJsonObject& postedObject);
+    bool recurseJSONObjectAndOverwriteSettings(const QJsonObject& postedObject);
 
     void updateSetting(const QString& key, const QJsonValue& newValue, QVariantMap& settingMap,
                        const QJsonObject& settingDescription);
     QJsonObject settingDescriptionFromGroup(const QJsonObject& groupObject, const QString& settingName);
+    void sortPermissions();
     void persistToFile();
 
     double _descriptionVersion;
@@ -58,6 +71,12 @@ private:
     HifiConfigVariantMap _configMap;
 
     friend class DomainServer;
+
+    void packPermissionsForMap(QString mapName, QHash<QString, NodePermissionsPointer> agentPermissions, QString keyPath);
+    void packPermissions();
+    void unpackPermissions();
+    QHash<QString, NodePermissionsPointer> _standardAgentPermissions; // anonymous, logged-in, localhost
+    QHash<QString, NodePermissionsPointer> _agentPermissions; // specific account-names
 };
 
 #endif // hifi_DomainServerSettingsManager_h
diff --git a/interface/resources/qml/controls-uit/ComboBox.qml b/interface/resources/qml/controls-uit/ComboBox.qml
index cd6dc8ede0..df3210a20d 100755
--- a/interface/resources/qml/controls-uit/ComboBox.qml
+++ b/interface/resources/qml/controls-uit/ComboBox.qml
@@ -199,7 +199,7 @@ FocusScope {
                         anchors.leftMargin: hifi.dimensions.textPadding
                         anchors.verticalCenter: parent.verticalCenter
                         id: popupText
-                        text: listView.model[index]
+                        text: listView.model[index] ? listView.model[index] : ""
                         size: hifi.fontSizes.textFieldInput
                         color: hifi.colors.baseGray
                     }
diff --git a/interface/resources/qml/dialogs/FileDialog.qml b/interface/resources/qml/dialogs/FileDialog.qml
index 96676e986a..ef82ce4884 100644
--- a/interface/resources/qml/dialogs/FileDialog.qml
+++ b/interface/resources/qml/dialogs/FileDialog.qml
@@ -186,7 +186,12 @@ ModalWindow {
                 }
 
                 if (helper.urlToPath(folder).toLowerCase() !== helper.urlToPath(fileTableModel.folder).toLowerCase()) {
+                    if (root.selectDirectory) {
+                        currentSelection.text = currentText !== "This PC" ? currentText : "";
+                        d.currentSelectionUrl = helper.pathToUrl(currentText);
+                    }
                     fileTableModel.folder = folder;
+                    fileTableView.forceActiveFocus();
                 }
             }
         }
@@ -212,9 +217,11 @@ ModalWindow {
             function update() {
                 var row = fileTableView.currentRow;
 
-                openButton.text = root.selectDirectory && row === -1 ? "Choose" : "Open"
-
                 if (row === -1) {
+                    if (!root.selectDirectory) {
+                        currentSelection.text = "";
+                        currentSelectionIsFolder = false;
+                    }
                     return;
                 }
 
@@ -445,12 +452,6 @@ ModalWindow {
 
             onSortIndicatorOrderChanged: { updateSort(); }
 
-            onActiveFocusChanged: {
-                if (activeFocus && currentRow == -1) {
-                    fileTableView.selection.select(0)
-                }
-            }
-
             itemDelegate: Item {
                 clip: true
 
@@ -607,6 +608,12 @@ ModalWindow {
             readOnly: !root.saveDialog
             activeFocusOnTab: !readOnly
             onActiveFocusChanged: if (activeFocus) { selectAll(); }
+            onTextChanged: {
+                if (root.saveDialog && text !== "") {
+                    fileTableView.selection.clear();
+                    fileTableView.currentRow = -1;
+                }
+            }
             onAccepted: okAction.trigger();
         }
 
@@ -652,7 +659,7 @@ ModalWindow {
 
         Action {
             id: okAction
-            text: root.saveDialog ? "Save" : (root.selectDirectory ? "Choose" : "Open")
+            text: currentSelection.text ? (root.selectDirectory && fileTableView.currentRow === -1 ? "Choose" : (root.saveDialog ? "Save" : "Open")) : "Open"
             enabled: currentSelection.text || !root.selectDirectory && d.currentSelectionIsFolder ? true : false
             onTriggered: {
                 if (!root.selectDirectory && !d.currentSelectionIsFolder
@@ -676,7 +683,6 @@ ModalWindow {
                     return;
                 }
 
-
                 // Handle the ambiguity between different cases
                 // * typed name (with or without extension)
                 // * full path vs relative vs filename only
diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp
index 1e6f7ba995..ec86f5f1e0 100644
--- a/interface/src/Application.cpp
+++ b/interface/src/Application.cpp
@@ -630,7 +630,6 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) :
     connect(&domainHandler, SIGNAL(connectedToDomain(const QString&)), SLOT(updateWindowTitle()));
     connect(&domainHandler, SIGNAL(disconnectedFromDomain()), SLOT(updateWindowTitle()));
     connect(&domainHandler, SIGNAL(disconnectedFromDomain()), SLOT(clearDomainOctreeDetails()));
-    connect(&domainHandler, &DomainHandler::resetting, nodeList.data(), &NodeList::resetDomainServerCheckInVersion);
     connect(&domainHandler, &DomainHandler::domainConnectionRefused, this, &Application::domainConnectionRefused);
 
     // update our location every 5 seconds in the metaverse server, assuming that we are authenticated with one
@@ -654,9 +653,11 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) :
     connect(nodeList.data(), &NodeList::nodeActivated, this, &Application::nodeActivated);
     connect(nodeList.data(), &NodeList::uuidChanged, getMyAvatar(), &MyAvatar::setSessionUUID);
     connect(nodeList.data(), &NodeList::uuidChanged, this, &Application::setSessionUUID);
-    connect(nodeList.data(), &NodeList::limitOfSilentDomainCheckInsReached, this, &Application::limitOfSilentDomainCheckInsReached);
     connect(nodeList.data(), &NodeList::packetVersionMismatch, this, &Application::notifyPacketVersionMismatch);
 
+    // you might think we could just do this in NodeList but we only want this connection for Interface
+    connect(nodeList.data(), &NodeList::limitOfSilentDomainCheckInsReached, nodeList.data(), &NodeList::reset);
+
     // connect to appropriate slots on AccountManager
     auto accountManager = DependencyManager::get<AccountManager>();
 
@@ -1074,8 +1075,6 @@ Application::Application(int& argc, char** argv, QElapsedTimer& startupTimer) :
 void Application::domainConnectionRefused(const QString& reasonMessage, int reasonCode) {
     switch (static_cast<DomainHandler::ConnectionRefusedReason>(reasonCode)) {
         case DomainHandler::ConnectionRefusedReason::ProtocolMismatch:
-            notifyPacketVersionMismatch();
-            break;
         case DomainHandler::ConnectionRefusedReason::TooManyUsers:
         case DomainHandler::ConnectionRefusedReason::Unknown: {
             QString message = "Unable to connect to the location you are visiting.\n";
@@ -4209,6 +4208,7 @@ void Application::resetSensors(bool andReload) {
     DependencyManager::get<DdeFaceTracker>()->reset();
     DependencyManager::get<EyeTracker>()->reset();
     getActiveDisplayPlugin()->resetSensors();
+    _overlayConductor.centerUI();
     getMyAvatar()->reset(andReload);
     QMetaObject::invokeMethod(DependencyManager::get<AudioClient>().data(), "reset", Qt::QueuedConnection);
 }
@@ -4294,7 +4294,7 @@ void Application::nodeActivated(SharedNodePointer node) {
         if (assetDialog) {
             auto nodeList = DependencyManager::get<NodeList>();
 
-            if (nodeList->getThisNodeCanRez()) {
+            if (nodeList->getThisNodeCanWriteAssets()) {
                 // call reload on the shown asset browser dialog to get the mappings (if permissions allow)
                 QMetaObject::invokeMethod(assetDialog, "reload");
             } else {
@@ -4620,17 +4620,6 @@ void Application::setSessionUUID(const QUuid& sessionUUID) const {
     Physics::setSessionUUID(sessionUUID);
 }
 
-
-// If we're not getting anything back from the domain server checkin, it might be that the domain speaks an 
-// older version of the DomainConnectRequest protocol. We will attempt to send and older version of DomainConnectRequest.
-// We won't actually complete the connection, but if the server responds, we know that it needs to be upgraded (or we
-// need to be downgraded to talk to it).
-void Application::limitOfSilentDomainCheckInsReached() {
-    auto nodeList = DependencyManager::get<NodeList>();
-    nodeList->downgradeDomainServerCheckInVersion(); // attempt to use an older domain checkin version
-    nodeList->reset();
-}
-
 bool Application::askToSetAvatarUrl(const QString& url) {
     QUrl realUrl(url);
     if (realUrl.isLocalFile()) {
@@ -4801,7 +4790,7 @@ void Application::toggleRunningScriptsWidget() const {
 }
 
 void Application::toggleAssetServerWidget(QString filePath) {
-    if (!DependencyManager::get<NodeList>()->getThisNodeCanRez()) {
+    if (!DependencyManager::get<NodeList>()->getThisNodeCanWriteAssets()) {
         return;
     }
 
diff --git a/interface/src/Application.h b/interface/src/Application.h
index f93434f581..6b6148be32 100644
--- a/interface/src/Application.h
+++ b/interface/src/Application.h
@@ -318,7 +318,6 @@ private slots:
     bool displayAvatarAttachmentConfirmationDialog(const QString& name) const;
 
     void setSessionUUID(const QUuid& sessionUUID) const;
-    void limitOfSilentDomainCheckInsReached();
 
     void domainChanged(const QString& domainHostname);
     void updateWindowTitle() const;
diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp
index 031564fa7a..6308ac6c73 100644
--- a/interface/src/Menu.cpp
+++ b/interface/src/Menu.cpp
@@ -136,8 +136,8 @@ Menu::Menu() {
                                                            Qt::CTRL | Qt::SHIFT | Qt::Key_A,
                                                            qApp, SLOT(toggleAssetServerWidget()));
     auto nodeList = DependencyManager::get<NodeList>();
-    QObject::connect(nodeList.data(), &NodeList::canRezChanged, assetServerAction, &QAction::setEnabled);
-    assetServerAction->setEnabled(nodeList->getThisNodeCanRez());
+    QObject::connect(nodeList.data(), &NodeList::canWriteAssetsChanged, assetServerAction, &QAction::setEnabled);
+    assetServerAction->setEnabled(nodeList->getThisNodeCanWriteAssets());
 
     // Edit > Package Model... [advanced]
     addActionToQMenuAndActionHash(editMenu, MenuOption::PackageModel, 0,
@@ -256,8 +256,7 @@ Menu::Menu() {
         UNSPECIFIED_POSITION, "Advanced");
 
     // View > Overlays
-    addCheckableActionToQMenuAndActionHash(viewMenu, MenuOption::Overlays, 0, true,
-        qApp, SLOT(setOverlaysVisible(bool)));
+    addCheckableActionToQMenuAndActionHash(viewMenu, MenuOption::Overlays, 0, true);
 
     // Navigate menu ----------------------------------
     MenuWrapper* navigateMenu = addMenu("Navigate");
@@ -542,6 +541,9 @@ Menu::Menu() {
     #if (PR_BUILD || DEV_BUILD)
     addCheckableActionToQMenuAndActionHash(networkMenu, MenuOption::SendWrongProtocolVersion, 0, false,
                 qApp, SLOT(sendWrongProtocolVersionsSignature(bool)));
+
+    addCheckableActionToQMenuAndActionHash(networkMenu, MenuOption::SendWrongDSConnectVersion, 0, false,
+                                           nodeList.data(), SLOT(toggleSendNewerDSConnectVersion(bool)));
     #endif
 
     
diff --git a/interface/src/Menu.h b/interface/src/Menu.h
index 8081e27eb8..503cbf51fa 100644
--- a/interface/src/Menu.h
+++ b/interface/src/Menu.h
@@ -166,6 +166,7 @@ namespace MenuOption {
     const QString RunTimingTests = "Run Timing Tests";
     const QString ScriptEditor = "Script Editor...";
     const QString ScriptedMotorControl = "Enable Scripted Motor Control";
+    const QString SendWrongDSConnectVersion = "Send wrong DS connect version";
     const QString SendWrongProtocolVersion = "Send wrong protocol version";
     const QString SetHomeLocation = "Set Home Location";
     const QString ShowDSConnectTable = "Show Domain Connection Timing";
diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp
index fa8db02658..3e629d04f3 100644
--- a/interface/src/avatar/MyAvatar.cpp
+++ b/interface/src/avatar/MyAvatar.cpp
@@ -1246,8 +1246,7 @@ void MyAvatar::prepareForPhysicsSimulation() {
 
     _characterController.setPositionAndOrientation(getPosition(), getOrientation());
     if (qApp->isHMDMode()) {
-        bool hasDriveInput = fabsf(_driveKeys[TRANSLATE_X]) > 0.0f || fabsf(_driveKeys[TRANSLATE_Z]) > 0.0f;
-        _follow.prePhysicsUpdate(*this, deriveBodyFromHMDSensor(), _bodySensorMatrix, hasDriveInput);
+        _follow.prePhysicsUpdate(*this, deriveBodyFromHMDSensor(), _bodySensorMatrix, hasDriveInput());
     } else {
         _follow.deactivate();
     }
@@ -2132,3 +2131,7 @@ bool MyAvatar::didTeleport() {
     lastPosition = pos;
     return (changeInPosition.length() > MAX_AVATAR_MOVEMENT_PER_FRAME);
 }
+
+bool MyAvatar::hasDriveInput() const {
+    return fabsf(_driveKeys[TRANSLATE_X]) > 0.0f || fabsf(_driveKeys[TRANSLATE_Y]) > 0.0f || fabsf(_driveKeys[TRANSLATE_Z]) > 0.0f;
+}
diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h
index 05afe39a32..6fa6e1fd19 100644
--- a/interface/src/avatar/MyAvatar.h
+++ b/interface/src/avatar/MyAvatar.h
@@ -266,6 +266,8 @@ public:
     controller::Pose getLeftHandControllerPoseInAvatarFrame() const;
     controller::Pose getRightHandControllerPoseInAvatarFrame() const;
 
+    bool hasDriveInput() const;
+
     Q_INVOKABLE void setCharacterControllerEnabled(bool enabled);
     Q_INVOKABLE bool getCharacterControllerEnabled();
 
diff --git a/interface/src/ui/OverlayConductor.cpp b/interface/src/ui/OverlayConductor.cpp
index 54dba229e3..a897d85472 100644
--- a/interface/src/ui/OverlayConductor.cpp
+++ b/interface/src/ui/OverlayConductor.cpp
@@ -17,122 +17,164 @@
 #include "OverlayConductor.h"
 
 OverlayConductor::OverlayConductor() {
+
 }
 
 OverlayConductor::~OverlayConductor() {
 }
 
-void OverlayConductor::update(float dt) {
+bool OverlayConductor::headOutsideOverlay() const {
+    glm::mat4 hmdMat = qApp->getHMDSensorPose();
+    glm::vec3 hmdPos = extractTranslation(hmdMat);
+    glm::vec3 hmdForward = transformVectorFast(hmdMat, glm::vec3(0.0f, 0.0f, -1.0f));
 
-    updateMode();
+    Transform uiTransform = qApp->getApplicationCompositor().getModelTransform();
+    glm::vec3 uiPos = uiTransform.getTranslation();
+    glm::vec3 uiForward = uiTransform.getRotation() * glm::vec3(0.0f, 0.0f, -1.0f);
 
-    switch (_mode) {
-    case SITTING: {
-        // when sitting, the overlay is at the origin, facing down the -z axis.
-        // the camera is taken directly from the HMD.
-        Transform identity;
-        qApp->getApplicationCompositor().setModelTransform(identity);
-        qApp->getApplicationCompositor().setCameraBaseTransform(identity);
-        break;
-    }
-    case STANDING: {
-        // when standing, the overlay is at a reference position, which is set when the overlay is
-        // enabled.  The camera is taken directly from the HMD, but in world space.
-        // So the sensorToWorldMatrix must be applied.
-        MyAvatar* myAvatar = DependencyManager::get<AvatarManager>()->getMyAvatar();
-        Transform t;
-        t.evalFromRawMatrix(myAvatar->getSensorToWorldMatrix());
-        qApp->getApplicationCompositor().setCameraBaseTransform(t);
-
-        // detect when head moves out side of sweet spot, or looks away.
-        mat4 headMat = myAvatar->getSensorToWorldMatrix() * qApp->getHMDSensorPose();
-        vec3 headWorldPos = extractTranslation(headMat);
-        vec3 headForward = glm::quat_cast(headMat) * glm::vec3(0.0f, 0.0f, -1.0f);
-        Transform modelXform = qApp->getApplicationCompositor().getModelTransform();
-        vec3 compositorWorldPos = modelXform.getTranslation();
-        vec3 compositorForward = modelXform.getRotation() * glm::vec3(0.0f, 0.0f, -1.0f);
-        const float MAX_COMPOSITOR_DISTANCE = 0.6f;
-        const float MAX_COMPOSITOR_ANGLE = 110.0f;
-        if (_enabled && (glm::distance(headWorldPos, compositorWorldPos) > MAX_COMPOSITOR_DISTANCE ||
-                         glm::dot(headForward, compositorForward) < cosf(glm::radians(MAX_COMPOSITOR_ANGLE)))) {
-            // fade out the overlay
-            setEnabled(false);
-        }
-        break;
-    }
-    case FLAT:
-        // do nothing
-        break;
+    const float MAX_COMPOSITOR_DISTANCE = 0.6f;
+    const float MAX_COMPOSITOR_ANGLE = 180.0f;  // rotation check is effectively disabled
+    if (glm::distance(uiPos, hmdPos) > MAX_COMPOSITOR_DISTANCE ||
+        glm::dot(uiForward, hmdForward) < cosf(glm::radians(MAX_COMPOSITOR_ANGLE))) {
+        return true;
     }
+    return false;
 }
 
-void OverlayConductor::updateMode() {
+bool OverlayConductor::updateAvatarIsAtRest() {
+
     MyAvatar* myAvatar = DependencyManager::get<AvatarManager>()->getMyAvatar();
-    if (myAvatar->getClearOverlayWhenDriving()) {
-        float speed = glm::length(myAvatar->getVelocity());
-        const float MIN_DRIVING = 0.2f;
-        const float MAX_NOT_DRIVING = 0.01f;
-        const quint64 REQUIRED_USECS_IN_NEW_MODE_BEFORE_INVISIBLE = 200 * 1000;
-        const quint64 REQUIRED_USECS_IN_NEW_MODE_BEFORE_VISIBLE = 1000 * 1000;
-        bool nowDriving = _driving; // Assume current _driving mode unless...
-        if (speed > MIN_DRIVING) {  // ... we're definitely moving...
-            nowDriving = true;
-        } else if (speed < MAX_NOT_DRIVING) { // ... or definitely not.
-            nowDriving = false;
-        }
-        // Check that we're in this new mode for long enough to really trigger a transition.
-        if (nowDriving == _driving) {  // If there's no change in state, clear any attepted timer.
-            _timeInPotentialMode = 0;
-        } else if (_timeInPotentialMode == 0) { // We've just changed with no timer, so start timing now.
-            _timeInPotentialMode = usecTimestampNow();
-        } else if ((usecTimestampNow() - _timeInPotentialMode) > (nowDriving ? REQUIRED_USECS_IN_NEW_MODE_BEFORE_INVISIBLE : REQUIRED_USECS_IN_NEW_MODE_BEFORE_VISIBLE)) {
-            _timeInPotentialMode = 0; // a real transition
-            if (nowDriving) {
-                _wantsOverlays = Menu::getInstance()->isOptionChecked(MenuOption::Overlays);
-            } else { // reset when coming out of driving
-                _mode = FLAT;  // Seems appropriate to let things reset, below, after the following.
-                // All reset of, e.g., room-scale location as though by apostrophe key, without all the other adjustments.
-                qApp->getActiveDisplayPlugin()->resetSensors();
-                myAvatar->reset(true, false, false);
+
+    const quint64 REST_ENABLE_TIME_USECS = 1000 * 1000; // 1 s
+    const quint64 REST_DISABLE_TIME_USECS = 200 * 1000;  // 200 ms
+
+    const float AT_REST_THRESHOLD = 0.01f;
+    bool desiredAtRest = glm::length(myAvatar->getVelocity()) < AT_REST_THRESHOLD;
+    if (desiredAtRest != _desiredAtRest) {
+        // start timer
+        _desiredAtRestTimer = usecTimestampNow() + (desiredAtRest ? REST_ENABLE_TIME_USECS : REST_DISABLE_TIME_USECS);
+    }
+
+    _desiredAtRest = desiredAtRest;
+
+    if (_desiredAtRestTimer != 0 && usecTimestampNow() > _desiredAtRestTimer) {
+        // timer expired
+        // change state!
+        _currentAtRest = _desiredAtRest;
+        // disable timer
+        _desiredAtRestTimer = 0;
+    }
+
+    return _currentAtRest;
+}
+
+bool OverlayConductor::updateAvatarHasDriveInput() {
+    MyAvatar* myAvatar = DependencyManager::get<AvatarManager>()->getMyAvatar();
+
+    const quint64 DRIVE_ENABLE_TIME_USECS = 200 * 1000;  // 200 ms
+    const quint64 DRIVE_DISABLE_TIME_USECS = 1000 * 1000; // 1 s
+
+    bool desiredDriving = myAvatar->hasDriveInput();
+    if (desiredDriving != _desiredDriving) {
+        // start timer
+        _desiredDrivingTimer = usecTimestampNow() + (desiredDriving ? DRIVE_ENABLE_TIME_USECS : DRIVE_DISABLE_TIME_USECS);
+    }
+
+    _desiredDriving = desiredDriving;
+
+    if (_desiredDrivingTimer != 0 && usecTimestampNow() > _desiredDrivingTimer) {
+        // timer expired
+        // change state!
+        _currentDriving = _desiredDriving;
+        // disable timer
+        _desiredDrivingTimer = 0;
+    }
+
+    return _currentDriving;
+}
+
+void OverlayConductor::centerUI() {
+    // place the overlay at the current hmd position in sensor space
+    auto camMat = cancelOutRollAndPitch(qApp->getHMDSensorPose());
+    qApp->getApplicationCompositor().setModelTransform(Transform(camMat));
+}
+
+bool OverlayConductor::userWishesToHide() const {
+    // user pressed toggle button.
+    return Menu::getInstance()->isOptionChecked(MenuOption::Overlays) != _prevOverlayMenuChecked && Menu::getInstance()->isOptionChecked(MenuOption::Overlays);
+}
+
+bool OverlayConductor::userWishesToShow() const {
+    // user pressed toggle button.
+    return Menu::getInstance()->isOptionChecked(MenuOption::Overlays) != _prevOverlayMenuChecked && !Menu::getInstance()->isOptionChecked(MenuOption::Overlays);
+}
+
+void OverlayConductor::setState(State state) {
+#ifdef WANT_DEBUG
+    static QString stateToString[NumStates] = { "Enabled", "DisabledByDrive", "DisabledByHead", "DisabledByToggle" };
+    qDebug() << "OverlayConductor " << stateToString[state] << "<--" << stateToString[_state];
+#endif
+    _state = state;
+}
+
+OverlayConductor::State OverlayConductor::getState() const {
+    return _state;
+}
+
+void OverlayConductor::update(float dt) {
+
+    MyAvatar* myAvatar = DependencyManager::get<AvatarManager>()->getMyAvatar();
+
+    // centerUI when hmd mode is first enabled
+    if (qApp->isHMDMode() && !_hmdMode) {
+        centerUI();
+    }
+    _hmdMode = qApp->isHMDMode();
+
+    bool prevDriving = _currentDriving;
+    bool isDriving = updateAvatarHasDriveInput();
+    bool drivingChanged = prevDriving != isDriving;
+
+    bool isAtRest = updateAvatarIsAtRest();
+
+    switch (getState()) {
+        case Enabled:
+            if (myAvatar->getClearOverlayWhenDriving() && qApp->isHMDMode() && headOutsideOverlay()) {
+                setState(DisabledByHead);
+                setEnabled(false);
             }
-            if (_wantsOverlays) {
-                setEnabled(!nowDriving);
+            if (userWishesToHide()) {
+                setState(DisabledByToggle);
+                setEnabled(false);
             }
-            _driving = nowDriving;
-        } // Else haven't accumulated enough time in new mode, but keep timing.
+            if (myAvatar->getClearOverlayWhenDriving() && drivingChanged && isDriving) {
+                setState(DisabledByDrive);
+                setEnabled(false);
+            }
+            break;
+        case DisabledByDrive:
+            if (!isDriving || userWishesToShow()) {
+                setState(Enabled);
+                setEnabled(true);
+            }
+            break;
+        case DisabledByHead:
+            if (isAtRest || userWishesToShow()) {
+                setState(Enabled);
+                setEnabled(true);
+            }
+            break;
+        case DisabledByToggle:
+            if (userWishesToShow()) {
+                setState(Enabled);
+                setEnabled(true);
+            }
+            break;
+        default:
+            break;
     }
 
-    Mode newMode;
-    if (qApp->isHMDMode()) {
-        newMode = SITTING;
-    } else {
-        newMode = FLAT;
-    }
-
-    if (newMode != _mode) {
-        switch (newMode) {
-        case SITTING: {
-            // enter the SITTING state
-            // place the overlay at origin
-            qApp->getApplicationCompositor().setModelTransform(Transform());
-            break;
-        }
-        case STANDING: {  // STANDING mode is not currently used.
-            // enter the STANDING state
-            // place the overlay at the current hmd position in world space
-            auto camMat = cancelOutRollAndPitch(myAvatar->getSensorToWorldMatrix() * qApp->getHMDSensorPose());
-            qApp->getApplicationCompositor().setModelTransform(Transform(camMat));
-            break;
-        }
-
-        case FLAT:
-            // do nothing
-            break;
-        }
-    }
-
-    _mode = newMode;
-
+    _prevOverlayMenuChecked = Menu::getInstance()->isOptionChecked(MenuOption::Overlays);
 }
 
 void OverlayConductor::setEnabled(bool enabled) {
@@ -143,13 +185,15 @@ void OverlayConductor::setEnabled(bool enabled) {
     _enabled = enabled; // set the new value
     auto offscreenUi = DependencyManager::get<OffscreenUi>();
     offscreenUi->setPinned(!_enabled);
+
+    // ensure that the the state of the menu item reflects the state of the overlay.
+    Menu::getInstance()->setIsOptionChecked(MenuOption::Overlays, _enabled);
+    _prevOverlayMenuChecked = _enabled;
+
     // if the new state is visible/enabled...
-    if (_enabled && _mode == STANDING) {
-        // place the overlay at the current hmd position in world space
-        MyAvatar* myAvatar = DependencyManager::get<AvatarManager>()->getMyAvatar();
-        auto camMat = cancelOutRollAndPitch(myAvatar->getSensorToWorldMatrix() * qApp->getHMDSensorPose());
-        qApp->getApplicationCompositor().setModelTransform(Transform(camMat));
-    } 
+    if (_enabled && qApp->isHMDMode()) {
+        centerUI();
+    }
 }
 
 bool OverlayConductor::getEnabled() const {
diff --git a/interface/src/ui/OverlayConductor.h b/interface/src/ui/OverlayConductor.h
index 1ec66663a4..375b2652f6 100644
--- a/interface/src/ui/OverlayConductor.h
+++ b/interface/src/ui/OverlayConductor.h
@@ -20,20 +20,41 @@ public:
     void setEnabled(bool enable);
     bool getEnabled() const;
 
-private:
-    void updateMode();
+    void centerUI();
 
-    enum Mode {
-        FLAT,
-        SITTING,
-        STANDING
+private:
+    bool headOutsideOverlay() const;
+    bool updateAvatarHasDriveInput();
+    bool updateAvatarIsAtRest();
+    bool userWishesToHide() const;
+    bool userWishesToShow() const;
+
+    enum State {
+        Enabled = 0,
+        DisabledByDrive,
+        DisabledByHead,
+        DisabledByToggle,
+        NumStates
     };
 
-    Mode _mode { FLAT };
+    void setState(State state);
+    State getState() const;
+
+    State _state { DisabledByDrive };
+
+    bool _prevOverlayMenuChecked { true };
     bool _enabled { false };
-    bool _driving { false };
-    quint64 _timeInPotentialMode { 0 };
-    bool _wantsOverlays { true };
+    bool _hmdMode { false };
+
+    // used by updateAvatarHasDriveInput
+    quint64 _desiredDrivingTimer { 0 };
+    bool _desiredDriving { false };
+    bool _currentDriving { false };
+
+    // used by updateAvatarIsAtRest
+    quint64 _desiredAtRestTimer { 0 };
+    bool _desiredAtRest { true };
+    bool _currentAtRest { true };
 };
 
 #endif
diff --git a/libraries/display-plugins/src/display-plugins/CompositorHelper.cpp b/libraries/display-plugins/src/display-plugins/CompositorHelper.cpp
index d4fff1b976..2d3c79071f 100644
--- a/libraries/display-plugins/src/display-plugins/CompositorHelper.cpp
+++ b/libraries/display-plugins/src/display-plugins/CompositorHelper.cpp
@@ -336,7 +336,9 @@ void CompositorHelper::computeHmdPickRay(const glm::vec2& cursorPos, glm::vec3&
 }
 
 glm::mat4 CompositorHelper::getUiTransform() const {
-    return _currentCamera * glm::inverse(_currentDisplayPlugin->getHeadPose());
+    glm::mat4 modelMat;
+    _modelTransform.getMatrix(modelMat);
+    return _currentCamera * glm::inverse(_currentDisplayPlugin->getHeadPose()) * modelMat;
 }
 
 //Finds the collision point of a world space ray
diff --git a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp
index 1616dcdb77..b9f6ad1a98 100644
--- a/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp
+++ b/libraries/display-plugins/src/display-plugins/hmd/HmdDisplayPlugin.cpp
@@ -253,12 +253,13 @@ void HmdDisplayPlugin::compositeScene() {
 void HmdDisplayPlugin::compositeOverlay() {
     using namespace oglplus;
     auto compositorHelper = DependencyManager::get<CompositorHelper>();
+    glm::mat4 modelMat = compositorHelper->getModelTransform().getMatrix();
 
     useProgram(_program);
     _sphereSection->Use();
     for_each_eye([&](Eye eye) {
         eyeViewport(eye);
-        auto modelView = glm::inverse(_currentPresentFrameInfo.presentPose * getEyeToHeadTransform(eye));
+        auto modelView = glm::inverse(_currentPresentFrameInfo.presentPose * getEyeToHeadTransform(eye)) * modelMat;
         auto mvp = _eyeProjections[eye] * modelView;
         Uniform<glm::mat4>(*_program, _mvpUniform).Set(mvp);
         _sphereSection->Draw();
diff --git a/libraries/entities/src/EntityScriptingInterface.cpp b/libraries/entities/src/EntityScriptingInterface.cpp
index d09fc60d9b..e0863041a1 100644
--- a/libraries/entities/src/EntityScriptingInterface.cpp
+++ b/libraries/entities/src/EntityScriptingInterface.cpp
@@ -32,6 +32,7 @@ EntityScriptingInterface::EntityScriptingInterface(bool bidOnSimulationOwnership
     auto nodeList = DependencyManager::get<NodeList>();
     connect(nodeList.data(), &NodeList::isAllowedEditorChanged, this, &EntityScriptingInterface::canAdjustLocksChanged);
     connect(nodeList.data(), &NodeList::canRezChanged, this, &EntityScriptingInterface::canRezChanged);
+    connect(nodeList.data(), &NodeList::canRezTmpChanged, this, &EntityScriptingInterface::canRezTmpChanged);
 }
 
 void EntityScriptingInterface::queueEntityMessage(PacketType packetType,
@@ -49,6 +50,11 @@ bool EntityScriptingInterface::canRez() {
     return nodeList->getThisNodeCanRez();
 }
 
+bool EntityScriptingInterface::canRezTmp() {
+    auto nodeList = DependencyManager::get<NodeList>();
+    return nodeList->getThisNodeCanRezTmp();
+}
+
 void EntityScriptingInterface::setEntityTree(EntityTreePointer elementTree) {
     if (_entityTree) {
         disconnect(_entityTree.get(), &EntityTree::addingEntity, this, &EntityScriptingInterface::addingEntity);
diff --git a/libraries/entities/src/EntityScriptingInterface.h b/libraries/entities/src/EntityScriptingInterface.h
index 8ae6a77dab..e9024eb721 100644
--- a/libraries/entities/src/EntityScriptingInterface.h
+++ b/libraries/entities/src/EntityScriptingInterface.h
@@ -80,6 +80,7 @@ public slots:
 
     // returns true if the DomainServer will allow this Node/Avatar to rez new entities
     Q_INVOKABLE bool canRez();
+    Q_INVOKABLE bool canRezTmp();
 
     /// adds a model with the specific properties
     Q_INVOKABLE QUuid addEntity(const EntityItemProperties& properties, bool clientOnly = false);
@@ -179,6 +180,7 @@ signals:
 
     void canAdjustLocksChanged(bool canAdjustLocks);
     void canRezChanged(bool canRez);
+    void canRezTmpChanged(bool canRez);
 
     void mousePressOnEntity(const EntityItemID& entityItemID, const MouseEvent& event);
     void mouseMoveOnEntity(const EntityItemID& entityItemID, const MouseEvent& event);
diff --git a/libraries/entities/src/EntityTree.cpp b/libraries/entities/src/EntityTree.cpp
index 581e0a9568..77a0c6d6fe 100644
--- a/libraries/entities/src/EntityTree.cpp
+++ b/libraries/entities/src/EntityTree.cpp
@@ -26,6 +26,8 @@
 #include "LogHandler.h"
 
 static const quint64 DELETED_ENTITIES_EXTRA_USECS_TO_CONSIDER = USECS_PER_MSEC * 50;
+const float EntityTree::DEFAULT_MAX_TMP_ENTITY_LIFETIME = 60 * 60; // 1 hour
+
 
 EntityTree::EntityTree(bool shouldReaverage) :
     Octree(shouldReaverage),
@@ -128,13 +130,16 @@ bool EntityTree::updateEntityWithElement(EntityItemPointer entity, const EntityI
     EntityItemProperties properties = origProperties;
 
     bool allowLockChange;
+    bool canRezPermanentEntities;
     QUuid senderID;
     if (senderNode.isNull()) {
         auto nodeList = DependencyManager::get<NodeList>();
         allowLockChange = nodeList->isAllowedEditor();
+        canRezPermanentEntities = nodeList->getThisNodeCanRez();
         senderID = nodeList->getSessionUUID();
     } else {
         allowLockChange = senderNode->isAllowedEditor();
+        canRezPermanentEntities = senderNode->getCanRez();
         senderID = senderNode->getUUID();
     }
 
@@ -143,6 +148,12 @@ bool EntityTree::updateEntityWithElement(EntityItemPointer entity, const EntityI
         return false;
     }
 
+    if (!canRezPermanentEntities && (entity->getLifetime() != properties.getLifetime())) {
+        // we don't allow a Node that can't create permanent entities to adjust lifetimes on existing ones
+        qCDebug(entities) << "Refusing disallowed entity lifetime adjustment.";
+        return false;
+    }
+
     // enforce support for locked entities. If an entity is currently locked, then the only
     // property we allow you to change is the locked property.
     if (entity->getLocked()) {
@@ -308,17 +319,39 @@ bool EntityTree::updateEntityWithElement(EntityItemPointer entity, const EntityI
     return true;
 }
 
+bool EntityTree::permissionsAllowRez(const EntityItemProperties& properties, bool canRez, bool canRezTmp) {
+    float lifeTime = properties.getLifetime();
+
+    if (lifeTime == 0.0f || lifeTime > _maxTmpEntityLifetime) {
+        // this is an attempt to rez a permanent entity.
+        if (!canRez) {
+            return false;
+        }
+    } else {
+        // this is an attempt to rez a temporary entity.
+        if (!canRezTmp) {
+            return false;
+        }
+    }
+
+    return true;
+}
+
 EntityItemPointer EntityTree::addEntity(const EntityItemID& entityID, const EntityItemProperties& properties) {
     EntityItemPointer result = NULL;
 
+    auto nodeList = DependencyManager::get<NodeList>();
+    if (!nodeList) {
+        qDebug() << "EntityTree::addEntity -- can't get NodeList";
+        return nullptr;
+    }
+
     bool clientOnly = properties.getClientOnly();
 
-    if (!clientOnly && getIsClient()) {
+    if (!clientOnly && getIsClient() &&
+        !permissionsAllowRez(properties, nodeList->getThisNodeCanRez(), nodeList->getThisNodeCanRezTmp())) {
         // if our Node isn't allowed to create entities in this domain, don't try.
-        auto nodeList = DependencyManager::get<NodeList>();
-        if (nodeList && !nodeList->getThisNodeCanRez()) {
-            return NULL;
-        }
+        return nullptr;
     }
 
     bool recordCreationTime = false;
@@ -920,7 +953,7 @@ int EntityTree::processEditPacketData(ReceivedMessage& message, const unsigned c
                     endUpdate = usecTimestampNow();
                     _totalUpdates++;
                 } else if (message.getType() == PacketType::EntityAdd) {
-                    if (senderNode->getCanRez()) {
+                    if (permissionsAllowRez(properties, senderNode->getCanRez(), senderNode->getCanRezTmp())) {
                         // this is a new entity... assign a new entityID
                         properties.setCreated(properties.getLastEdited());
                         startCreate = usecTimestampNow();
@@ -1430,6 +1463,12 @@ bool EntityTree::readFromMap(QVariantMap& map) {
     QVariantList entitiesQList = map["Entities"].toList();
     QScriptEngine scriptEngine;
 
+    if (entitiesQList.length() == 0) {
+        // Empty map or invalidly formed file.
+        return false;
+    }
+
+    bool success = true;
     foreach (QVariant entityVariant, entitiesQList) {
         // QVariantMap --> QScriptValue --> EntityItemProperties --> Entity
         QVariantMap entityMap = entityVariant.toMap();
@@ -1447,9 +1486,10 @@ bool EntityTree::readFromMap(QVariantMap& map) {
         EntityItemPointer entity = addEntity(entityItemID, properties);
         if (!entity) {
             qCDebug(entities) << "adding Entity failed:" << entityItemID << properties.getType();
+            success = false;
         }
     }
-    return true;
+    return success;
 }
 
 void EntityTree::resetClientEditStats() {
diff --git a/libraries/entities/src/EntityTree.h b/libraries/entities/src/EntityTree.h
index a85624c9ae..8afb8d878f 100644
--- a/libraries/entities/src/EntityTree.h
+++ b/libraries/entities/src/EntityTree.h
@@ -62,6 +62,10 @@ public:
 
     void createRootElement();
 
+
+    void setEntityMaxTmpLifetime(float maxTmpEntityLifetime) { _maxTmpEntityLifetime = maxTmpEntityLifetime; }
+    bool permissionsAllowRez(const EntityItemProperties& properties, bool canRez, bool canRezTmp);
+
     /// Implements our type specific root element factory
     virtual OctreeElementPointer createNewElement(unsigned char* octalCode = NULL) override;
 
@@ -252,6 +256,8 @@ public:
 
     void notifyNewCollisionSoundURL(const QString& newCollisionSoundURL, const EntityItemID& entityID);
 
+    static const float DEFAULT_MAX_TMP_ENTITY_LIFETIME;
+
 public slots:
     void callLoader(EntityItemID entityID);
 
@@ -331,6 +337,8 @@ protected:
     // we maintain a list of avatarIDs to notice when an entity is a child of one.
     QSet<QUuid> _avatarIDs; // IDs of avatars connected to entity server
     QHash<QUuid, QSet<EntityItemID>> _childrenOfAvatars;  // which entities are children of which avatars
+
+    float _maxTmpEntityLifetime { DEFAULT_MAX_TMP_ENTITY_LIFETIME };
 };
 
 #endif // hifi_EntityTree_h
diff --git a/libraries/networking/src/AddressManager.cpp b/libraries/networking/src/AddressManager.cpp
index 1b7ed11cce..80989acd2c 100644
--- a/libraries/networking/src/AddressManager.cpp
+++ b/libraries/networking/src/AddressManager.cpp
@@ -144,12 +144,21 @@ bool AddressManager::handleUrl(const QUrl& lookupUrl, LookupTrigger trigger) {
         // 4. domain network address (IP or dns resolvable hostname)
 
         // use our regex'ed helpers to figure out what we're supposed to do with this
-        if (!handleUsername(lookupUrl.authority())) {
+        if (handleUsername(lookupUrl.authority())) {
+            // handled a username for lookup
+
+            // in case we're failing to connect to where we thought this user was
+            // store their username as previous lookup so we can refresh their location via API
+            _previousLookup = lookupUrl;
+        } else {
             // we're assuming this is either a network address or global place name
             // check if it is a network address first
             bool hostChanged;
             if (handleNetworkAddress(lookupUrl.host()
-                                      + (lookupUrl.port() == -1 ? "" : ":" + QString::number(lookupUrl.port())), trigger, hostChanged)) {
+                                     + (lookupUrl.port() == -1 ? "" : ":" + QString::number(lookupUrl.port())), trigger, hostChanged)) {
+
+                // a network address lookup clears the previous lookup since we don't expect to re-attempt it
+                _previousLookup.clear();
 
                 // If the host changed then we have already saved to history
                 if (hostChanged) {
@@ -165,10 +174,16 @@ bool AddressManager::handleUrl(const QUrl& lookupUrl, LookupTrigger trigger) {
                 // we may have a path that defines a relative viewpoint - if so we should jump to that now
                 handlePath(path, trigger);
             } else if (handleDomainID(lookupUrl.host())){
+                // store this domain ID as the previous lookup in case we're failing to connect and want to refresh API info
+                _previousLookup = lookupUrl;
+
                 // no place name - this is probably a domain ID
                 // try to look up the domain ID on the metaverse API
                 attemptDomainIDLookup(lookupUrl.host(), lookupUrl.path(), trigger);
             } else {
+                // store this place name as the previous lookup in case we fail to connect and want to refresh API info
+                _previousLookup = lookupUrl;
+
                 // wasn't an address - lookup the place name
                 // we may have a path that defines a relative viewpoint - pass that through the lookup so we can go to it after
                 attemptPlaceNameLookup(lookupUrl.host(), lookupUrl.path(), trigger);
@@ -180,9 +195,13 @@ bool AddressManager::handleUrl(const QUrl& lookupUrl, LookupTrigger trigger) {
     } else if (lookupUrl.toString().startsWith('/')) {
         qCDebug(networking) << "Going to relative path" << lookupUrl.path();
 
+        // a path lookup clears the previous lookup since we don't expect to re-attempt it
+        _previousLookup.clear();
+
         // if this is a relative path then handle it as a relative viewpoint
         handlePath(lookupUrl.path(), trigger, true);
         emit lookupResultsFinished();
+
         return true;
     }
 
@@ -276,7 +295,7 @@ void AddressManager::goToAddressFromObject(const QVariantMap& dataObject, const
 
                     qCDebug(networking) << "Possible domain change required to connect to" << domainHostname
                         << "on" << domainPort;
-                    emit possibleDomainChangeRequired(domainHostname, domainPort);
+                    emit possibleDomainChangeRequired(domainHostname, domainPort, domainID);
                 } else {
                     QString iceServerAddress = domainObject[DOMAIN_ICE_SERVER_ADDRESS_KEY].toString();
 
@@ -315,7 +334,10 @@ void AddressManager::goToAddressFromObject(const QVariantMap& dataObject, const
                 QString overridePath = reply.property(OVERRIDE_PATH_KEY).toString();
 
                 if (!overridePath.isEmpty()) {
-                    handlePath(overridePath, trigger);
+                    // make sure we don't re-handle an overriden path if this was a refresh of info from API
+                    if (trigger != LookupTrigger::AttemptedRefresh) {
+                        handlePath(overridePath, trigger);
+                    }
                 } else {
                     // take the path that came back
                     const QString PLACE_PATH_KEY = "path";
@@ -362,7 +384,7 @@ void AddressManager::handleAPIError(QNetworkReply& errorReply) {
 
     if (errorReply.error() == QNetworkReply::ContentNotFoundError) {
         // if this is a lookup that has no result, don't keep re-trying it
-        //_previousLookup.clear();
+        _previousLookup.clear();
 
         emit lookupResultIsNotFound();
     }
@@ -598,7 +620,7 @@ bool AddressManager::setDomainInfo(const QString& hostname, quint16 port, Lookup
 
     DependencyManager::get<NodeList>()->flagTimeForConnectionStep(LimitedNodeList::ConnectionStep::HandleAddress);
 
-    emit possibleDomainChangeRequired(hostname, port);
+    emit possibleDomainChangeRequired(hostname, port, QUuid());
 
     return hostChanged;
 }
@@ -618,6 +640,13 @@ void AddressManager::goToUser(const QString& username) {
                                               QByteArray(), nullptr, requestParams);
 }
 
+void AddressManager::refreshPreviousLookup() {
+    // if we have a non-empty previous lookup, fire it again now (but don't re-store it in the history)
+    if (!_previousLookup.isEmpty()) {
+        handleUrl(_previousLookup, LookupTrigger::AttemptedRefresh);
+    }
+}
+
 void AddressManager::copyAddress() {
     QApplication::clipboard()->setText(currentAddress().toString());
 }
@@ -629,7 +658,10 @@ void AddressManager::copyPath() {
 void AddressManager::addCurrentAddressToHistory(LookupTrigger trigger) {
 
     // if we're cold starting and this is called for the first address (from settings) we don't do anything
-    if (trigger != LookupTrigger::StartupFromSettings && trigger != LookupTrigger::DomainPathResponse) {
+    if (trigger != LookupTrigger::StartupFromSettings
+        && trigger != LookupTrigger::DomainPathResponse
+        && trigger != LookupTrigger::AttemptedRefresh) {
+
         if (trigger == LookupTrigger::Back) {
             // we're about to push to the forward stack
             // if it's currently empty emit our signal to say that going forward is now possible
diff --git a/libraries/networking/src/AddressManager.h b/libraries/networking/src/AddressManager.h
index 643924ff5c..a3aaee3ba2 100644
--- a/libraries/networking/src/AddressManager.h
+++ b/libraries/networking/src/AddressManager.h
@@ -48,7 +48,8 @@ public:
         Forward,
         StartupFromSettings,
         DomainPathResponse,
-        Internal
+        Internal,
+        AttemptedRefresh
     };
 
     bool isConnected();
@@ -89,6 +90,8 @@ public slots:
 
     void goToUser(const QString& username);
 
+    void refreshPreviousLookup();
+
     void storeCurrentAddress();
 
     void copyAddress();
@@ -99,7 +102,7 @@ signals:
     void lookupResultIsOffline();
     void lookupResultIsNotFound();
 
-    void possibleDomainChangeRequired(const QString& newHostname, quint16 newPort);
+    void possibleDomainChangeRequired(const QString& newHostname, quint16 newPort, const QUuid& domainID);
     void possibleDomainChangeRequiredViaICEForID(const QString& iceServerHostname, const QUuid& domainID);
 
     void locationChangeRequired(const glm::vec3& newPosition,
@@ -152,6 +155,8 @@ private:
     quint64 _lastBackPush = 0;
 
     QString _newHostLookupPath;
+    
+    QUrl _previousLookup;
 };
 
 #endif // hifi_AddressManager_h
diff --git a/libraries/networking/src/DomainHandler.cpp b/libraries/networking/src/DomainHandler.cpp
index 4f85296f03..6880b7a329 100644
--- a/libraries/networking/src/DomainHandler.cpp
+++ b/libraries/networking/src/DomainHandler.cpp
@@ -14,6 +14,7 @@
 #include <QtCore/QJsonDocument>
 #include <QtCore/QDataStream>
 
+#include "AddressManager.h"
 #include "Assignment.h"
 #include "HifiSockAddr.h"
 #include "NodeList.h"
@@ -28,17 +29,10 @@
 
 DomainHandler::DomainHandler(QObject* parent) :
     QObject(parent),
-    _uuid(),
     _sockAddr(HifiSockAddr(QHostAddress::Null, DEFAULT_DOMAIN_SERVER_PORT)),
-    _assignmentUUID(),
-    _connectionToken(),
-    _iceDomainID(),
-    _iceClientID(),
-    _iceServerSockAddr(),
     _icePeer(this),
-    _isConnected(false),
-    _settingsObject(),
-    _settingsTimer(this)
+    _settingsTimer(this),
+    _apiRefreshTimer(this)
 {
     _sockAddr.setObjectName("DomainServer");
 
@@ -49,6 +43,16 @@ DomainHandler::DomainHandler(QObject* parent) :
     static const int DOMAIN_SETTINGS_TIMEOUT_MS = 5000;
     _settingsTimer.setInterval(DOMAIN_SETTINGS_TIMEOUT_MS);
     connect(&_settingsTimer, &QTimer::timeout, this, &DomainHandler::settingsReceiveFail);
+
+    // setup the API refresh timer for auto connection information refresh from API when failing to connect
+    const int API_REFRESH_TIMEOUT_MSEC = 2500;
+    _apiRefreshTimer.setInterval(API_REFRESH_TIMEOUT_MSEC);
+
+    auto addressManager = DependencyManager::get<AddressManager>();
+    connect(&_apiRefreshTimer, &QTimer::timeout, addressManager.data(), &AddressManager::refreshPreviousLookup);
+
+    // stop the refresh timer if we connect to a domain
+    connect(this, &DomainHandler::connectedToDomain, &_apiRefreshTimer, &QTimer::stop);
 }
 
 void DomainHandler::disconnect() {
@@ -93,10 +97,14 @@ void DomainHandler::softReset() {
     
     clearSettings();
 
+    _domainConnectionRefusals.clear();
     _connectionDenialsSinceKeypairRegen = 0;
 
     // cancel the failure timeout for any pending requests for settings
     QMetaObject::invokeMethod(&_settingsTimer, "stop");
+
+    // restart the API refresh timer in case we fail to connect and need to refresh information
+    QMetaObject::invokeMethod(&_apiRefreshTimer, "start");
 }
 
 void DomainHandler::hardReset() {
@@ -105,7 +113,7 @@ void DomainHandler::hardReset() {
     softReset();
 
     qCDebug(networking) << "Hard reset in NodeList DomainHandler.";
-    _iceDomainID = QUuid();
+    _pendingDomainID = QUuid();
     _iceServerSockAddr = HifiSockAddr();
     _hostname = QString();
     _sockAddr.clear();
@@ -139,7 +147,9 @@ void DomainHandler::setUUID(const QUuid& uuid) {
     }
 }
 
-void DomainHandler::setHostnameAndPort(const QString& hostname, quint16 port) {
+void DomainHandler::setSocketAndID(const QString& hostname, quint16 port, const QUuid& domainID) {
+
+    _pendingDomainID = domainID;
 
     if (hostname != _hostname || _sockAddr.getPort() != port) {
         // re-set the domain info so that auth information is reloaded
@@ -149,9 +159,6 @@ void DomainHandler::setHostnameAndPort(const QString& hostname, quint16 port) {
             // set the new hostname
             _hostname = hostname;
 
-            // FIXME - is this the right place???
-            _domainConnectionRefusals.clear();
-
             qCDebug(networking) << "Updated domain hostname to" << _hostname;
 
             // re-set the sock addr to null and fire off a lookup of the IP address for this domain-server's hostname
@@ -174,14 +181,15 @@ void DomainHandler::setHostnameAndPort(const QString& hostname, quint16 port) {
 }
 
 void DomainHandler::setIceServerHostnameAndID(const QString& iceServerHostname, const QUuid& id) {
-    if (id != _uuid) {
+
+    if (_iceServerSockAddr.getAddress().toString() != iceServerHostname || id != _pendingDomainID) {
         // re-set the domain info to connect to new domain
         hardReset();
         
         // refresh our ICE client UUID to something new
         _iceClientID = QUuid::createUuid();
         
-        _iceDomainID = id;
+        _pendingDomainID = id;
 
         HifiSockAddr* replaceableSockAddr = &_iceServerSockAddr;
         replaceableSockAddr->~HifiSockAddr();
@@ -255,6 +263,7 @@ void DomainHandler::setIsConnected(bool isConnected) {
 
             // we've connected to new domain - time to ask it for global settings
             requestDomainSettings();
+
         } else {
             emit disconnectedFromDomain();
         }
@@ -305,6 +314,9 @@ void DomainHandler::processICEPingReplyPacket(QSharedPointer<ReceivedMessage> me
     qCDebug(networking) << "Received reply from domain-server on" << senderSockAddr;
 
     if (getIP().isNull()) {
+        // we're hearing back from this domain-server, no need to refresh API information
+        _apiRefreshTimer.stop();
+
         // for now we're unsafely assuming this came back from the domain
         if (senderSockAddr == _icePeer.getLocalSocket()) {
             qCDebug(networking) << "Connecting to domain using local socket";
@@ -333,17 +345,20 @@ void DomainHandler::processDTLSRequirementPacket(QSharedPointer<ReceivedMessage>
 void DomainHandler::processICEResponsePacket(QSharedPointer<ReceivedMessage> message) {
     if (_icePeer.hasSockets()) {
         qDebug() << "Received an ICE peer packet for domain-server but we already have sockets. Not processing.";
-        // bail on processing this packet if our ice peer doesn't have sockets
+        // bail on processing this packet if our ice peer already has sockets
         return;
     }
 
+    // start or restart the API refresh timer now that we have new information
+    _apiRefreshTimer.start();
+
     QDataStream iceResponseStream(message->getMessage());
 
     iceResponseStream >> _icePeer;
 
     DependencyManager::get<NodeList>()->flagTimeForConnectionStep(LimitedNodeList::ConnectionStep::ReceiveDSPeerInformation);
 
-    if (_icePeer.getUUID() != _iceDomainID) {
+    if (_icePeer.getUUID() != _pendingDomainID) {
         qCDebug(networking) << "Received a network peer with ID that does not match current domain. Will not attempt connection.";
         _icePeer.reset();
     } else {
@@ -373,6 +388,9 @@ bool DomainHandler::reasonSuggestsLogin(ConnectionRefusedReason reasonCode) {
 }
 
 void DomainHandler::processDomainServerConnectionDeniedPacket(QSharedPointer<ReceivedMessage> message) {
+    // we're hearing from this domain-server, don't need to refresh API info
+    _apiRefreshTimer.stop();
+
     // Read deny reason from packet
     uint8_t reasonCodeWire;
 
diff --git a/libraries/networking/src/DomainHandler.h b/libraries/networking/src/DomainHandler.h
index bcee7668d1..1328174e87 100644
--- a/libraries/networking/src/DomainHandler.h
+++ b/libraries/networking/src/DomainHandler.h
@@ -58,8 +58,8 @@ public:
     
     const QUuid& getAssignmentUUID() const { return _assignmentUUID; }
     void setAssignmentUUID(const QUuid& assignmentUUID) { _assignmentUUID = assignmentUUID; }
-    
-    const QUuid& getICEDomainID() const { return _iceDomainID; }
+
+    const QUuid& getPendingDomainID() const { return _pendingDomainID; }
 
     const QUuid& getICEClientID() const { return _iceClientID; }
 
@@ -75,7 +75,6 @@ public:
     bool hasSettings() const { return !_settingsObject.isEmpty(); }
     void requestDomainSettings();
     const QJsonObject& getSettingsObject() const { return _settingsObject; }
-
    
     void setPendingPath(const QString& pendingPath) { _pendingPath = pendingPath; }
     const QString& getPendingPath() { return _pendingPath; }
@@ -94,7 +93,7 @@ public:
     };
 
 public slots:
-    void setHostnameAndPort(const QString& hostname, quint16 port = DEFAULT_DOMAIN_SERVER_PORT);
+    void setSocketAndID(const QString& hostname, quint16 port = DEFAULT_DOMAIN_SERVER_PORT, const QUuid& id = QUuid());
     void setIceServerHostnameAndID(const QString& iceServerHostname, const QUuid& id);
 
     void processSettingsPacketList(QSharedPointer<ReceivedMessage> packetList);
@@ -136,11 +135,11 @@ private:
     HifiSockAddr _sockAddr;
     QUuid _assignmentUUID;
     QUuid _connectionToken;
-    QUuid _iceDomainID;
+    QUuid _pendingDomainID; // ID of domain being connected to, via ICE or direct connection
     QUuid _iceClientID;
     HifiSockAddr _iceServerSockAddr;
     NetworkPeer _icePeer;
-    bool _isConnected;
+    bool _isConnected { false };
     QJsonObject _settingsObject;
     QString _pendingPath;
     QTimer _settingsTimer;
@@ -148,6 +147,8 @@ private:
     QStringList _domainConnectionRefusals;
     bool _hasCheckedForAccessToken { false };
     int _connectionDenialsSinceKeypairRegen { 0 };
+
+    QTimer _apiRefreshTimer;
 };
 
 #endif // hifi_DomainHandler_h
diff --git a/libraries/networking/src/LimitedNodeList.cpp b/libraries/networking/src/LimitedNodeList.cpp
index 9efe51183e..d7a2d47fab 100644
--- a/libraries/networking/src/LimitedNodeList.cpp
+++ b/libraries/networking/src/LimitedNodeList.cpp
@@ -52,7 +52,7 @@ LimitedNodeList::LimitedNodeList(unsigned short socketListenPort, unsigned short
     _numCollectedPackets(0),
     _numCollectedBytes(0),
     _packetStatTimer(),
-    _thisNodeCanRez(true)
+    _permissions(NodePermissions())
 {
     static bool firstCall = true;
     if (firstCall) {
@@ -130,17 +130,22 @@ void LimitedNodeList::setSessionUUID(const QUuid& sessionUUID) {
     }
 }
 
-void LimitedNodeList::setIsAllowedEditor(bool isAllowedEditor) {
-    if (_isAllowedEditor != isAllowedEditor) {
-        _isAllowedEditor = isAllowedEditor;
-        emit isAllowedEditorChanged(isAllowedEditor);
-    }
-}
+void LimitedNodeList::setPermissions(const NodePermissions& newPermissions) {
+    NodePermissions originalPermissions = _permissions;
 
-void LimitedNodeList::setThisNodeCanRez(bool canRez) {
-    if (_thisNodeCanRez != canRez) {
-        _thisNodeCanRez = canRez;
-        emit canRezChanged(canRez);
+    _permissions = newPermissions;
+
+    if (originalPermissions.canAdjustLocks != newPermissions.canAdjustLocks) {
+        emit isAllowedEditorChanged(_permissions.canAdjustLocks);
+    }
+    if (originalPermissions.canRezPermanentEntities != newPermissions.canRezPermanentEntities) {
+        emit canRezChanged(_permissions.canRezPermanentEntities);
+    }
+    if (originalPermissions.canRezTemporaryEntities != newPermissions.canRezTemporaryEntities) {
+        emit canRezTmpChanged(_permissions.canRezTemporaryEntities);
+    }
+    if (originalPermissions.canWriteToAssetServer != newPermissions.canWriteToAssetServer) {
+        emit canWriteAssetsChanged(_permissions.canWriteToAssetServer);
     }
 }
 
@@ -515,7 +520,7 @@ void LimitedNodeList::handleNodeKill(const SharedNodePointer& node) {
 
 SharedNodePointer LimitedNodeList::addOrUpdateNode(const QUuid& uuid, NodeType_t nodeType,
                                                    const HifiSockAddr& publicSocket, const HifiSockAddr& localSocket,
-                                                   bool isAllowedEditor, bool canRez,
+                                                   const NodePermissions& permissions,
                                                    const QUuid& connectionSecret) {
     NodeHash::const_iterator it = _nodeHash.find(uuid);
 
@@ -524,14 +529,13 @@ SharedNodePointer LimitedNodeList::addOrUpdateNode(const QUuid& uuid, NodeType_t
 
         matchingNode->setPublicSocket(publicSocket);
         matchingNode->setLocalSocket(localSocket);
-        matchingNode->setIsAllowedEditor(isAllowedEditor);
-        matchingNode->setCanRez(canRez);
+        matchingNode->setPermissions(permissions);
         matchingNode->setConnectionSecret(connectionSecret);
 
         return matchingNode;
     } else {
         // we didn't have this node, so add them
-        Node* newNode = new Node(uuid, nodeType, publicSocket, localSocket, isAllowedEditor, canRez, connectionSecret, this);
+        Node* newNode = new Node(uuid, nodeType, publicSocket, localSocket, permissions, connectionSecret, this);
 
         if (nodeType == NodeType::AudioMixer) {
             LimitedNodeList::flagTimeForConnectionStep(LimitedNodeList::AddedAudioMixer);
diff --git a/libraries/networking/src/LimitedNodeList.h b/libraries/networking/src/LimitedNodeList.h
index 5a3c10e8c3..483aa0734c 100644
--- a/libraries/networking/src/LimitedNodeList.h
+++ b/libraries/networking/src/LimitedNodeList.h
@@ -104,12 +104,12 @@ public:
     const QUuid& getSessionUUID() const { return _sessionUUID; }
     void setSessionUUID(const QUuid& sessionUUID);
 
-    bool isAllowedEditor() const { return _isAllowedEditor; }
-    void setIsAllowedEditor(bool isAllowedEditor);
+    void setPermissions(const NodePermissions& newPermissions);
+    bool isAllowedEditor() const { return _permissions.canAdjustLocks; }
+    bool getThisNodeCanRez() const { return _permissions.canRezPermanentEntities; }
+    bool getThisNodeCanRezTmp() const { return _permissions.canRezTemporaryEntities; }
+    bool getThisNodeCanWriteAssets() const { return _permissions.canWriteToAssetServer; }
 
-    bool getThisNodeCanRez() const { return _thisNodeCanRez; }
-    void setThisNodeCanRez(bool canRez);
-    
     quint16 getSocketLocalPort() const { return _nodeSocket.localPort(); }
     QUdpSocket& getDTLSSocket();
 
@@ -137,7 +137,7 @@ public:
 
     SharedNodePointer addOrUpdateNode(const QUuid& uuid, NodeType_t nodeType,
                                       const HifiSockAddr& publicSocket, const HifiSockAddr& localSocket,
-                                      bool isAllowedEditor = false, bool canRez = false,
+                                      const NodePermissions& permissions = DEFAULT_AGENT_PERMISSIONS,
                                       const QUuid& connectionSecret = QUuid());
 
     bool hasCompletedInitialSTUN() const { return _hasCompletedInitialSTUN; }
@@ -254,6 +254,8 @@ signals:
 
     void isAllowedEditorChanged(bool isAllowedEditor);
     void canRezChanged(bool canRez);
+    void canRezTmpChanged(bool canRezTmp);
+    void canWriteAssetsChanged(bool canWriteAssets);
 
 protected slots:
     void connectedForLocalSocketTest();
@@ -300,8 +302,7 @@ protected:
     int _numCollectedBytes;
 
     QElapsedTimer _packetStatTimer;
-    bool _isAllowedEditor { false };
-    bool _thisNodeCanRez;
+    NodePermissions _permissions;
 
     QPointer<QTimer> _initialSTUNTimer;
 
diff --git a/libraries/networking/src/NLPacket.cpp b/libraries/networking/src/NLPacket.cpp
index 34a159ae6c..a11dd69753 100644
--- a/libraries/networking/src/NLPacket.cpp
+++ b/libraries/networking/src/NLPacket.cpp
@@ -184,6 +184,11 @@ void NLPacket::setType(PacketType type) {
     writeTypeAndVersion();
 }
 
+void NLPacket::setVersion(PacketVersion version) {
+    _version = version;
+    writeTypeAndVersion();
+}
+
 void NLPacket::readType() {
     _type = NLPacket::typeInHeader(*this);
 }
diff --git a/libraries/networking/src/NLPacket.h b/libraries/networking/src/NLPacket.h
index f49f8498a5..33de262dfb 100644
--- a/libraries/networking/src/NLPacket.h
+++ b/libraries/networking/src/NLPacket.h
@@ -65,6 +65,7 @@ public:
     void setType(PacketType type);
     
     PacketVersion getVersion() const { return _version; }
+    void setVersion(PacketVersion version);
 
     const QUuid& getSourceID() const { return _sourceID; }
     
diff --git a/libraries/networking/src/Node.cpp b/libraries/networking/src/Node.cpp
index 1e1cec2413..7201b2fd9a 100644
--- a/libraries/networking/src/Node.cpp
+++ b/libraries/networking/src/Node.cpp
@@ -16,6 +16,7 @@
 
 #include "Node.h"
 #include "SharedUtil.h"
+#include "NodePermissions.h"
 
 #include <QtCore/QDataStream>
 #include <QtCore/QDebug>
@@ -47,7 +48,7 @@ const QString& NodeType::getNodeTypeName(NodeType_t nodeType) {
 }
 
 Node::Node(const QUuid& uuid, NodeType_t type, const HifiSockAddr& publicSocket,
-           const HifiSockAddr& localSocket, bool isAllowedEditor, bool canRez, const QUuid& connectionSecret,
+           const HifiSockAddr& localSocket, const NodePermissions& permissions, const QUuid& connectionSecret,
            QObject* parent) :
     NetworkPeer(uuid, publicSocket, localSocket, parent),
     _type(type),
@@ -57,8 +58,7 @@ Node::Node(const QUuid& uuid, NodeType_t type, const HifiSockAddr& publicSocket,
     _clockSkewUsec(0),
     _mutex(),
     _clockSkewMovingPercentile(30, 0.8f),   // moving 80th percentile of 30 samples
-    _isAllowedEditor(isAllowedEditor),
-    _canRez(canRez)
+    _permissions(permissions)
 {
     // Update socket's object name
     setType(_type);
@@ -78,15 +78,12 @@ void Node::updateClockSkewUsec(qint64 clockSkewSample) {
     _clockSkewUsec = (quint64)_clockSkewMovingPercentile.getValueAtPercentile();
 }
 
-
 QDataStream& operator<<(QDataStream& out, const Node& node) {
     out << node._type;
     out << node._uuid;
     out << node._publicSocket;
     out << node._localSocket;
-    out << node._isAllowedEditor;
-    out << node._canRez;
-
+    out << node._permissions;
     return out;
 }
 
@@ -95,9 +92,7 @@ QDataStream& operator>>(QDataStream& in, Node& node) {
     in >> node._uuid;
     in >> node._publicSocket;
     in >> node._localSocket;
-    in >> node._isAllowedEditor;
-    in >> node._canRez;
-
+    in >> node._permissions;
     return in;
 }
 
diff --git a/libraries/networking/src/Node.h b/libraries/networking/src/Node.h
index 3927672319..b277ac0083 100644
--- a/libraries/networking/src/Node.h
+++ b/libraries/networking/src/Node.h
@@ -27,13 +27,14 @@
 #include "NodeType.h"
 #include "SimpleMovingAverage.h"
 #include "MovingPercentile.h"
+#include "NodePermissions.h"
 
 class Node : public NetworkPeer {
     Q_OBJECT
 public:
     Node(const QUuid& uuid, NodeType_t type,
          const HifiSockAddr& publicSocket, const HifiSockAddr& localSocket,
-         bool isAllowedEditor, bool canRez, const QUuid& connectionSecret = QUuid(),
+         const NodePermissions& permissions, const QUuid& connectionSecret = QUuid(),
          QObject* parent = 0);
 
     bool operator==(const Node& otherNode) const { return _uuid == otherNode._uuid; }
@@ -58,11 +59,12 @@ public:
     void updateClockSkewUsec(qint64 clockSkewSample);
     QMutex& getMutex() { return _mutex; }
 
-    void setIsAllowedEditor(bool isAllowedEditor) { _isAllowedEditor = isAllowedEditor; }
-    bool isAllowedEditor() { return _isAllowedEditor; }
-
-    void setCanRez(bool canRez) { _canRez = canRez; }
-    bool getCanRez() { return _canRez; }
+    void setPermissions(const NodePermissions& newPermissions) { _permissions = newPermissions; }
+    NodePermissions getPermissions() const { return _permissions; }
+    bool isAllowedEditor() const { return _permissions.canAdjustLocks; }
+    bool getCanRez() const { return _permissions.canRezPermanentEntities; }
+    bool getCanRezTmp() const { return _permissions.canRezTemporaryEntities; }
+    bool getCanWriteToAssetServer() const { return _permissions.canWriteToAssetServer; }
 
     friend QDataStream& operator<<(QDataStream& out, const Node& node);
     friend QDataStream& operator>>(QDataStream& in, Node& node);
@@ -81,8 +83,7 @@ private:
     qint64 _clockSkewUsec;
     QMutex _mutex;
     MovingPercentile _clockSkewMovingPercentile;
-    bool _isAllowedEditor;
-    bool _canRez;
+    NodePermissions _permissions;
 };
 
 Q_DECLARE_METATYPE(Node*)
diff --git a/libraries/networking/src/NodeList.cpp b/libraries/networking/src/NodeList.cpp
index 16a4083b08..fd1442d639 100644
--- a/libraries/networking/src/NodeList.cpp
+++ b/libraries/networking/src/NodeList.cpp
@@ -50,7 +50,7 @@ NodeList::NodeList(char newOwnerType, unsigned short socketListenPort, unsigned
 
     // handle domain change signals from AddressManager
     connect(addressManager.data(), &AddressManager::possibleDomainChangeRequired,
-            &_domainHandler, &DomainHandler::setHostnameAndPort);
+            &_domainHandler, &DomainHandler::setSocketAndID);
 
     connect(addressManager.data(), &AddressManager::possibleDomainChangeRequiredViaICEForID,
             &_domainHandler, &DomainHandler::setIceServerHostnameAndID);
@@ -250,7 +250,6 @@ void NodeList::sendDomainServerCheckIn() {
         qCDebug(networking) << "Waiting for ICE discovered domain-server socket. Will not send domain-server check in.";
         handleICEConnectionToDomainServer();
     } else if (!_domainHandler.getIP().isNull()) {
-        bool isUsingDTLS = false;
 
         PacketType domainPacketType = !_domainHandler.isConnected()
             ? PacketType::DomainConnectRequest : PacketType::DomainListRequest;
@@ -292,12 +291,18 @@ void NodeList::sendDomainServerCheckIn() {
             return;
         }
 
-        auto packetVersion = (domainPacketType == PacketType::DomainConnectRequest) ? _domainConnectRequestVersion : 0;
-        auto domainPacket = NLPacket::create(domainPacketType, -1, false, false, packetVersion);
+        auto domainPacket = NLPacket::create(domainPacketType);
         
         QDataStream packetStream(domainPacket.get());
 
         if (domainPacketType == PacketType::DomainConnectRequest) {
+
+#if (PR_BUILD || DEV_BUILD)
+            if (_shouldSendNewerVersion) {
+                domainPacket->setVersion(versionForPacketType(domainPacketType) + 1);
+            }
+#endif
+
             QUuid connectUUID;
 
             if (!_domainHandler.getAssignmentUUID().isNull()) {
@@ -315,18 +320,14 @@ void NodeList::sendDomainServerCheckIn() {
             packetStream << connectUUID;
 
             // include the protocol version signature in our connect request
-            if (_domainConnectRequestVersion >= static_cast<PacketVersion>(DomainConnectRequestVersion::HasProtocolVersions)) {
-                QByteArray protocolVersionSig = protocolVersionsSignature();
-                packetStream.writeBytes(protocolVersionSig.constData(), protocolVersionSig.size());
-            }
+            QByteArray protocolVersionSig = protocolVersionsSignature();
+            packetStream.writeBytes(protocolVersionSig.constData(), protocolVersionSig.size());
         }
 
         // pack our data to send to the domain-server including
         // the hostname information (so the domain-server can see which place name we came in on)
         packetStream << _ownerType << _publicSockAddr << _localSockAddr << _nodeTypesOfInterest.toList();
-        if (_domainConnectRequestVersion >= static_cast<PacketVersion>(DomainConnectRequestVersion::HasHostname)) {
-            packetStream << DependencyManager::get<AddressManager>()->getPlaceName();
-        }
+        packetStream << DependencyManager::get<AddressManager>()->getPlaceName();
 
         if (!_domainHandler.isConnected()) {
             DataServerAccountInfo& accountInfo = accountManager->getAccountInfo();
@@ -341,9 +342,7 @@ void NodeList::sendDomainServerCheckIn() {
 
         flagTimeForConnectionStep(LimitedNodeList::ConnectionStep::SendDSCheckIn);
 
-        if (!isUsingDTLS) {
-            sendPacket(std::move(domainPacket), _domainHandler.getSockAddr());
-        }
+        sendPacket(std::move(domainPacket), _domainHandler.getSockAddr());
 
         if (_numNoReplyDomainCheckIns >= MAX_SILENT_DOMAIN_SERVER_CHECK_INS) {
             // we haven't heard back from DS in MAX_SILENT_DOMAIN_SERVER_CHECK_INS
@@ -462,7 +461,7 @@ void NodeList::handleICEConnectionToDomainServer() {
 
         LimitedNodeList::sendPeerQueryToIceServer(_domainHandler.getICEServerSockAddr(),
                                                   _domainHandler.getICEClientID(),
-                                                  _domainHandler.getICEDomainID());
+                                                  _domainHandler.getPendingDomainID());
     }
 }
 
@@ -475,7 +474,7 @@ void NodeList::pingPunchForDomainServer() {
 
         if (_domainHandler.getICEPeer().getConnectionAttempts() == 0) {
             qCDebug(networking) << "Sending ping packets to establish connectivity with domain-server with ID"
-                << uuidStringWithoutCurlyBraces(_domainHandler.getICEDomainID());
+                << uuidStringWithoutCurlyBraces(_domainHandler.getPendingDomainID());
         } else {
             if (_domainHandler.getICEPeer().getConnectionAttempts() % NUM_DOMAIN_SERVER_PINGS_BEFORE_RESET == 0) {
                 // if we have then nullify the domain handler's network peer and send a fresh ICE heartbeat
@@ -527,7 +526,7 @@ void NodeList::processDomainServerList(QSharedPointer<ReceivedMessage> message)
     DependencyManager::get<NodeList>()->flagTimeForConnectionStep(LimitedNodeList::ConnectionStep::ReceiveDSList);
 
     QDataStream packetStream(message->getMessage());
-    
+
     // grab the domain's ID from the beginning of the packet
     QUuid domainUUID;
     packetStream >> domainUUID;
@@ -543,14 +542,11 @@ void NodeList::processDomainServerList(QSharedPointer<ReceivedMessage> message)
     packetStream >> newUUID;
     setSessionUUID(newUUID);
 
-    quint8 isAllowedEditor;
-    packetStream >> isAllowedEditor;
-    setIsAllowedEditor((bool) isAllowedEditor);
+    // pull the permissions/right/privileges for this node out of the stream
+    NodePermissions newPermissions;
+    packetStream >> newPermissions;
+    setPermissions(newPermissions);
 
-    quint8 thisNodeCanRez;
-    packetStream >> thisNodeCanRez;
-    setThisNodeCanRez((bool) thisNodeCanRez);
-    
     // pull each node in the packet
     while (packetStream.device()->pos() < message->getSize()) {
         parseNodeFromPacketStream(packetStream);
@@ -577,10 +573,9 @@ void NodeList::parseNodeFromPacketStream(QDataStream& packetStream) {
     qint8 nodeType;
     QUuid nodeUUID, connectionUUID;
     HifiSockAddr nodePublicSocket, nodeLocalSocket;
-    bool isAllowedEditor;
-    bool canRez;
+    NodePermissions permissions;
 
-    packetStream >> nodeType >> nodeUUID >> nodePublicSocket >> nodeLocalSocket >> isAllowedEditor >> canRez;
+    packetStream >> nodeType >> nodeUUID >> nodePublicSocket >> nodeLocalSocket >> permissions;
 
     // if the public socket address is 0 then it's reachable at the same IP
     // as the domain server
@@ -591,8 +586,7 @@ void NodeList::parseNodeFromPacketStream(QDataStream& packetStream) {
     packetStream >> connectionUUID;
 
     SharedNodePointer node = addOrUpdateNode(nodeUUID, nodeType, nodePublicSocket,
-                                             nodeLocalSocket, isAllowedEditor, canRez,
-                                             connectionUUID);
+                                             nodeLocalSocket, permissions, connectionUUID);
 }
 
 void NodeList::sendAssignment(Assignment& assignment) {
diff --git a/libraries/networking/src/NodeList.h b/libraries/networking/src/NodeList.h
index b269554e77..3fbc86c736 100644
--- a/libraries/networking/src/NodeList.h
+++ b/libraries/networking/src/NodeList.h
@@ -68,9 +68,6 @@ public:
     
     void setIsShuttingDown(bool isShuttingDown) { _isShuttingDown = isShuttingDown; }
 
-    /// downgrades the DomainConnnectRequest PacketVersion to attempt to probe for older domain servers
-    void downgradeDomainServerCheckInVersion() { _domainConnectRequestVersion--; }
-
 public slots:
     void reset();
     void sendDomainServerCheckIn();
@@ -88,8 +85,9 @@ public slots:
 
     void processICEPingPacket(QSharedPointer<ReceivedMessage> message);
 
-    void resetDomainServerCheckInVersion() 
-            { _domainConnectRequestVersion = versionForPacketType(PacketType::DomainConnectRequest); }
+#if (PR_BUILD || DEV_BUILD)
+    void toggleSendNewerDSConnectVersion(bool shouldSendNewerVersion) { _shouldSendNewerVersion = shouldSendNewerVersion; }
+#endif
 
 signals:
     void limitOfSilentDomainCheckInsReached();
@@ -105,6 +103,7 @@ private slots:
     void pingPunchForDomainServer();
     
     void sendKeepAlivePings();
+    
 private:
     NodeList() : LimitedNodeList(0, 0) { assert(false); } // Not implemented, needed for DependencyManager templates compile
     NodeList(char ownerType, unsigned short socketListenPort = 0, unsigned short dtlsListenPort = 0);
@@ -130,7 +129,9 @@ private:
     bool _isShuttingDown { false };
     QTimer _keepAlivePingTimer;
 
-    PacketVersion _domainConnectRequestVersion = versionForPacketType(PacketType::DomainConnectRequest);
+#if (PR_BUILD || DEV_BUILD)
+    bool _shouldSendNewerVersion { false };
+#endif
 };
 
 #endif // hifi_NodeList_h
diff --git a/libraries/networking/src/NodePermissions.cpp b/libraries/networking/src/NodePermissions.cpp
new file mode 100644
index 0000000000..fb74ccdc94
--- /dev/null
+++ b/libraries/networking/src/NodePermissions.cpp
@@ -0,0 +1,97 @@
+//
+//  NodePermissions.cpp
+//  libraries/networking/src/
+//
+//  Created by Seth Alves on 2016-6-1.
+//  Copyright 2016 High Fidelity, Inc.
+//
+//  Distributed under the Apache License, Version 2.0.
+//  See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
+//
+
+#include <QDataStream>
+#include <QtCore/QDebug>
+#include "NodePermissions.h"
+
+QString NodePermissions::standardNameLocalhost = QString("localhost");
+QString NodePermissions::standardNameLoggedIn = QString("logged-in");
+QString NodePermissions::standardNameAnonymous = QString("anonymous");
+
+QStringList NodePermissions::standardNames = QList<QString>()
+    << NodePermissions::standardNameLocalhost
+    << NodePermissions::standardNameLoggedIn
+    << NodePermissions::standardNameAnonymous;
+
+NodePermissions& NodePermissions::operator|=(const NodePermissions& rhs) {
+    this->canConnectToDomain |= rhs.canConnectToDomain;
+    this->canAdjustLocks |= rhs.canAdjustLocks;
+    this->canRezPermanentEntities |= rhs.canRezPermanentEntities;
+    this->canRezTemporaryEntities |= rhs.canRezTemporaryEntities;
+    this->canWriteToAssetServer |= rhs.canWriteToAssetServer;
+    this->canConnectPastMaxCapacity |= rhs.canConnectPastMaxCapacity;
+    return *this;
+}
+NodePermissions& NodePermissions::operator|=(const NodePermissionsPointer& rhs) {
+    if (rhs) {
+        *this |= *rhs.get();
+    }
+    return *this;
+}
+NodePermissionsPointer& operator|=(NodePermissionsPointer& lhs, const NodePermissionsPointer& rhs) {
+    if (lhs && rhs) {
+        *lhs.get() |= rhs;
+    }
+    return lhs;
+}
+
+
+QDataStream& operator<<(QDataStream& out, const NodePermissions& perms) {
+    out << perms.canConnectToDomain;
+    out << perms.canAdjustLocks;
+    out << perms.canRezPermanentEntities;
+    out << perms.canRezTemporaryEntities;
+    out << perms.canWriteToAssetServer;
+    out << perms.canConnectPastMaxCapacity;
+    return out;
+}
+
+QDataStream& operator>>(QDataStream& in, NodePermissions& perms) {
+    in >> perms.canConnectToDomain;
+    in >> perms.canAdjustLocks;
+    in >> perms.canRezPermanentEntities;
+    in >> perms.canRezTemporaryEntities;
+    in >> perms.canWriteToAssetServer;
+    in >> perms.canConnectPastMaxCapacity;
+    return in;
+}
+
+QDebug operator<<(QDebug debug, const NodePermissions& perms) {
+    debug.nospace() << "[permissions: " << perms.getID() << " --";
+    if (perms.canConnectToDomain) {
+        debug << " connect";
+    }
+    if (perms.canAdjustLocks) {
+        debug << " locks";
+    }
+    if (perms.canRezPermanentEntities) {
+        debug << " rez";
+    }
+    if (perms.canRezTemporaryEntities) {
+        debug << " rez-tmp";
+    }
+    if (perms.canWriteToAssetServer) {
+        debug << " asset-server";
+    }
+    if (perms.canConnectPastMaxCapacity) {
+        debug << " ignore-max-cap";
+    }
+    debug.nospace() << "]";
+    return debug.nospace();
+}
+QDebug operator<<(QDebug debug, const NodePermissionsPointer& perms) {
+    if (perms) {
+        return operator<<(debug, *perms.get());
+    }
+    debug.nospace() << "[permissions: null]";
+    return debug.nospace();
+}
diff --git a/libraries/networking/src/NodePermissions.h b/libraries/networking/src/NodePermissions.h
new file mode 100644
index 0000000000..c153878a7e
--- /dev/null
+++ b/libraries/networking/src/NodePermissions.h
@@ -0,0 +1,97 @@
+//
+//  NodePermissions.h
+//  libraries/networking/src/
+//
+//  Created by Seth Alves on 2016-6-1.
+//  Copyright 2016 High Fidelity, Inc.
+//
+//  Distributed under the Apache License, Version 2.0.
+//  See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
+//
+
+#ifndef hifi_NodePermissions_h
+#define hifi_NodePermissions_h
+
+#include <memory>
+#include <QString>
+#include <QMap>
+#include <QVariant>
+#include <QUuid>
+
+class NodePermissions;
+using NodePermissionsPointer = std::shared_ptr<NodePermissions>;
+
+class NodePermissions {
+public:
+    NodePermissions() { _id = QUuid::createUuid().toString(); }
+    NodePermissions(const QString& name) { _id = name; }
+    NodePermissions(QMap<QString, QVariant> perms) {
+        _id = perms["permissions_id"].toString();
+        canConnectToDomain = perms["id_can_connect"].toBool();
+        canAdjustLocks = perms["id_can_adjust_locks"].toBool();
+        canRezPermanentEntities = perms["id_can_rez"].toBool();
+        canRezTemporaryEntities = perms["id_can_rez_tmp"].toBool();
+        canWriteToAssetServer = perms["id_can_write_to_asset_server"].toBool();
+        canConnectPastMaxCapacity = perms["id_can_connect_past_max_capacity"].toBool();
+    }
+
+    QString getID() const { return _id; }
+
+    // the _id member isn't authenticated and _username is.
+    void setUserName(QString userName) { _userName = userName; }
+    QString getUserName() { return _userName; }
+
+    bool isAssignment { false };
+
+    // these 3 names have special meaning.
+    static QString standardNameLocalhost;
+    static QString standardNameLoggedIn;
+    static QString standardNameAnonymous;
+    static QStringList standardNames;
+
+    // the initializations here should match the defaults in describe-settings.json
+    bool canConnectToDomain { true };
+    bool canAdjustLocks { false };
+    bool canRezPermanentEntities { false };
+    bool canRezTemporaryEntities { false };
+    bool canWriteToAssetServer { false };
+    bool canConnectPastMaxCapacity { false };
+
+    void setAll(bool value) {
+        canConnectToDomain = value;
+        canAdjustLocks = value;
+        canRezPermanentEntities = value;
+        canRezTemporaryEntities = value;
+        canWriteToAssetServer = value;
+        canConnectPastMaxCapacity = value;
+    }
+
+    QVariant toVariant() {
+        QMap<QString, QVariant> values;
+        values["permissions_id"] = _id;
+        values["id_can_connect"] = canConnectToDomain;
+        values["id_can_adjust_locks"] = canAdjustLocks;
+        values["id_can_rez"] = canRezPermanentEntities;
+        values["id_can_rez_tmp"] = canRezTemporaryEntities;
+        values["id_can_write_to_asset_server"] = canWriteToAssetServer;
+        values["id_can_connect_past_max_capacity"] = canConnectPastMaxCapacity;
+        return QVariant(values);
+    }
+
+    NodePermissions& operator|=(const NodePermissions& rhs);
+    NodePermissions& operator|=(const NodePermissionsPointer& rhs);
+    friend QDataStream& operator<<(QDataStream& out, const NodePermissions& perms);
+    friend QDataStream& operator>>(QDataStream& in, NodePermissions& perms);
+
+protected:
+    QString _id;
+    QString _userName;
+};
+
+const NodePermissions DEFAULT_AGENT_PERMISSIONS;
+
+QDebug operator<<(QDebug debug, const NodePermissions& perms);
+QDebug operator<<(QDebug debug, const NodePermissionsPointer& perms);
+NodePermissionsPointer& operator|=(NodePermissionsPointer& lhs, const NodePermissionsPointer& rhs);
+
+#endif // hifi_NodePermissions_h
diff --git a/libraries/networking/src/udt/PacketHeaders.cpp b/libraries/networking/src/udt/PacketHeaders.cpp
index db743f81e4..6ca50420f3 100644
--- a/libraries/networking/src/udt/PacketHeaders.cpp
+++ b/libraries/networking/src/udt/PacketHeaders.cpp
@@ -45,7 +45,7 @@ const QSet<PacketType> RELIABLE_PACKETS = QSet<PacketType>();
 PacketVersion versionForPacketType(PacketType packetType) {
     switch (packetType) {
         case PacketType::DomainList:
-            return 18;
+            return static_cast<PacketVersion>(DomainListVersion::PermissionsGrid);
         case PacketType::EntityAdd:
         case PacketType::EntityEdit:
         case PacketType::EntityData:
@@ -69,6 +69,9 @@ PacketVersion versionForPacketType(PacketType packetType) {
         case PacketType::DomainConnectRequest:
             return static_cast<PacketVersion>(DomainConnectRequestVersion::HasProtocolVersions);
 
+        case PacketType::DomainServerAddedNode:
+            return static_cast<PacketVersion>(DomainServerAddedNodeVersion::PermissionsGrid);
+
         default:
             return 17;
     }
diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h
index 320635379d..ae54450fee 100644
--- a/libraries/networking/src/udt/PacketHeaders.h
+++ b/libraries/networking/src/udt/PacketHeaders.h
@@ -199,4 +199,14 @@ enum class DomainConnectionDeniedVersion : PacketVersion {
     IncludesReasonCode
 };
 
+enum class DomainServerAddedNodeVersion : PacketVersion {
+    PrePermissionsGrid = 17,
+    PermissionsGrid
+};
+
+enum class DomainListVersion : PacketVersion {
+    PrePermissionsGrid = 18,
+    PermissionsGrid
+};
+
 #endif // hifi_PacketHeaders_h
diff --git a/libraries/octree/src/Octree.cpp b/libraries/octree/src/Octree.cpp
index 39be760944..475beef03c 100644
--- a/libraries/octree/src/Octree.cpp
+++ b/libraries/octree/src/Octree.cpp
@@ -1863,9 +1863,9 @@ bool Octree::readJSONFromStream(unsigned long streamLength, QDataStream& inputSt
     QJsonDocument asDocument = QJsonDocument::fromJson(jsonBuffer);
     QVariant asVariant = asDocument.toVariant();
     QVariantMap asMap = asVariant.toMap();
-    readFromMap(asMap);
+    bool success = readFromMap(asMap);
     delete[] rawData;
-    return true;
+    return success;
 }
 
 void Octree::writeToFile(const char* fileName, OctreeElementPointer element, QString persistAsFileType) {
diff --git a/libraries/shared/src/HifiConfigVariantMap.cpp b/libraries/shared/src/HifiConfigVariantMap.cpp
index b3920e70bc..5ae5ff740d 100644
--- a/libraries/shared/src/HifiConfigVariantMap.cpp
+++ b/libraries/shared/src/HifiConfigVariantMap.cpp
@@ -213,10 +213,12 @@ QVariant* valueForKeyPath(QVariantMap& variantMap, const QString& keyPath, bool
     if (shouldCreateIfMissing || variantMap.contains(firstKey)) {
         if (dotIndex == -1) {
             return &variantMap[firstKey];
-        } else if (variantMap[firstKey].canConvert(QMetaType::QVariantMap)) {
-            return valueForKeyPath(*static_cast<QVariantMap*>(variantMap[firstKey].data()), keyPath.mid(dotIndex + 1),
-                                   shouldCreateIfMissing);
         }
+        if (!variantMap[firstKey].canConvert(QMetaType::QVariantMap)) {
+            variantMap[firstKey] = QVariantMap();
+        }
+        return valueForKeyPath(*static_cast<QVariantMap*>(variantMap[firstKey].data()), keyPath.mid(dotIndex + 1),
+                               shouldCreateIfMissing);
     }
 
     return NULL;
diff --git a/scripts/system/controllers/handControllerGrab.js b/scripts/system/controllers/handControllerGrab.js
index 928f11dd36..25cd100991 100644
--- a/scripts/system/controllers/handControllerGrab.js
+++ b/scripts/system/controllers/handControllerGrab.js
@@ -894,30 +894,24 @@ function MyController(hand) {
             var grabData = getEntityCustomData(GRABBABLE_DATA_KEY, entities[i], undefined);
             var grabProps = Entities.getEntityProperties(entities[i], GRABBABLE_PROPERTIES);
             if (grabData) {
-
-                var hotspotPos = grabProps.position;
-
                 // does this entity have an attach point?
                 var wearableData = getEntityCustomData("wearable", entities[i], undefined);
-                if (wearableData) {
+                if (wearableData && wearableData.joints) {
                     var handJointName = this.hand === RIGHT_HAND ? "RightHand" : "LeftHand";
-                    if (wearableData[handJointName]) {
-                        // draw the hotspot around the attach point.
-                        hotspotPos = wearableData[handJointName][0];
+                    if (wearableData.joints[handJointName]) {
+                        // draw the hotspot
+                        this.equipHotspotOverlays.push(Overlays.addOverlay("sphere", {
+                            position: grabProps.position,
+                            size: 0.2,
+                            color: { red: 90, green: 255, blue: 90 },
+                            alpha: 0.7,
+                            solid: true,
+                            visible: true,
+                            ignoreRayIntersection: false,
+                            drawInFront: false
+                        }));
                     }
                 }
-
-                // draw a hotspot!
-                this.equipHotspotOverlays.push(Overlays.addOverlay("sphere", {
-                    position: hotspotPos,
-                    size: 0.2,
-                    color: { red: 90, green: 255, blue: 90 },
-                    alpha: 0.7,
-                    solid: true,
-                    visible: true,
-                    ignoreRayIntersection: false,
-                    drawInFront: false
-                }));
             }
         }
     };
diff --git a/scripts/system/edit.js b/scripts/system/edit.js
index 38d596f83e..0c4f4f3c5c 100644
--- a/scripts/system/edit.js
+++ b/scripts/system/edit.js
@@ -1222,7 +1222,7 @@ function handeMenuEvent(menuItem) {
             Window.alert("No entities have been selected.");
         } else {
             var filename = "entities__" + Window.location.hostname + ".svo.json";
-            filename = Window.save("Select where to save", filename, "*.json")
+            filename = Window.save("Select Where to Save", filename, "*.json")
             if (filename) {
                 var success = Clipboard.exportEntities(filename, selectionManager.selections);
                 if (!success) {
@@ -1234,7 +1234,7 @@ function handeMenuEvent(menuItem) {
 
         var importURL = null;
         if (menuItem == "Import Entities") {
-            var fullPath = Window.browse("Select models to import", "", "*.json");
+            var fullPath = Window.browse("Select Model to Import", "", "*.json");
             if (fullPath) {
                 importURL = "file:///" + fullPath;
             }