diff --git a/domain-server/resources/describe-settings.json b/domain-server/resources/describe-settings.json index e1334ee46f..c888fa301b 100644 --- a/domain-server/resources/describe-settings.json +++ b/domain-server/resources/describe-settings.json @@ -1,5 +1,5 @@ { - "version": 1.5, + "version": 1.7, "settings": [ { "name": "metaverse", @@ -384,18 +384,18 @@ "name": "standard_permissions", "type": "table", "label": "Domain-Wide User Permissions", - "help": "Indicate which users or groups can have which domain-wide permissions.", + "help": "Indicate which types of users can have which domain-wide permissions.", "caption": "Standard Permissions", "can_add_new_rows": false, "groups": [ { - "label": "User / Group", + "label": "Type of User", "span": 1 }, { "label": "Permissions ?", - "span": 6 + "span": 7 } ], @@ -409,7 +409,7 @@ "label": "Connect", "type": "checkbox", "editable": true, - "default": true + "default": false }, { "name": "id_can_adjust_locks", @@ -445,6 +445,13 @@ "type": "checkbox", "editable": true, "default": false + }, + { + "name": "id_can_kick", + "label": "Kick Users", + "type": "checkbox", + "editable": true, + "default": false } ], @@ -452,33 +459,62 @@ "non-deletable-row-values": ["localhost", "anonymous", "logged-in"] }, { - "name": "permissions", + "name": "group_permissions", "type": "table", - "caption": "Permissions for Specific Users", - "can_add_new_rows": true, + "caption": "Permissions for Users in Groups", + "categorize_by_key": "permissions_id", + "can_add_new_categories": true, + "can_add_new_rows": false, + "new_category_placeholder": "Add Group", + "new_category_message": "Save and reload to see ranks", "groups": [ { - "label": "User / Group", + "label": "Rank", "span": 1 }, { - "label": "Permissions ?", - "span": 6 + "label": "Permissions ?", + "span": 7 } ], "columns": [ { "name": "permissions_id", - "label": "" + "label": "Group Name", + "readonly": true, + "hidden": true + }, + { + "name": "rank_id", + "label": "Rank ID", + "readonly": true, + "hidden": true + }, + { + "name": "rank_order", + "label": "Rank Order", + "readonly": true, + "hidden": true + }, + { + "name": "rank_name", + "label": "", + "readonly": true + }, + { + "name": "group_id", + "label": "Group ID", + "readonly": true, + "hidden": true }, { "name": "id_can_connect", "label": "Connect", "type": "checkbox", "editable": true, - "default": true + "default": false }, { "name": "id_can_adjust_locks", @@ -514,6 +550,257 @@ "type": "checkbox", "editable": true, "default": false + }, + { + "name": "id_can_kick", + "label": "Kick Users", + "type": "checkbox", + "editable": true, + "default": false + } + ] + }, + { + "name": "group_forbiddens", + "type": "table", + "caption": "Permissions Denied to Users in Groups", + "categorize_by_key": "permissions_id", + "can_add_new_categories": true, + "can_add_new_rows": false, + "new_category_placeholder": "Add Blacklist Group", + "new_category_message": "Save and reload to see ranks", + + "groups": [ + { + "label": "Rank", + "span": 1 + }, + { + "label": "Permissions ?", + "span": 7 + } + ], + + "columns": [ + { + "name": "permissions_id", + "label": "Group Name", + "hidden": true + }, + { + "name": "rank_id", + "label": "Rank ID", + "hidden": true + }, + { + "name": "rank_order", + "label": "Rank Order", + "hidden": true + }, + { + "name": "rank_name", + "label": "", + "readonly": true + }, + { + "name": "group_id", + "label": "Group ID", + "readonly": true, + "hidden": true + }, + { + "name": "id_can_connect", + "label": "Connect", + "type": "checkbox", + "editable": true, + "default": false + }, + { + "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 + }, + { + "name": "id_can_kick", + "label": "Kick Users", + "type": "checkbox", + "editable": true, + "default": false + } + ] + }, + { + "name": "ip_permissions", + "type": "table", + "caption": "Permissions for Users from IP Addresses", + "can_add_new_rows": true, + "groups": [ + { + "label": "IP Address", + "span": 1 + }, + { + "label": "Permissions ?", + "span": 7 + } + ], + + "columns": [ + { + "name": "permissions_id", + "label": "" + }, + { + "name": "id_can_connect", + "label": "Connect", + "type": "checkbox", + "editable": true, + "default": false + }, + { + "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 + }, + { + "name": "id_can_kick", + "label": "Kick Users", + "type": "checkbox", + "editable": true, + "default": false + } + ] + }, + { + "name": "permissions", + "type": "table", + "caption": "Permissions for Specific Users", + "can_add_new_rows": true, + + "groups": [ + { + "label": "User", + "span": 1 + }, + { + "label": "Permissions ?", + "span": 7 + } + ], + + "columns": [ + { + "name": "permissions_id", + "label": "" + }, + { + "name": "id_can_connect", + "label": "Connect", + "type": "checkbox", + "editable": true, + "default": false + }, + { + "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 + }, + { + "name": "id_can_kick", + "label": "Kick Users", + "type": "checkbox", + "editable": true, + "default": false } ] } diff --git a/domain-server/resources/web/css/style.css b/domain-server/resources/web/css/style.css index 2862feed87..ef967a47bf 100644 --- a/domain-server/resources/web/css/style.css +++ b/domain-server/resources/web/css/style.css @@ -3,6 +3,10 @@ body { padding-bottom: 30px; } +[hidden] { + display: none !important; +} + .table-lead .lead-line { background-color: black; } @@ -20,7 +24,9 @@ body { top: 40px; } -.table .value-row td, .table .inputs td { +.table .value-row td, +.table .value-category td, +.table .inputs td { vertical-align: middle; } @@ -31,6 +37,31 @@ body { margin-right: auto; } +.value-category:not(.inputs) { + font-weight: bold; + background: #f5f5f5; +} + +.table .value-category [message]::after { + content: attr(message); + font-style: italic; + font-weight: normal; +} + +.table .value-row.contracted, +.table .inputs.contracted { + display: none; +} + +.toggle-category { + cursor: pointer; +} + +.toggle-category-icon { + padding: 4px; + margin-right: 8px; +} + .glyphicon-remove { font-size: 24px; } @@ -44,15 +75,6 @@ span.port { color: #666666; } -.locked { - color: #428bca; -} - -.locked-table { - cursor: not-allowed; - background-color: #eee; -} - .advanced-setting { display: none; } @@ -133,7 +155,7 @@ table .headers + .headers td { color: #222; } -table[name="security.standard_permissions"] .headers td + td, table[name="security.permissions"] .headers td + td { +#security table .headers td + td { text-align: center; } diff --git a/domain-server/resources/web/settings/index.shtml b/domain-server/resources/web/settings/index.shtml index 3f969ef913..4c937d6139 100644 --- a/domain-server/resources/web/settings/index.shtml +++ b/domain-server/resources/web/settings/index.shtml @@ -57,15 +57,13 @@
<% _.each(split_settings[0], function(setting) { %> <% keypath = isGrouped ? group.name + "." + setting.name : setting.name %> - <%= getFormGroup(keypath, setting, values, false, - (_.has(locked, group.name) && _.has(locked[group.name], setting.name))) %> + <%= getFormGroup(keypath, setting, values, false) %> <% }); %> <% if (!_.isEmpty(split_settings[1])) { %> <% $("#advanced-toggle-button").show() %> <% _.each(split_settings[1], function(setting) { %> <% keypath = isGrouped ? group.name + "." + setting.name : setting.name %> - <%= getFormGroup(keypath, setting, values, true, - (_.has(locked, group.name) && _.has(locked[group.name], setting.name))) %> + <%= getFormGroup(keypath, setting, values, true) %> <% }); %> <% }%>
diff --git a/domain-server/resources/web/settings/js/settings.js b/domain-server/resources/web/settings/js/settings.js index 4f153d6190..42fcb05d2e 100644 --- a/domain-server/resources/web/settings/js/settings.js +++ b/domain-server/resources/web/settings/js/settings.js @@ -5,10 +5,20 @@ var Settings = { TRIGGER_CHANGE_CLASS: 'trigger-change', DATA_ROW_CLASS: 'value-row', DATA_COL_CLASS: 'value-col', + DATA_CATEGORY_CLASS: 'value-category', ADD_ROW_BUTTON_CLASS: 'add-row', ADD_ROW_SPAN_CLASSES: 'glyphicon glyphicon-plus add-row', DEL_ROW_BUTTON_CLASS: 'del-row', DEL_ROW_SPAN_CLASSES: 'glyphicon glyphicon-remove del-row', + ADD_CATEGORY_BUTTON_CLASS: 'add-category', + ADD_CATEGORY_SPAN_CLASSES: 'glyphicon glyphicon-plus add-category', + TOGGLE_CATEGORY_COLUMN_CLASS: 'toggle-category', + TOGGLE_CATEGORY_SPAN_CLASS: 'toggle-category-icon', + TOGGLE_CATEGORY_SPAN_CLASSES: 'glyphicon toggle-category-icon', + TOGGLE_CATEGORY_EXPANDED_CLASS: 'glyphicon-triangle-bottom', + TOGGLE_CATEGORY_CONTRACTED_CLASS: 'glyphicon-triangle-right', + DEL_CATEGORY_BUTTON_CLASS: 'del-category', + DEL_CATEGORY_SPAN_CLASSES: 'glyphicon glyphicon-remove del-category', MOVE_UP_BUTTON_CLASS: 'move-up', MOVE_UP_SPAN_CLASSES: 'glyphicon glyphicon-chevron-up move-up', MOVE_DOWN_BUTTON_CLASS: 'move-down', @@ -31,11 +41,11 @@ var Settings = { }; var viewHelpers = { - getFormGroup: function(keypath, setting, values, isAdvanced, isLocked) { + getFormGroup: function(keypath, setting, values, isAdvanced) { form_group = "
"; setting_value = _(values).valueForKeyPath(keypath); - if (typeof setting_value == 'undefined' || setting_value === null) { + if (_.isUndefined(setting_value) || _.isNull(setting_value)) { if (_.has(setting, 'default')) { setting_value = setting.default; } else { @@ -44,16 +54,13 @@ var viewHelpers = { } label_class = 'control-label'; - if (isLocked) { - label_class += ' locked'; - } function common_attrs(extra_classes) { - extra_classes = (typeof extra_classes !== 'undefined' ? extra_classes : ""); + extra_classes = (!_.isUndefined(extra_classes) ? extra_classes : ""); return " class='" + (setting.type !== 'checkbox' ? 'form-control' : '') + " " + Settings.TRIGGER_CHANGE_CLASS + " " + extra_classes + "' data-short-name='" + setting.name + "' name='" + keypath + "' " - + "id='" + (typeof setting.html_id !== 'undefined' ? setting.html_id : keypath) + "'"; + + "id='" + (!_.isUndefined(setting.html_id) ? setting.html_id : keypath) + "'"; } if (setting.type === 'checkbox') { @@ -61,9 +68,8 @@ var viewHelpers = { form_group += "" } - form_group += "
" - form_group += "" + form_group += "
" + form_group += "" if (setting.help) { form_group += "" + setting.help + ""; @@ -78,7 +84,7 @@ var viewHelpers = { } if (input_type === 'table') { - form_group += makeTable(setting, keypath, setting_value, isLocked) + form_group += makeTable(setting, keypath, setting_value) } else { if (input_type === 'select') { form_group += "" + "' value='" + setting_value + "'/>" } form_group += "" + setting.help + "" @@ -162,19 +166,31 @@ $(document).ready(function(){ }); $('#' + Settings.FORM_ID).on('click', '.' + Settings.ADD_ROW_BUTTON_CLASS, function(){ - addTableRow(this); + addTableRow($(this).closest('tr')); }); $('#' + Settings.FORM_ID).on('click', '.' + Settings.DEL_ROW_BUTTON_CLASS, function(){ - deleteTableRow(this); + deleteTableRow($(this).closest('tr')); + }); + + $('#' + Settings.FORM_ID).on('click', '.' + Settings.ADD_CATEGORY_BUTTON_CLASS, function(){ + addTableCategory($(this).closest('tr')); + }); + + $('#' + Settings.FORM_ID).on('click', '.' + Settings.DEL_CATEGORY_BUTTON_CLASS, function(){ + deleteTableCategory($(this).closest('tr')); + }); + + $('#' + Settings.FORM_ID).on('click', '.' + Settings.TOGGLE_CATEGORY_COLUMN_CLASS, function(){ + toggleTableCategory($(this).closest('tr')); }); $('#' + Settings.FORM_ID).on('click', '.' + Settings.MOVE_UP_BUTTON_CLASS, function(){ - moveTableRow(this, true); + moveTableRow($(this).closest('tr'), true); }); $('#' + Settings.FORM_ID).on('click', '.' + Settings.MOVE_DOWN_BUTTON_CLASS, function(){ - moveTableRow(this, false); + moveTableRow($(this).closest('tr'), false); }); $('#' + Settings.FORM_ID).on('keyup', function(e){ @@ -196,10 +212,11 @@ $(document).ready(function(){ } if (sibling.hasClass(Settings.ADD_DEL_BUTTONS_CLASS)) { - sibling.find('.' + Settings.ADD_ROW_BUTTON_CLASS).click() + sibling.find('.' + Settings.ADD_ROW_BUTTON_CLASS).click(); + sibling.find("." + Settings.ADD_CATEGORY_BUTTON_CLASS).click(); // set focus to the first input in the new row - $target.closest('table').find('tr.inputs input:first').focus() + $target.closest('table').find('tr.inputs input:first').focus(); } } @@ -436,10 +453,8 @@ function setupHFAccountButton() { $("[data-keypath='metaverse.automatic_networking']").hide(); } - var tokenLocked = _(Settings.data).valueForKeyPath("locked.metaverse.access_token"); - // use the existing getFormGroup helper to ask for a button - var buttonGroup = viewHelpers.getFormGroup('', buttonSetting, Settings.data.values, false, tokenLocked); + var buttonGroup = viewHelpers.getFormGroup('', buttonSetting, Settings.data.values, false); // add the button group to the top of the metaverse panel $('#metaverse .panel-body').prepend(buttonGroup); @@ -629,7 +644,7 @@ function setupPlacesTable() { label: 'Places', html_id: Settings.PLACES_TABLE_ID, help: "The following places currently point to this domain.
To point places to this domain, " - + " go to the My Places " + + " go to the My Places " + "page in your High Fidelity Metaverse account.", read_only: true, columns: [ @@ -650,7 +665,7 @@ function setupPlacesTable() { } // get a table for the places - var placesTableGroup = viewHelpers.getFormGroup('', placesTableSetting, Settings.data.values, false, false); + var placesTableGroup = viewHelpers.getFormGroup('', placesTableSetting, Settings.data.values, false); // append the places table in the right place $('#places_paths .panel-body').prepend(placesTableGroup); @@ -771,7 +786,7 @@ function chooseFromHighFidelityDomains(clickedButton) { modal_buttons["success"] = { label: 'Create new domain', callback: function() { - window.open("https://metaverse.highfidelity.com/user/domains", '_blank'); + window.open(Settings.METAVERSE_URL + "/user/domains", '_blank'); } } modal_body = "

You do not have any domains in your High Fidelity account." + @@ -850,10 +865,8 @@ function reloadSettings(callback) { Settings.data = data; Settings.initialValues = form2js('settings-form', ".", false, cleanupFormValues, true); - if (!_.has(data["locked"], "metaverse") && !_.has(data["locked"]["metaverse"], "id")) { - // append the domain selection modal, as long as it's not locked - appendDomainIDButtons(); - } + // append the domain selection modal + appendDomainIDButtons(); // call our method to setup the HF account button setupHFAccountButton(); @@ -866,12 +879,6 @@ function reloadSettings(callback) { $('[data-toggle="tooltip"]').tooltip(); - // add tooltip to locked settings - $('label.locked').tooltip({ - placement: 'right', - title: 'This setting is in the master config file and cannot be changed' - }); - // call the callback now that settings are loaded callback(true); }).fail(function() { @@ -920,9 +927,10 @@ $('body').on('click', '.save-button', function(e){ return false; }); -function makeTable(setting, keypath, setting_value, isLocked) { +function makeTable(setting, keypath, setting_value) { var isArray = !_.has(setting, 'key'); - var isHash = !isArray; + var categoryKey = setting.categorize_by_key; + var isCategorized = !!categoryKey && isArray; if (!isArray && setting.can_order) { setting.can_order = false; @@ -937,9 +945,10 @@ function makeTable(setting, keypath, setting_value, isLocked) { var nonDeletableRowKey = setting["non-deletable-row-key"]; var nonDeletableRowValues = setting["non-deletable-row-values"]; - html += ""; + html += "
"; if (setting.caption) { html += "" @@ -951,7 +960,7 @@ function makeTable(setting, keypath, setting_value, isLocked) { _.each(setting.groups, function (group) { html += "" }) - if (!isLocked && !setting.read_only) { + if (!setting.read_only) { if (setting.can_order) { html += ""; @@ -972,24 +981,43 @@ function makeTable(setting, keypath, setting_value, isLocked) { html += "" // Key } + var numVisibleColumns = 0; _.each(setting.columns, function(col) { - html += "" // Data + if (!col.hidden) numVisibleColumns++; + html += "" // Data }) - if (!isLocked && !setting.read_only) { + if (!setting.read_only) { if (setting.can_order) { + numVisibleColumns++; html += ""; } - html += "" + numVisibleColumns++; + html += ""; } // populate rows in the table from existing values var row_num = 1; if (keypath.length > 0 && _.size(setting_value) > 0) { + var rowIsObject = setting.columns.length > 1; + _.each(setting_value, function(row, rowIndexOrName) { - html += "" + var categoryPair = {}; + var categoryValue = ""; + if (isCategorized) { + categoryValue = rowIsObject ? row[categoryKey] : row; + categoryPair[categoryKey] = categoryValue; + if (_.findIndex(setting_value, categoryPair) === rowIndexOrName) { + html += makeTableCategoryHeader(categoryKey, categoryValue, numVisibleColumns, setting.can_add_new_categories, ""); + } + } + + html += ""; if (setting.numbered === true) { html += "" @@ -1003,8 +1031,8 @@ function makeTable(setting, keypath, setting_value, isLocked) { _.each(setting.columns, function(col) { + var colValue, colName; if (isArray) { - rowIsObject = setting.columns.length > 1; colValue = rowIsObject ? row[col.name] : row; colName = keypath + "[" + rowIndexOrName + "]" + (rowIsObject ? "." + col.name : ""); } else { @@ -1016,22 +1044,30 @@ function makeTable(setting, keypath, setting_value, isLocked) { || (nonDeletableRowKey === col.name && nonDeletableRowValues.indexOf(colValue) !== -1); if (isArray && col.type === "checkbox" && col.editable) { - html += ""; + html += + ""; } else if (isArray && col.type === "time" && col.editable) { - html += ""; + html += + ""; } else { // Use a hidden input so that the values are posted. - html += ""; + html += + ""; } - }) + }); - if (!isLocked && !setting.read_only) { + if (!setting.read_only) { if (setting.can_order) { html += "" + if (isCategorized && setting.can_add_new_rows && _.findLastIndex(setting_value, categoryPair) === rowIndexOrName) { + html += makeTableInputs(setting, categoryPair, categoryValue); + } + row_num++ }); } // populate inputs in the table for new values - if (!isLocked && !setting.read_only && setting.can_add_new_rows) { - html += makeTableInputs(setting) + if (!setting.read_only) { + if (setting.can_add_new_categories) { + html += makeTableCategoryInput(setting, numVisibleColumns); + } + if (setting.can_add_new_rows || setting.can_add_new_categories) { + html += makeTableInputs(setting, {}, ""); + } } html += "
" + setting.caption + "
" + group.label + "" + setting.key.label + "" + col.label + "" + col.label + "
" + row_num + "" - + "" + + "" + + "" - + "" + + "" + + "" - + colValue + "" + + colValue + + "" + + "" @@ -1047,24 +1083,53 @@ function makeTable(setting, keypath, setting_value, isLocked) { html += "
" return html; } -function makeTableInputs(setting) { - var html = "" +function makeTableCategoryHeader(categoryKey, categoryValue, numVisibleColumns, canRemove, message) { + var html = + "" + + "" + + "" + + "" + categoryValue + "" + + "" + + ((canRemove) ? ( + "" + + "" + + "" + ) : ( + "" + )) + + ""; + return html; +} + +function makeTableInputs(setting, initialValues, categoryValue) { + var html = ""; if (setting.numbered === true) { - html += "" + html += ""; } if (setting.key) { @@ -1074,15 +1139,21 @@ function makeTableInputs(setting) { } _.each(setting.columns, function(col) { + var defaultValue = _.has(initialValues, col.name) ? initialValues[col.name] : col.default; if (col.type === "checkbox") { - html += "" - + ""; + html += + "" + + "" + + ""; } else { - html += "\ - \ - " + html += + "" + + "" + + ""; } }) @@ -1090,12 +1161,30 @@ function makeTableInputs(setting) { html += "" } html += "" + "'>" html += "" return html } +function makeTableCategoryInput(setting, numVisibleColumns) { + var canAddRows = setting.can_add_new_rows; + var categoryKey = setting.categorize_by_key; + var placeholder = setting.new_category_placeholder || ""; + var message = setting.new_category_message || ""; + var html = + "" + + "" + + "" + + "" + + "" + + "" + + "" + + ""; + return html; +} + function badgeSidebarForDifferences(changedElement) { // figure out which group this input is in var panelParentID = changedElement.closest('.panel').attr('id'); @@ -1134,13 +1223,12 @@ function badgeSidebarForDifferences(changedElement) { $("a[href='#" + panelParentID + "'] .badge").html(badgeValue); } -function addTableRow(add_glyphicon) { - var row = $(add_glyphicon).closest('tr') +function addTableRow(row) { + var table = row.parents('table'); + var isArray = table.data('setting-type') === 'array'; + var keepField = row.data("keep-field"); - var table = row.parents('table') - var isArray = table.data('setting-type') === 'array' - - var columns = row.parent().children('.' + Settings.DATA_ROW_CLASS) + var columns = row.parent().children('.' + Settings.DATA_ROW_CLASS); if (!isArray) { // Check key spaces @@ -1257,10 +1345,12 @@ function addTableRow(add_glyphicon) { } else { console.log("Unknown table element") } - }) + }); - input_clone.find('input').each(function(){ - $(this).val($(this).attr('data-default')); + input_clone.children('td').each(function () { + if ($(this).attr("name") !== keepField) { + $(this).find("input").val($(this).attr('data-default')); + } }); if (isArray) { @@ -1272,44 +1362,132 @@ function addTableRow(add_glyphicon) { badgeSidebarForDifferences($(table)) - row.parent().append(input_clone) + row.after(input_clone) } -function deleteTableRow(delete_glyphicon) { - var row = $(delete_glyphicon).closest('tr') +function deleteTableRow($row) { + var $table = $row.closest('table'); + var categoryName = $row.data("category"); + var isArray = $table.data('setting-type') === 'array'; - var table = $(row).closest('table') - var isArray = table.data('setting-type') === 'array' - - row.empty(); + $row.empty(); if (!isArray) { - row.html(""); + $row.html(""); } else { - if (table.find('.' + Settings.DATA_ROW_CLASS).length > 1) { - updateDataChangedForSiblingRows(row) + if ($table.find('.' + Settings.DATA_ROW_CLASS + "[data-category='" + categoryName + "']").length <= 1) { + // This is the last row of the category, so delete the header + $table.find('.' + Settings.DATA_CATEGORY_CLASS + "[data-category='" + categoryName + "']").remove(); + } + + if ($table.find('.' + Settings.DATA_ROW_CLASS).length > 1) { + updateDataChangedForSiblingRows($row); // this isn't the last row - we can just remove it - row.remove() + $row.remove(); } else { // this is the last row, we can't remove it completely since we need to post an empty array - - row.removeClass(Settings.DATA_ROW_CLASS).removeClass(Settings.NEW_ROW_CLASS) - row.addClass('empty-array-row') - - row.html(""); + $row + .removeClass(Settings.DATA_ROW_CLASS) + .removeClass(Settings.NEW_ROW_CLASS) + .removeAttr("data-category") + .addClass('empty-array-row') + .html(""); } } // we need to fire a change event on one of the remaining inputs so that the sidebar badge is updated - badgeSidebarForDifferences($(table)) + badgeSidebarForDifferences($table); } -function moveTableRow(move_glyphicon, move_up) { - var row = $(move_glyphicon).closest('tr') +function addTableCategory($categoryInputRow) { + var $input = $categoryInputRow.find("input").first(); + var categoryValue = $input.prop("value"); + if (!categoryValue || $categoryInputRow.closest("table").find("tr[data-category='" + categoryValue + "']").length !== 0) { + $categoryInputRow.addClass("has-warning"); + setTimeout(function () { + $categoryInputRow.removeClass("has-warning"); + }, 400); + + return; + } + + var $rowInput = $categoryInputRow.next(".inputs").clone(); + if (!$rowInput) { + console.error("Error cloning inputs"); + } + + var canAddRows = $categoryInputRow.data("can-add-rows"); + var message = $categoryInputRow.data("message"); + var categoryKey = $categoryInputRow.data("key"); + var width = 0; + $categoryInputRow + .children("td") + .each(function () { + width += $(this).prop("colSpan") || 1; + }); + + $input + .prop("value", "") + .focus(); + + $rowInput.find("td[name='" + categoryKey + "'] > input").first() + .prop("value", categoryValue); + $rowInput + .attr("data-category", categoryValue) + .addClass(Settings.NEW_ROW_CLASS); + + var $newCategoryRow = $(makeTableCategoryHeader(categoryKey, categoryValue, width, true, " - " + message)); + $newCategoryRow.addClass(Settings.NEW_ROW_CLASS); + + $categoryInputRow + .before($newCategoryRow) + .before($rowInput); + + if (canAddRows) { + $rowInput.removeAttr("hidden"); + } else { + addTableRow($rowInput); + } +} + +function deleteTableCategory($categoryHeaderRow) { + var categoryName = $categoryHeaderRow.data("category"); + + $categoryHeaderRow + .closest("table") + .find("tr[data-category='" + categoryName + "']") + .each(function () { + if ($(this).hasClass(Settings.DATA_ROW_CLASS)) { + deleteTableRow($(this)); + } else { + $(this).remove(); + } + }); +} + +function toggleTableCategory($categoryHeaderRow) { + var $icon = $categoryHeaderRow.find("." + Settings.TOGGLE_CATEGORY_SPAN_CLASS).first(); + var categoryName = $categoryHeaderRow.data("category"); + var wasExpanded = $icon.hasClass(Settings.TOGGLE_CATEGORY_EXPANDED_CLASS); + if (wasExpanded) { + $icon + .addClass(Settings.TOGGLE_CATEGORY_CONTRACTED_CLASS) + .removeClass(Settings.TOGGLE_CATEGORY_EXPANDED_CLASS); + } else { + $icon + .addClass(Settings.TOGGLE_CATEGORY_EXPANDED_CLASS) + .removeClass(Settings.TOGGLE_CATEGORY_CONTRACTED_CLASS); + } + $categoryHeaderRow + .closest("table") + .find("tr[data-category='" + categoryName + "']") + .toggleClass("contracted", wasExpanded); +} + +function moveTableRow(row, move_up) { var table = $(row).closest('table') var isArray = table.data('setting-type') === 'array' if (!isArray) { diff --git a/domain-server/src/DomainGatekeeper.cpp b/domain-server/src/DomainGatekeeper.cpp index c4a7d1a425..8f8c8e001c 100644 --- a/domain-server/src/DomainGatekeeper.cpp +++ b/domain-server/src/DomainGatekeeper.cpp @@ -120,6 +120,102 @@ void DomainGatekeeper::processConnectRequestPacket(QSharedPointer_settingsManager.getStandardPermissionsForName(NodePermissions::standardNameLocalhost); +#ifdef WANT_DEBUG + qDebug() << "| user-permissions: is local user, so:" << userPerms; +#endif + } + + if (verifiedUsername.isEmpty()) { + userPerms |= _server->_settingsManager.getStandardPermissionsForName(NodePermissions::standardNameAnonymous); +#ifdef WANT_DEBUG + qDebug() << "| user-permissions: unverified or no username for" << userPerms.getID() << ", so:" << userPerms; +#endif + + if (_server->_settingsManager.hasPermissionsForIP(senderAddress)) { + // this user comes from an IP we have in our permissions table, apply those permissions + userPerms = _server->_settingsManager.getPermissionsForIP(senderAddress); + +#ifdef WANT_DEBUG + qDebug() << "| user-permissions: specific IP matches, so:" << userPerms; +#endif + } + } else { + if (_server->_settingsManager.havePermissionsForName(verifiedUsername)) { + userPerms = _server->_settingsManager.getPermissionsForName(verifiedUsername); +#ifdef WANT_DEBUG + qDebug() << "| user-permissions: specific user matches, so:" << userPerms; +#endif + } else if (_server->_settingsManager.hasPermissionsForIP(senderAddress)) { + // this user comes from an IP we have in our permissions table, apply those permissions + userPerms = _server->_settingsManager.getPermissionsForIP(senderAddress); + +#ifdef WANT_DEBUG + qDebug() << "| user-permissions: specific IP matches, so:" << userPerms; +#endif + } else { + // they are logged into metaverse, but we don't have specific permissions for them. + userPerms |= _server->_settingsManager.getStandardPermissionsForName(NodePermissions::standardNameLoggedIn); +#ifdef WANT_DEBUG + qDebug() << "| user-permissions: user is logged-into metaverse, so:" << userPerms; +#endif + + // if this user is a friend of the domain-owner, give them friend's permissions + if (_domainOwnerFriends.contains(verifiedUsername)) { + userPerms |= _server->_settingsManager.getStandardPermissionsForName(NodePermissions::standardNameFriends); +#ifdef WANT_DEBUG + qDebug() << "| user-permissions: user is friends with domain-owner, so:" << userPerms; +#endif + } + + // if this user is a known member of a group, give them the implied permissions + foreach (QUuid groupID, _server->_settingsManager.getGroupIDs()) { + QUuid rankID = _server->_settingsManager.isGroupMember(verifiedUsername, groupID); + if (rankID != QUuid()) { + userPerms |= _server->_settingsManager.getPermissionsForGroup(groupID, rankID); + + GroupRank rank = _server->_settingsManager.getGroupRank(groupID, rankID); +#ifdef WANT_DEBUG + qDebug() << "| user-permissions: user is in group:" << groupID << " rank:" + << rank.name << "so:" << userPerms; +#endif + } + } + + // if this user is a known member of a blacklist group, remove the implied permissions + foreach (QUuid groupID, _server->_settingsManager.getBlacklistGroupIDs()) { + QUuid rankID = _server->_settingsManager.isGroupMember(verifiedUsername, groupID); + if (rankID != QUuid()) { + QUuid rankID = _server->_settingsManager.isGroupMember(verifiedUsername, groupID); + if (rankID != QUuid()) { + userPerms &= ~_server->_settingsManager.getForbiddensForGroup(groupID, rankID); + + GroupRank rank = _server->_settingsManager.getGroupRank(groupID, rankID); +#ifdef WANT_DEBUG + qDebug() << "| user-permissions: user is in blacklist group:" << groupID << " rank:" << rank.name + << "so:" << userPerms; +#endif + } + } + } + } + + userPerms.setID(verifiedUsername); + userPerms.setVerifiedUserName(verifiedUsername); + } + +#ifdef WANT_DEBUG + qDebug() << "| user-permissions: final:" << userPerms; +#endif + return userPerms; +} + 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 @@ -129,40 +225,34 @@ void DomainGatekeeper::updateNodePermissions() { auto limitedNodeList = DependencyManager::get(); limitedNodeList->eachNode([this, limitedNodeList, &nodesToKill](const SharedNodePointer& node){ - QString username = node->getPermissions().getUserName(); - NodePermissions userPerms(username); + // the id and the username in NodePermissions will often be the same, but id is set before + // authentication and verifiedUsername is only set once they user's key has been confirmed. + QString verifiedUsername = node->getPermissions().getVerifiedUserName(); + NodePermissions userPerms(NodePermissionsKey(verifiedUsername, 0)); if (node->getPermissions().isAssignment) { // this node is an assignment-client userPerms.isAssignment = true; - userPerms.canAdjustLocks = true; - userPerms.canRezPermanentEntities = true; - userPerms.canRezTemporaryEntities = true; + userPerms.permissions |= NodePermissions::Permission::canConnectToDomain; + userPerms.permissions |= NodePermissions::Permission::canAdjustLocks; + userPerms.permissions |= NodePermissions::Permission::canRezPermanentEntities; + userPerms.permissions |= NodePermissions::Permission::canRezTemporaryEntities; } 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); - } - } + // at this point we don't have a sending socket for packets from this node - assume it is the active socket + // or the public socket if we haven't activated a socket for the node yet + HifiSockAddr connectingAddr = node->getActiveSocket() ? *node->getActiveSocket() : node->getPublicSocket(); + + userPerms = setPermissionsForUser(isLocalUser, verifiedUsername, connectingAddr.getAddress()); } node->setPermissions(userPerms); - if (!userPerms.canConnectToDomain) { + if (!userPerms.can(NodePermissions::Permission::canConnectToDomain)) { qDebug() << "node" << node->getUUID() << "no longer has permission to connect."; // hang up on this node nodesToKill << node; @@ -215,12 +305,13 @@ SharedNodePointer DomainGatekeeper::processAssignmentConnectRequest(const NodeCo // cleanup the PendingAssignedNodeData for this assignment now that it's connecting _pendingAssignedNodes.erase(it); - // always allow assignment clients to create and destroy entities NodePermissions userPerms; userPerms.isAssignment = true; - userPerms.canAdjustLocks = true; - userPerms.canRezPermanentEntities = true; - userPerms.canRezTemporaryEntities = true; + userPerms.permissions |= NodePermissions::Permission::canConnectToDomain; + // always allow assignment clients to create and destroy entities + userPerms.permissions |= NodePermissions::Permission::canAdjustLocks; + userPerms.permissions |= NodePermissions::Permission::canRezPermanentEntities; + userPerms.permissions |= NodePermissions::Permission::canRezTemporaryEntities; newNode->setPermissions(userPerms); return newNode; } @@ -234,64 +325,58 @@ SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnect auto limitedNodeList = DependencyManager::get(); // start with empty permissions - NodePermissions userPerms(username); + NodePermissions userPerms(NodePermissionsKey(username, 0)); 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 (isLocalUser) { - userPerms |= _server->_settingsManager.getStandardPermissionsForName(NodePermissions::standardNameLocalhost); - qDebug() << "user-permissions: is local user, so:" << userPerms; - } - 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) { + QString verifiedUsername; // if this remains empty, consider this an anonymous connection attempt + if (!username.isEmpty()) { + if (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); + getGroupMemberships(username); // optimistically get started on group memberships +#ifdef WANT_DEBUG + qDebug() << "stalling login because we have no username-signature:" << username; +#endif return SharedNodePointer(); - } - } - - 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 if (verifyUserSignature(username, usernameSignature, nodeConnection.senderSockAddr)) { + // they sent us a username and the signature verifies it + getGroupMemberships(username); + verifiedUsername = username; } else { - // 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) { + // they sent us a username, but it didn't check out + requestUserPublicKey(username); +#ifdef WANT_DEBUG + qDebug() << "stalling login because signature verification failed:" << username; +#endif return SharedNodePointer(); } } - qDebug() << "user-permissions: final:" << userPerms; + userPerms = setPermissionsForUser(isLocalUser, verifiedUsername, nodeConnection.senderSockAddr.getAddress()); - if (!userPerms.canConnectToDomain) { + if (!userPerms.can(NodePermissions::Permission::canConnectToDomain)) { sendConnectionDeniedPacket("You lack the required permissions to connect to this domain.", nodeConnection.senderSockAddr, DomainHandler::ConnectionRefusedReason::TooManyUsers); +#ifdef WANT_DEBUG + qDebug() << "stalling login due to permissions:" << username; +#endif return SharedNodePointer(); } - if (!userPerms.canConnectPastMaxCapacity && !isWithinMaxCapacity()) { + if (!userPerms.can(NodePermissions::Permission::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); +#ifdef WANT_DEBUG + qDebug() << "stalling login due to max capacity:" << username; +#endif return SharedNodePointer(); } @@ -305,10 +390,8 @@ SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnect // we have a node that already has these exact sockets - this occurs if a node // is unable to connect to the domain hintNodeID = node->getUUID(); - return false; } - return true; }); @@ -328,6 +411,10 @@ SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnect nodeData->addOverrideForKey(USERNAME_UUID_REPLACEMENT_STATS_KEY, uuidStringWithoutCurlyBraces(newNode->getUUID()), username); +#ifdef WANT_DEBUG + qDebug() << "accepting login:" << username; +#endif + return newNode; } @@ -365,11 +452,11 @@ SharedNodePointer DomainGatekeeper::addVerifiedNodeFromConnectRequest(const Node bool DomainGatekeeper::verifyUserSignature(const QString& username, const QByteArray& usernameSignature, const HifiSockAddr& senderSockAddr) { - // it's possible this user can be allowed to connect, but we need to check their username signature - QByteArray publicKeyArray = _userPublicKeys.value(username); + auto lowerUsername = username.toLower(); + QByteArray publicKeyArray = _userPublicKeys.value(lowerUsername); - const QUuid& connectionToken = _connectionTokenHash.value(username.toLower()); + const QUuid& connectionToken = _connectionTokenHash.value(lowerUsername); if (!publicKeyArray.isEmpty() && !connectionToken.isNull()) { // if we do have a public key for the user, check for a signature match @@ -379,8 +466,8 @@ bool DomainGatekeeper::verifyUserSignature(const QString& username, // 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()), + QByteArray lowercaseUsernameUTF8 = lowerUsername.toUtf8(); + QByteArray usernameWithToken = QCryptographicHash::hash(lowercaseUsernameUTF8.append(connectionToken.toRfc4122()), QCryptographicHash::Sha256); if (rsaPublicKey) { @@ -471,10 +558,20 @@ void DomainGatekeeper::requestUserPublicKey(const QString& username) { return; } + QString lowerUsername = username.toLower(); + if (_inFlightPublicKeyRequests.contains(lowerUsername)) { + // public-key request for this username is already flight, not rerequesting + return; + } + _inFlightPublicKeyRequests += lowerUsername; + // 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"; + callbackParams.errorCallbackReceiver = this; + callbackParams.errorCallbackMethod = "publicKeyJSONErrorCallback"; + const QString USER_PUBLIC_KEY_PATH = "api/v1/users/%1/public_key"; @@ -485,28 +582,37 @@ void DomainGatekeeper::requestUserPublicKey(const QString& username) { QNetworkAccessManager::GetOperation, callbackParams); } +QString extractUsernameFromPublicKeyRequest(QNetworkReply& requestReply) { + // extract the username from the request url + QString username; + 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) { + username = usernameRegex.cap(1); + } + return username.toLower(); +} + void DomainGatekeeper::publicKeyJSONCallback(QNetworkReply& requestReply) { QJsonObject jsonObject = QJsonDocument::fromJson(requestReply.readAll()).object(); + QString username = extractUsernameFromPublicKeyRequest(requestReply); - if (jsonObject["status"].toString() == "success") { - // figure out which user this is for + if (jsonObject["status"].toString() == "success" && !username.isEmpty()) { + // pull the public key as a QByteArray from this response + const QString JSON_DATA_KEY = "data"; + const QString JSON_PUBLIC_KEY_KEY = "public_key"; - 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()); - } + _userPublicKeys[username.toLower()] = + QByteArray::fromBase64(jsonObject[JSON_DATA_KEY].toObject()[JSON_PUBLIC_KEY_KEY].toString().toUtf8()); } + + _inFlightPublicKeyRequests.remove(username); +} + +void DomainGatekeeper::publicKeyJSONErrorCallback(QNetworkReply& requestReply) { + qDebug() << "publicKey api call failed:" << requestReply.error(); + QString username = extractUsernameFromPublicKeyRequest(requestReply); + _inFlightPublicKeyRequests.remove(username); } void DomainGatekeeper::sendProtocolMismatchConnectionDenial(const HifiSockAddr& senderSockAddr) { @@ -645,3 +751,159 @@ void DomainGatekeeper::processICEPingReplyPacket(QSharedPointer sendingPeer->activateMatchingOrNewSymmetricSocket(message->getSenderSockAddr()); } } + +void DomainGatekeeper::getGroupMemberships(const QString& username) { + // loop through the groups mentioned on the settings page and ask if this user is in each. The replies + // will be received asynchronously and permissions will be updated as the answers come in. + + // if we've already asked, wait for the answer before asking again + QString lowerUsername = username.toLower(); + if (_inFlightGroupMembershipsRequests.contains(lowerUsername)) { + // public-key request for this username is already flight, not rerequesting + return; + } + _inFlightGroupMembershipsRequests += lowerUsername; + + QJsonObject json; + QSet groupIDSet; + foreach (QUuid groupID, _server->_settingsManager.getGroupIDs() + _server->_settingsManager.getBlacklistGroupIDs()) { + groupIDSet += groupID.toString().mid(1,36); + } + QJsonArray groupIDs = QJsonArray::fromStringList(groupIDSet.toList()); + json["groups"] = groupIDs; + + JSONCallbackParameters callbackParams; + callbackParams.jsonCallbackReceiver = this; + callbackParams.jsonCallbackMethod = "getIsGroupMemberJSONCallback"; + callbackParams.errorCallbackReceiver = this; + callbackParams.errorCallbackMethod = "getIsGroupMemberErrorCallback"; + + const QString GET_IS_GROUP_MEMBER_PATH = "api/v1/groups/members/%2"; + DependencyManager::get()->sendRequest(GET_IS_GROUP_MEMBER_PATH.arg(username), + AccountManagerAuth::Required, + QNetworkAccessManager::PostOperation, callbackParams, + QJsonDocument(json).toJson()); + +} + +QString extractUsernameFromGroupMembershipsReply(QNetworkReply& requestReply) { + // extract the username from the request url + QString username; + const QString GROUP_MEMBERSHIPS_URL_REGEX_STRING = "api\\/v1\\/groups\\/members\\/([A-Za-z0-9_\\.]+)"; + QRegExp usernameRegex(GROUP_MEMBERSHIPS_URL_REGEX_STRING); + if (usernameRegex.indexIn(requestReply.url().toString()) != -1) { + username = usernameRegex.cap(1); + } + return username.toLower(); +} + +void DomainGatekeeper::getIsGroupMemberJSONCallback(QNetworkReply& requestReply) { + // { + // "data":{ + // "username":"sethalves", + // "groups":{ + // "fd55479a-265d-4990-854e-3d04214ad1b0":{ + // "name":"Blerg Blah", + // "rank":{ + // "name":"admin", + // "order":1 + // } + // } + // } + // }, + // "status":"success" + // } + + QJsonObject jsonObject = QJsonDocument::fromJson(requestReply.readAll()).object(); + if (jsonObject["status"].toString() == "success") { + QJsonObject data = jsonObject["data"].toObject(); + QJsonObject groups = data["groups"].toObject(); + QString username = data["username"].toString(); + _server->_settingsManager.clearGroupMemberships(username); + foreach (auto groupID, groups.keys()) { + QJsonObject group = groups[groupID].toObject(); + QJsonObject rank = group["rank"].toObject(); + QUuid rankID = QUuid(rank["id"].toString()); + _server->_settingsManager.recordGroupMembership(username, groupID, rankID); + } + } else { + qDebug() << "getIsGroupMember api call returned:" << QJsonDocument(jsonObject).toJson(QJsonDocument::Compact); + } + + _inFlightGroupMembershipsRequests.remove(extractUsernameFromGroupMembershipsReply(requestReply)); +} + +void DomainGatekeeper::getIsGroupMemberErrorCallback(QNetworkReply& requestReply) { + qDebug() << "getIsGroupMember api call failed:" << requestReply.error(); + _inFlightGroupMembershipsRequests.remove(extractUsernameFromGroupMembershipsReply(requestReply)); +} + +void DomainGatekeeper::getDomainOwnerFriendsList() { + JSONCallbackParameters callbackParams; + callbackParams.jsonCallbackReceiver = this; + callbackParams.jsonCallbackMethod = "getDomainOwnerFriendsListJSONCallback"; + callbackParams.errorCallbackReceiver = this; + callbackParams.errorCallbackMethod = "getDomainOwnerFriendsListErrorCallback"; + + const QString GET_FRIENDS_LIST_PATH = "api/v1/user/friends"; + DependencyManager::get()->sendRequest(GET_FRIENDS_LIST_PATH, AccountManagerAuth::Required, + QNetworkAccessManager::GetOperation, callbackParams, QByteArray(), + NULL, QVariantMap()); +} + +void DomainGatekeeper::getDomainOwnerFriendsListJSONCallback(QNetworkReply& requestReply) { + // { + // status: "success", + // data: { + // friends: [ + // "chris", + // "freidrica", + // "G", + // "huffman", + // "leo", + // "philip", + // "ryan", + // "sam", + // "ZappoMan" + // ] + // } + // } + QJsonObject jsonObject = QJsonDocument::fromJson(requestReply.readAll()).object(); + if (jsonObject["status"].toString() == "success") { + _domainOwnerFriends.clear(); + QJsonArray friends = jsonObject["data"].toObject()["friends"].toArray(); + for (int i = 0; i < friends.size(); i++) { + _domainOwnerFriends += friends.at(i).toString(); + } + } else { + qDebug() << "getDomainOwnerFriendsList api call returned:" << QJsonDocument(jsonObject).toJson(QJsonDocument::Compact); + } +} + +void DomainGatekeeper::getDomainOwnerFriendsListErrorCallback(QNetworkReply& requestReply) { + qDebug() << "getDomainOwnerFriendsList api call failed:" << requestReply.error(); +} + +void DomainGatekeeper::refreshGroupsCache() { + // if agents are connected to this domain, refresh our cached information about groups and memberships in such. + getDomainOwnerFriendsList(); + + auto nodeList = DependencyManager::get(); + nodeList->eachNode([&](const SharedNodePointer& node) { + if (!node->getPermissions().isAssignment) { + // this node is an agent + const QString& verifiedUserName = node->getPermissions().getVerifiedUserName(); + if (!verifiedUserName.isEmpty()) { + getGroupMemberships(verifiedUserName); + } + } + }); + + _server->_settingsManager.apiRefreshGroupInformation(); + + updateNodePermissions(); + +#if WANT_DEBUG + _server->_settingsManager.debugDumpGroupsState(); +#endif +} diff --git a/domain-server/src/DomainGatekeeper.h b/domain-server/src/DomainGatekeeper.h index 50bbf38543..06ecfcf285 100644 --- a/domain-server/src/DomainGatekeeper.h +++ b/domain-server/src/DomainGatekeeper.h @@ -51,7 +51,16 @@ public slots: void processICEPeerInformationPacket(QSharedPointer message); void publicKeyJSONCallback(QNetworkReply& requestReply); - + void publicKeyJSONErrorCallback(QNetworkReply& requestReply); + + void getIsGroupMemberJSONCallback(QNetworkReply& requestReply); + void getIsGroupMemberErrorCallback(QNetworkReply& requestReply); + + void getDomainOwnerFriendsListJSONCallback(QNetworkReply& requestReply); + void getDomainOwnerFriendsListErrorCallback(QNetworkReply& requestReply); + + void refreshGroupsCache(); + signals: void killNode(SharedNodePointer node); void connectedNode(SharedNodePointer node); @@ -93,6 +102,15 @@ private: QHash _connectionTokenHash; QHash _userPublicKeys; + QSet _inFlightPublicKeyRequests; // keep track of which we've already asked for + QSet _domainOwnerFriends; // keep track of friends of the domain owner + QSet _inFlightGroupMembershipsRequests; // keep track of which we've already asked for + + NodePermissions setPermissionsForUser(bool isLocalUser, QString verifiedUsername, const QHostAddress& senderAddress); + + void getGroupMemberships(const QString& username); + // void getIsGroupMember(const QString& username, const QUuid groupID); + void getDomainOwnerFriendsList(); }; diff --git a/domain-server/src/DomainMetadata.cpp b/domain-server/src/DomainMetadata.cpp index d2762e788b..d614b1bbd3 100644 --- a/domain-server/src/DomainMetadata.cpp +++ b/domain-server/src/DomainMetadata.cpp @@ -184,10 +184,10 @@ void DomainMetadata::securityChanged(bool send) { QString restriction; const auto& settingsManager = static_cast(parent())->_settingsManager; - bool hasAnonymousAccess = - settingsManager.getStandardPermissionsForName(NodePermissions::standardNameAnonymous).canConnectToDomain; - bool hasHifiAccess = - settingsManager.getStandardPermissionsForName(NodePermissions::standardNameLoggedIn).canConnectToDomain; + bool hasAnonymousAccess = settingsManager.getStandardPermissionsForName(NodePermissions::standardNameAnonymous).can( + NodePermissions::Permission::canConnectToDomain); + bool hasHifiAccess = settingsManager.getStandardPermissionsForName(NodePermissions::standardNameLoggedIn).can( + NodePermissions::Permission::canConnectToDomain); if (hasAnonymousAccess) { restriction = hasHifiAccess ? RESTRICTION_OPEN : RESTRICTION_ANON; } else if (hasHifiAccess) { diff --git a/domain-server/src/DomainServer.cpp b/domain-server/src/DomainServer.cpp index 664db98acb..23e37efaf1 100644 --- a/domain-server/src/DomainServer.cpp +++ b/domain-server/src/DomainServer.cpp @@ -38,6 +38,7 @@ #include #include #include +#include #include "DomainServerNodeData.h" #include "NodeConnectionData.h" @@ -106,11 +107,15 @@ DomainServer::DomainServer(int argc, char* argv[]) : connect(&_settingsManager, &DomainServerSettingsManager::updateNodePermissions, &_gatekeeper, &DomainGatekeeper::updateNodePermissions); + setupGroupCacheRefresh(); + // if we were given a certificate/private key or oauth credentials they must succeed if (!(optionallyReadX509KeyAndCertificate() && optionallySetupOAuth())) { return; } + _settingsManager.apiRefreshGroupInformation(); + setupNodeListAndAssignments(); setupAutomaticNetworking(); if (!getID().isNull()) { @@ -406,6 +411,7 @@ void DomainServer::setupNodeListAndAssignments() { // NodeList won't be available to the settings manager when it is created, so call registerListener here packetReceiver.registerListener(PacketType::DomainSettingsRequest, &_settingsManager, "processSettingsRequestPacket"); + packetReceiver.registerListener(PacketType::NodeKickRequest, &_settingsManager, "processNodeKickRequestPacket"); // register the gatekeeper for the packets it needs to receive packetReceiver.registerListener(PacketType::DomainConnectRequest, &_gatekeeper, "processConnectRequestPacket"); @@ -1098,12 +1104,11 @@ void DomainServer::sendHeartbeatToMetaverse(const QString& networkAddress) { static const QString AUTOMATIC_NETWORKING_KEY = "automatic_networking"; domainObject[AUTOMATIC_NETWORKING_KEY] = _automaticNetworkingSetting; - // add access level for anonymous connections // consider the domain to be "restricted" if anonymous connections are disallowed static const QString RESTRICTED_ACCESS_FLAG = "restricted"; NodePermissions anonymousPermissions = _settingsManager.getPermissionsForName(NodePermissions::standardNameAnonymous); - domainObject[RESTRICTED_ACCESS_FLAG] = !anonymousPermissions.canConnectToDomain; + domainObject[RESTRICTED_ACCESS_FLAG] = !anonymousPermissions.can(NodePermissions::Permission::canConnectToDomain); const auto& temporaryDomainKey = DependencyManager::get()->getTemporaryDomainKey(getID()); if (!temporaryDomainKey.isEmpty()) { @@ -2327,3 +2332,14 @@ void DomainServer::randomizeICEServerAddress(bool shouldTriggerHostLookup) { // immediately send an update to the metaverse API when our ice-server changes sendICEServerAddressToMetaverseAPI(); } + +void DomainServer::setupGroupCacheRefresh() { + const int REFRESH_GROUPS_INTERVAL_MSECS = 15 * MSECS_PER_SECOND; + + if (!_metaverseGroupCacheTimer) { + // setup a timer to refresh this server's cached group details + _metaverseGroupCacheTimer = new QTimer { this }; + connect(_metaverseGroupCacheTimer, &QTimer::timeout, &_gatekeeper, &DomainGatekeeper::refreshGroupsCache); + _metaverseGroupCacheTimer->start(REFRESH_GROUPS_INTERVAL_MSECS); + } +} diff --git a/domain-server/src/DomainServer.h b/domain-server/src/DomainServer.h index 138cb9ca2d..4004333789 100644 --- a/domain-server/src/DomainServer.h +++ b/domain-server/src/DomainServer.h @@ -41,7 +41,7 @@ class DomainServer : public QCoreApplication, public HTTPSRequestHandler { public: DomainServer(int argc, char* argv[]); ~DomainServer(); - + static int const EXIT_CODE_REBOOT; bool handleHTTPRequest(HTTPConnection* connection, const QUrl& url, bool skipSubHandler = false); @@ -64,7 +64,7 @@ public slots: void processNodeDisconnectRequestPacket(QSharedPointer message); void processICEServerHeartbeatDenialPacket(QSharedPointer message); void processICEServerHeartbeatACK(QSharedPointer message); - + private slots: void aboutToQuit(); @@ -74,7 +74,7 @@ private slots: void performIPAddressUpdate(const HifiSockAddr& newPublicSockAddr); void sendHeartbeatToMetaverse() { sendHeartbeatToMetaverse(QString()); } void sendHeartbeatToIceServer(); - + void handleConnectedNode(SharedNodePointer newNode); void handleTempDomainSuccess(QNetworkReply& requestReply); @@ -96,7 +96,7 @@ signals: void iceServerChanged(); void userConnected(); void userDisconnected(); - + private: const QUuid& getID(); @@ -136,7 +136,7 @@ private: SharedAssignmentPointer deployableAssignmentForRequest(const Assignment& requestAssignment); void refreshStaticAssignmentAndAddToQueue(SharedAssignmentPointer& assignment); void addStaticAssignmentsToQueue(); - + QUrl oauthRedirectURL(); QUrl oauthAuthorizationURL(const QUuid& stateUUID = QUuid::createUuid()); @@ -151,7 +151,9 @@ private: QJsonObject jsonForSocket(const HifiSockAddr& socket); QJsonObject jsonObjectForNode(const SharedNodePointer& node); - + + void setupGroupCacheRefresh(); + DomainGatekeeper _gatekeeper; HTTPManager _httpManager; @@ -184,6 +186,7 @@ private: DomainMetadata* _metadata { nullptr }; QTimer* _iceHeartbeatTimer { nullptr }; QTimer* _metaverseHeartbeatTimer { nullptr }; + QTimer* _metaverseGroupCacheTimer { nullptr }; QList _iceServerAddresses; QSet _failedIceServerAddresses; diff --git a/domain-server/src/DomainServerSettingsManager.cpp b/domain-server/src/DomainServerSettingsManager.cpp index 262cc9d9ee..dc49bc6126 100644 --- a/domain-server/src/DomainServerSettingsManager.cpp +++ b/domain-server/src/DomainServerSettingsManager.cpp @@ -20,7 +20,7 @@ #include #include #include - +#include #include #include @@ -31,9 +31,6 @@ #include "DomainServerSettingsManager.h" -#define WANT_DEBUG 1 - - const QString SETTINGS_DESCRIPTION_RELATIVE_PATH = "/resources/describe-settings.json"; const QString DESCRIPTION_SETTINGS_KEY = "settings"; @@ -98,6 +95,8 @@ void DomainServerSettingsManager::processSettingsRequestPacket(QSharedPointercanConvert(QMetaType::QVariantList) @@ -132,9 +131,7 @@ void DomainServerSettingsManager::setupConfigMap(const QStringList& argumentList // In the pre-toggle system the user had a list of allowed users, so // we need to set security.restricted_access to true - QVariant* restrictedAccess = valueForKeyPath(_configMap.getUserConfig(), - RESTRICTED_ACCESS_SETTINGS_KEYPATH, - true); + QVariant* restrictedAccess = _configMap.valueForKeyPath(RESTRICTED_ACCESS_SETTINGS_KEYPATH, true); *restrictedAccess = QVariant(true); @@ -152,21 +149,20 @@ void DomainServerSettingsManager::setupConfigMap(const QStringList& argumentList static const QString ENTITY_FILE_PATH_KEYPATH = ENTITY_SERVER_SETTINGS_KEY + ".persistFilePath"; // this was prior to change of poorly named entitiesFileName to entitiesFilePath - QVariant* persistFileNameVariant = valueForKeyPath(_configMap.getMergedConfig(), - ENTITY_SERVER_SETTINGS_KEY + "." + ENTITY_FILE_NAME_KEY); + QVariant* persistFileNameVariant = _configMap.valueForKeyPath(ENTITY_SERVER_SETTINGS_KEY + "." + ENTITY_FILE_NAME_KEY); if (persistFileNameVariant && persistFileNameVariant->canConvert(QMetaType::QString)) { QString persistFileName = persistFileNameVariant->toString(); qDebug() << "Migrating persistFilename to persistFilePath for entity-server settings"; // grab the persistFilePath option, create it if it doesn't exist - QVariant* persistFilePath = valueForKeyPath(_configMap.getUserConfig(), ENTITY_FILE_PATH_KEYPATH, true); + QVariant* persistFilePath = _configMap.valueForKeyPath(ENTITY_FILE_PATH_KEYPATH, true); // write the migrated value *persistFilePath = persistFileName; // remove the old setting - QVariant* entityServerVariant = valueForKeyPath(_configMap.getUserConfig(), ENTITY_SERVER_SETTINGS_KEY); + QVariant* entityServerVariant = _configMap.valueForKeyPath(ENTITY_SERVER_SETTINGS_KEY); if (entityServerVariant && entityServerVariant->canConvert(QMetaType::QVariantMap)) { QVariantMap entityServerMap = entityServerVariant->toMap(); entityServerMap.remove(ENTITY_FILE_NAME_KEY); @@ -188,7 +184,7 @@ void DomainServerSettingsManager::setupConfigMap(const QStringList& argumentList // If we have a password in the previous settings file, make it base 64 static const QString BASIC_AUTH_PASSWORD_KEY_PATH { "security.http_password" }; - QVariant* passwordVariant = valueForKeyPath(_configMap.getUserConfig(), BASIC_AUTH_PASSWORD_KEY_PATH); + QVariant* passwordVariant = _configMap.valueForKeyPath(BASIC_AUTH_PASSWORD_KEY_PATH); if (passwordVariant && passwordVariant->canConvert(QMetaType::QString)) { QString plaintextPassword = passwordVariant->toString(); @@ -219,40 +215,50 @@ void DomainServerSettingsManager::setupConfigMap(const QStringList& argumentList new NodePermissions(NodePermissions::standardNameAnonymous)); _standardAgentPermissions[NodePermissions::standardNameLoggedIn].reset( new NodePermissions(NodePermissions::standardNameLoggedIn)); + _standardAgentPermissions[NodePermissions::standardNameFriends].reset( + new NodePermissions(NodePermissions::standardNameFriends)); if (isRestrictedAccess) { // only users in allow-users list can connect - _standardAgentPermissions[NodePermissions::standardNameAnonymous]->canConnectToDomain = false; - _standardAgentPermissions[NodePermissions::standardNameLoggedIn]->canConnectToDomain = false; + _standardAgentPermissions[NodePermissions::standardNameAnonymous]->clear( + NodePermissions::Permission::canConnectToDomain); + _standardAgentPermissions[NodePermissions::standardNameLoggedIn]->clear( + NodePermissions::Permission::canConnectToDomain); } // 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)); + _agentPermissions[NodePermissionsKey(allowedUser, 0)].reset(new NodePermissions(allowedUser)); + _agentPermissions[NodePermissionsKey(allowedUser, 0)]->set(NodePermissions::Permission::canConnectToDomain); } foreach (QString allowedEditor, allowedEditors) { - if (!_agentPermissions.contains(allowedEditor)) { - _agentPermissions[allowedEditor].reset(new NodePermissions(allowedEditor)); + NodePermissionsKey editorKey(allowedEditor, 0); + if (!_agentPermissions.contains(editorKey)) { + _agentPermissions[editorKey].reset(new NodePermissions(allowedEditor)); if (isRestrictedAccess) { // they can change locks, but can't connect. - _agentPermissions[allowedEditor]->canConnectToDomain = false; + _agentPermissions[editorKey]->clear(NodePermissions::Permission::canConnectToDomain); } } - _agentPermissions[allowedEditor]->canAdjustLocks = true; + _agentPermissions[editorKey]->set(NodePermissions::Permission::canAdjustLocks); } - QList> permissionsSets; + QList> permissionsSets; permissionsSets << _standardAgentPermissions.get() << _agentPermissions.get(); foreach (auto permissionsSet, permissionsSets) { - foreach (QString userName, permissionsSet.keys()) { + foreach (NodePermissionsKey userKey, permissionsSet.keys()) { if (onlyEditorsAreRezzers) { - permissionsSet[userName]->canRezPermanentEntities = permissionsSet[userName]->canAdjustLocks; - permissionsSet[userName]->canRezTemporaryEntities = permissionsSet[userName]->canAdjustLocks; + if (permissionsSet[userKey]->can(NodePermissions::Permission::canAdjustLocks)) { + permissionsSet[userKey]->set(NodePermissions::Permission::canRezPermanentEntities); + permissionsSet[userKey]->set(NodePermissions::Permission::canRezTemporaryEntities); + } else { + permissionsSet[userKey]->clear(NodePermissions::Permission::canRezPermanentEntities); + permissionsSet[userKey]->clear(NodePermissions::Permission::canRezTemporaryEntities); + } } else { - permissionsSet[userName]->canRezPermanentEntities = true; - permissionsSet[userName]->canRezTemporaryEntities = true; + permissionsSet[userKey]->set(NodePermissions::Permission::canRezPermanentEntities); + permissionsSet[userKey]->set(NodePermissions::Permission::canRezTemporaryEntities); } } } @@ -266,6 +272,28 @@ void DomainServerSettingsManager::setupConfigMap(const QStringList& argumentList // This was prior to operating hours, so add default hours validateDescriptorsMap(); } + + if (oldVersion < 1.6) { + unpackPermissions(); + + // This was prior to addition of kick permissions, add that to localhost permissions by default + _standardAgentPermissions[NodePermissions::standardNameLocalhost]->set(NodePermissions::Permission::canKick); + + packPermissions(); + } + + if (oldVersion < 1.7) { + // This was prior to the removal of the master config file + // So we write the merged config to the user config file, and stop reading from the user config file + + qDebug() << "Migrating merged config to user config file. The master config file is deprecated."; + + // replace the user config by the merged config + _configMap.getConfig() = _configMap.getMergedConfig(); + + // persist the new config so the user config file has the correctly merged config + persistToFile(); + } } unpackPermissions(); @@ -286,9 +314,9 @@ void DomainServerSettingsManager::validateDescriptorsMap() { static const QString WEEKEND_HOURS{ "descriptors.weekend_hours" }; static const QString UTC_OFFSET{ "descriptors.utc_offset" }; - QVariant* weekdayHours = valueForKeyPath(_configMap.getUserConfig(), WEEKDAY_HOURS, true); - QVariant* weekendHours = valueForKeyPath(_configMap.getUserConfig(), WEEKEND_HOURS, true); - QVariant* utcOffset = valueForKeyPath(_configMap.getUserConfig(), UTC_OFFSET, true); + QVariant* weekdayHours = _configMap.valueForKeyPath(WEEKDAY_HOURS, true); + QVariant* weekendHours = _configMap.valueForKeyPath(WEEKEND_HOURS, true); + QVariant* utcOffset = _configMap.valueForKeyPath(UTC_OFFSET, true); static const QString OPEN{ "open" }; static const QString CLOSE{ "close" }; @@ -311,137 +339,390 @@ void DomainServerSettingsManager::validateDescriptorsMap() { if (wasMalformed) { // write the new settings to file persistToFile(); + } +} - // reload the master and user config so the merged config is correct - _configMap.loadMasterAndUserConfig(_argumentList); + +void DomainServerSettingsManager::initializeGroupPermissions(NodePermissionsMap& permissionsRows, + QString groupName, NodePermissionsPointer perms) { + // this is called when someone has used the domain-settings webpage to add a group. They type the group's name + // and give it some permissions. The domain-server asks api for the group's ranks and populates the map + // with them. Here, that initial user-entered row is removed and it's permissions are copied to all the ranks + // except owner. + + QString groupNameLower = groupName.toLower(); + + foreach (NodePermissionsKey nameKey, permissionsRows.keys()) { + if (nameKey.first.toLower() != groupNameLower) { + continue; + } + QUuid groupID = _groupIDs[groupNameLower]; + QUuid rankID = nameKey.second; + GroupRank rank = _groupRanks[groupID][rankID]; + if (rank.order == 0) { + // we don't copy the initial permissions to the owner. + continue; + } + permissionsRows[nameKey]->setAll(false); + *(permissionsRows[nameKey]) |= *perms; } } void DomainServerSettingsManager::packPermissionsForMap(QString mapName, - NodePermissionsMap& agentPermissions, + NodePermissionsMap& permissionsRows, QString keyPath) { - QVariant* security = valueForKeyPath(_configMap.getUserConfig(), "security"); - if (!security || !security->canConvert(QMetaType::QVariantMap)) { - security = valueForKeyPath(_configMap.getUserConfig(), "security", true); + // find (or create) the "security" section of the settings map + QVariant* security = _configMap.valueForKeyPath("security", true); + if (!security->canConvert(QMetaType::QVariantMap)) { (*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); + // find (or create) whichever subsection of "security" we are packing + QVariant* permissions = _configMap.valueForKeyPath(keyPath, true); + if (!permissions->canConvert(QMetaType::QVariantList)) { (*permissions) = QVariantList(); } + // convert details for each member of the subsection QVariantList* permissionsList = reinterpret_cast(permissions); (*permissionsList).clear(); - foreach (QString userName, agentPermissions.keys()) { - *permissionsList += agentPermissions[userName]->toVariant(); + QList permissionsKeys = permissionsRows.keys(); + + // when a group is added from the domain-server settings page, the config map has a group-name with + // no ID or rank. We need to leave that there until we get a valid response back from the api. + // once we have the ranks and IDs, we need to delete the original entry so that it doesn't show + // up in the settings-page with undefined's after it. + QHash groupNamesWithRanks; + // note which groups have rank/ID information + foreach (NodePermissionsKey userKey, permissionsKeys) { + NodePermissionsPointer perms = permissionsRows[userKey]; + if (perms->getRankID() != QUuid()) { + groupNamesWithRanks[userKey.first] = true; + } + } + foreach (NodePermissionsKey userKey, permissionsKeys) { + NodePermissionsPointer perms = permissionsRows[userKey]; + if (perms->isGroup()) { + QString groupName = userKey.first; + if (perms->getRankID() == QUuid() && groupNamesWithRanks.contains(groupName)) { + // copy the values from this user-added entry to the other (non-owner) ranks and remove it. + permissionsRows.remove(userKey); + initializeGroupPermissions(permissionsRows, groupName, perms); + } + } + } + + // convert each group-name / rank-id pair to a variant-map + foreach (NodePermissionsKey userKey, permissionsKeys) { + if (!permissionsRows.contains(userKey)) { + continue; + } + NodePermissionsPointer perms = permissionsRows[userKey]; + if (perms->isGroup()) { + QHash& groupRanks = _groupRanks[perms->getGroupID()]; + *permissionsList += perms->toVariant(groupRanks); + } else { + *permissionsList += perms->toVariant(); + } } } void DomainServerSettingsManager::packPermissions() { // transfer details from _agentPermissions to _configMap + + // save settings for anonymous / logged-in / localhost packPermissionsForMap("standard_permissions", _standardAgentPermissions, AGENT_STANDARD_PERMISSIONS_KEYPATH); // save settings for specific users packPermissionsForMap("permissions", _agentPermissions, AGENT_PERMISSIONS_KEYPATH); + // save settings for IP addresses + packPermissionsForMap("permissions", _ipPermissions, IP_PERMISSIONS_KEYPATH); + + // save settings for groups + packPermissionsForMap("permissions", _groupPermissions, GROUP_PERMISSIONS_KEYPATH); + + // save settings for blacklist groups + packPermissionsForMap("permissions", _groupForbiddens, GROUP_FORBIDDENS_KEYPATH); + persistToFile(); - _configMap.loadMasterAndUserConfig(_argumentList); } -void DomainServerSettingsManager::unpackPermissions() { - // transfer details from _configMap to _agentPermissions; +bool DomainServerSettingsManager::unpackPermissionsForKeypath(const QString& keyPath, + NodePermissionsMap* mapPointer, + std::function customUnpacker) { - _standardAgentPermissions.clear(); - _agentPermissions.clear(); + mapPointer->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); + QVariant* permissions = _configMap.valueForKeyPath(keyPath, true); + if (!permissions->canConvert(QMetaType::QVariantList)) { + qDebug() << "Failed to extract permissions for key path" << keyPath << "from settings."; (*permissions) = QVariantList(); } - QList 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; - } - } + bool needPack = false; QList 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; + + NodePermissionsKey idKey = perms->getKey(); + + if (mapPointer->contains(idKey)) { + qDebug() << "Duplicate name in permissions table for" << keyPath << " - " << id; + *((*mapPointer)[idKey]) |= *perms; needPack = true; } else { - _agentPermissions[id] = perms; + (*mapPointer)[idKey] = perms; + } + + if (customUnpacker) { + customUnpacker(perms); } } + return needPack; + +} + +void DomainServerSettingsManager::unpackPermissions() { + // transfer details from _configMap to _agentPermissions + + bool needPack = false; + + needPack |= unpackPermissionsForKeypath(AGENT_STANDARD_PERMISSIONS_KEYPATH, &_standardAgentPermissions); + + needPack |= unpackPermissionsForKeypath(AGENT_PERMISSIONS_KEYPATH, &_agentPermissions); + + needPack |= unpackPermissionsForKeypath(IP_PERMISSIONS_KEYPATH, &_ipPermissions, + [&](NodePermissionsPointer perms){ + // make sure that this permission row is for a valid IP address + if (QHostAddress(perms->getKey().first).isNull()) { + _ipPermissions.remove(perms->getKey()); + + // we removed a row from the IP permissions, we'll need a re-pack + needPack = true; + } + }); + + + needPack |= unpackPermissionsForKeypath(GROUP_PERMISSIONS_KEYPATH, &_groupPermissions, + [&](NodePermissionsPointer perms){ + if (perms->isGroup()) { + // the group-id was cached. hook-up the uuid in the uuid->group hash + _groupPermissionsByUUID[GroupByUUIDKey(perms->getGroupID(), perms->getRankID())] = _groupPermissions[perms->getKey()]; + needPack |= setGroupID(perms->getID(), perms->getGroupID()); + } + }); + + needPack |= unpackPermissionsForKeypath(GROUP_FORBIDDENS_KEYPATH, &_groupForbiddens, + [&](NodePermissionsPointer perms) { + if (perms->isGroup()) { + // the group-id was cached. hook-up the uuid in the uuid->group hash + _groupForbiddensByUUID[GroupByUUIDKey(perms->getGroupID(), perms->getRankID())] = _groupForbiddens[perms->getKey()]; + needPack |= setGroupID(perms->getID(), perms->getGroupID()); + } + }); + // 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; + foreach(const QString& standardName, NodePermissions::standardNames) { + NodePermissionsKey standardKey { standardName, 0 }; + if (!_standardAgentPermissions.contains(standardKey)) { + // we don't have permissions for one of the standard groups, so we'll add them now + NodePermissionsPointer perms { new NodePermissions(standardKey) }; + + // the localhost user is granted all permissions by default + if (standardKey == NodePermissions::standardNameLocalhost) { + perms->setAll(true); + } + + // add the permissions to the standard map + _standardAgentPermissions[standardKey] = perms; + + // this will require a packing of permissions + needPack = true; + } } + needPack |= ensurePermissionsForGroupRanks(); + if (needPack) { packPermissions(); } #ifdef WANT_DEBUG qDebug() << "--------------- permissions ---------------------"; - QList> permissionsSets; - permissionsSets << _standardAgentPermissions.get() << _agentPermissions.get(); + QList> permissionsSets; + permissionsSets << _standardAgentPermissions.get() << _agentPermissions.get() + << _groupPermissions.get() << _groupForbiddens.get() << _ipPermissions.get(); foreach (auto permissionSet, permissionsSets) { - QHashIterator i(permissionSet); + QHashIterator i(permissionSet); while (i.hasNext()) { i.next(); NodePermissionsPointer perms = i.value(); - qDebug() << i.key() << perms; + if (perms->isGroup()) { + qDebug() << i.key() << perms->getGroupID() << perms; + } else { + qDebug() << i.key() << perms; + } } } #endif } -NodePermissions DomainServerSettingsManager::getStandardPermissionsForName(const QString& name) const { +bool DomainServerSettingsManager::ensurePermissionsForGroupRanks() { + // make sure each rank in each group has its own set of permissions + bool changed = false; + QList permissionGroupIDs = getGroupIDs(); + foreach (QUuid groupID, permissionGroupIDs) { + QString groupName = _groupNames[groupID]; + QHash& ranksForGroup = _groupRanks[groupID]; + foreach (QUuid rankID, ranksForGroup.keys()) { + NodePermissionsKey nameKey = NodePermissionsKey(groupName, rankID); + GroupByUUIDKey idKey = GroupByUUIDKey(groupID, rankID); + NodePermissionsPointer perms; + if (_groupPermissions.contains(nameKey)) { + perms = _groupPermissions[nameKey]; + } else { + perms = NodePermissionsPointer(new NodePermissions(nameKey)); + _groupPermissions[nameKey] = perms; + changed = true; + } + if (perms->getGroupID() != groupID) { + perms->setGroupID(groupID); + changed = true; + } + if (perms->getRankID() != rankID) { + perms->setRankID(rankID); + changed = true; + } + _groupPermissionsByUUID[idKey] = perms; + } + } + + QList forbiddenGroupIDs = getBlacklistGroupIDs(); + foreach (QUuid groupID, forbiddenGroupIDs) { + QString groupName = _groupNames[groupID]; + QHash& ranksForGroup = _groupRanks[groupID]; + foreach (QUuid rankID, ranksForGroup.keys()) { + NodePermissionsKey nameKey = NodePermissionsKey(groupName, rankID); + GroupByUUIDKey idKey = GroupByUUIDKey(groupID, rankID); + NodePermissionsPointer perms; + if (_groupForbiddens.contains(nameKey)) { + perms = _groupForbiddens[nameKey]; + } else { + perms = NodePermissionsPointer(new NodePermissions(nameKey)); + _groupForbiddens[nameKey] = perms; + changed = true; + } + if (perms->getGroupID() != groupID) { + perms->setGroupID(groupID); + changed = true; + } + if (perms->getRankID() != rankID) { + perms->setRankID(rankID); + changed = true; + } + _groupForbiddensByUUID[idKey] = perms; + } + } + + return changed; +} + + +void DomainServerSettingsManager::processNodeKickRequestPacket(QSharedPointer message, SharedNodePointer sendingNode) { + // before we do any processing on this packet make sure it comes from a node that is allowed to kick + if (sendingNode->getCanKick()) { + // pull the UUID being kicked from the packet + QUuid nodeUUID = QUuid::fromRfc4122(message->readWithoutCopy(NUM_BYTES_RFC4122_UUID)); + + if (!nodeUUID.isNull() && nodeUUID != sendingNode->getUUID()) { + // make sure we actually have a node with this UUID + auto limitedNodeList = DependencyManager::get(); + + auto matchingNode = limitedNodeList->nodeWithUUID(nodeUUID); + + if (matchingNode) { + // we have a matching node, time to decide how to store updated permissions for this node + + NodePermissionsPointer destinationPermissions; + + auto verifiedUsername = matchingNode->getPermissions().getVerifiedUserName(); + + bool hadExistingPermissions = false; + + if (!verifiedUsername.isEmpty()) { + // if we have a verified user name for this user, we apply the kick to the username + + // check if there were already permissions + hadExistingPermissions = havePermissionsForName(verifiedUsername); + + // grab or create permissions for the given username + destinationPermissions = _agentPermissions[matchingNode->getPermissions().getKey()]; + } else { + // otherwise we apply the kick to the IP from active socket for this node + // (falling back to the public socket if not yet active) + auto& kickAddress = matchingNode->getActiveSocket() + ? matchingNode->getActiveSocket()->getAddress() + : matchingNode->getPublicSocket().getAddress(); + + NodePermissionsKey ipAddressKey(kickAddress.toString(), QUuid()); + + // check if there were already permissions for the IP + hadExistingPermissions = hasPermissionsForIP(kickAddress); + + // grab or create permissions for the given IP address + destinationPermissions = _ipPermissions[ipAddressKey]; + } + + // make sure we didn't already have existing permissions that disallowed connect + if (!hadExistingPermissions + || destinationPermissions->can(NodePermissions::Permission::canConnectToDomain)) { + + qDebug() << "Removing connect permission for node" << uuidStringWithoutCurlyBraces(matchingNode->getUUID()) + << "after kick request"; + + // ensure that the connect permission is clear + destinationPermissions->clear(NodePermissions::Permission::canConnectToDomain); + + // we've changed permissions, time to store them to disk and emit our signal to say they have changed + packPermissions(); + + emit updateNodePermissions(); + } else { + qWarning() << "Received kick request for node" << uuidStringWithoutCurlyBraces(matchingNode->getUUID()) + << "that already did not have permission to connect"; + + // in this case, though we don't expect the node to be connected to the domain, it is + // emit updateNodePermissions so that the DomainGatekeeper kicks it out + emit updateNodePermissions(); + } + + } else { + qWarning() << "Node kick request received for unknown node. Refusing to process."; + } + } else { + // this isn't a UUID we can use + qWarning() << "Node kick request received for invalid node ID or from node being kicked. Refusing to process."; + } + + } else { + qWarning() << "Refusing to process a kick packet from node" << uuidStringWithoutCurlyBraces(sendingNode->getUUID()) + << "that does not have kick permissions."; + } +} + +QStringList DomainServerSettingsManager::getAllNames() const { + QStringList result; + foreach (auto key, _agentPermissions.keys()) { + result << key.first.toLower(); + } + return result; +} + +NodePermissions DomainServerSettingsManager::getStandardPermissionsForName(const NodePermissionsKey& name) const { if (_standardAgentPermissions.contains(name)) { return *(_standardAgentPermissions[name].get()); } @@ -451,16 +732,70 @@ NodePermissions DomainServerSettingsManager::getStandardPermissionsForName(const } NodePermissions DomainServerSettingsManager::getPermissionsForName(const QString& name) const { - if (_agentPermissions.contains(name)) { - return *(_agentPermissions[name].get()); + NodePermissionsKey nameKey = NodePermissionsKey(name, 0); + if (_agentPermissions.contains(nameKey)) { + return *(_agentPermissions[nameKey].get()); } NodePermissions nullPermissions; nullPermissions.setAll(false); return nullPermissions; } +NodePermissions DomainServerSettingsManager::getPermissionsForIP(const QHostAddress& address) const { + NodePermissionsKey ipKey = NodePermissionsKey(address.toString(), 0); + if (_ipPermissions.contains(ipKey)) { + return *(_ipPermissions[ipKey].get()); + } + NodePermissions nullPermissions; + nullPermissions.setAll(false); + return nullPermissions; +} + +NodePermissions DomainServerSettingsManager::getPermissionsForGroup(const QString& groupName, QUuid rankID) const { + NodePermissionsKey groupRankKey = NodePermissionsKey(groupName, rankID); + if (_groupPermissions.contains(groupRankKey)) { + return *(_groupPermissions[groupRankKey].get()); + } + NodePermissions nullPermissions; + nullPermissions.setAll(false); + return nullPermissions; +} + +NodePermissions DomainServerSettingsManager::getPermissionsForGroup(const QUuid& groupID, QUuid rankID) const { + GroupByUUIDKey byUUIDKey = GroupByUUIDKey(groupID, rankID); + if (!_groupPermissionsByUUID.contains(byUUIDKey)) { + NodePermissions nullPermissions; + nullPermissions.setAll(false); + return nullPermissions; + } + NodePermissionsKey groupKey = _groupPermissionsByUUID[byUUIDKey]->getKey(); + return getPermissionsForGroup(groupKey.first, groupKey.second); +} + +NodePermissions DomainServerSettingsManager::getForbiddensForGroup(const QString& groupName, QUuid rankID) const { + NodePermissionsKey groupRankKey = NodePermissionsKey(groupName, rankID); + if (_groupForbiddens.contains(groupRankKey)) { + return *(_groupForbiddens[groupRankKey].get()); + } + NodePermissions allForbiddens; + allForbiddens.setAll(true); + return allForbiddens; +} + +NodePermissions DomainServerSettingsManager::getForbiddensForGroup(const QUuid& groupID, QUuid rankID) const { + GroupByUUIDKey byUUIDKey = GroupByUUIDKey(groupID, rankID); + if (!_groupForbiddensByUUID.contains(byUUIDKey)) { + NodePermissions allForbiddens; + allForbiddens.setAll(true); + return allForbiddens; + } + + NodePermissionsKey groupKey = _groupForbiddensByUUID[byUUIDKey]->getKey(); + return getForbiddensForGroup(groupKey.first, groupKey.second); +} + QVariant DomainServerSettingsManager::valueOrDefaultValueForKeyPath(const QString& keyPath) { - const QVariant* foundValue = valueForKeyPath(_configMap.getMergedConfig(), keyPath); + const QVariant* foundValue = _configMap.valueForKeyPath(keyPath); if (foundValue) { return *foundValue; @@ -518,8 +853,6 @@ bool DomainServerSettingsManager::handleAuthenticatedHTTPRequest(HTTPConnection QJsonDocument postedDocument = QJsonDocument::fromJson(connection->requestContent()); QJsonObject postedObject = postedDocument.object(); - qDebug() << "DomainServerSettingsManager postedObject -" << postedObject; - // we recurse one level deep below each group for the appropriate setting bool restartRequired = recurseJSONObjectAndOverwriteSettings(postedObject); @@ -536,6 +869,7 @@ bool DomainServerSettingsManager::handleAuthenticatedHTTPRequest(HTTPConnection QTimer::singleShot(DOMAIN_SERVER_RESTART_TIMER_MSECS, qApp, SLOT(restart())); } else { unpackPermissions(); + apiRefreshGroupInformation(); emit updateNodePermissions(); } @@ -544,13 +878,10 @@ bool DomainServerSettingsManager::handleAuthenticatedHTTPRequest(HTTPConnection // setup a JSON Object with descriptions and non-omitted settings const QString SETTINGS_RESPONSE_DESCRIPTION_KEY = "descriptions"; const QString SETTINGS_RESPONSE_VALUE_KEY = "values"; - const QString SETTINGS_RESPONSE_LOCKED_VALUES_KEY = "locked"; QJsonObject rootObject; rootObject[SETTINGS_RESPONSE_DESCRIPTION_KEY] = _descriptionArray; 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"); } @@ -595,13 +926,13 @@ QJsonObject DomainServerSettingsManager::responseObjectForType(const QString& ty QVariant variantValue; if (!groupKey.isEmpty()) { - QVariant settingsMapGroupValue = _configMap.getMergedConfig().value(groupKey); + QVariant settingsMapGroupValue = _configMap.value(groupKey); if (!settingsMapGroupValue.isNull()) { variantValue = settingsMapGroupValue.toMap().value(settingName); } } else { - variantValue = _configMap.getMergedConfig().value(settingName); + variantValue = _configMap.value(settingName); } QJsonValue result; @@ -730,7 +1061,8 @@ void DomainServerSettingsManager::updateSetting(const QString& key, const QJsonV sortPermissions(); } -QJsonObject DomainServerSettingsManager::settingDescriptionFromGroup(const QJsonObject& groupObject, const QString& settingName) { +QJsonObject DomainServerSettingsManager::settingDescriptionFromGroup(const QJsonObject& groupObject, + const QString& settingName) { foreach(const QJsonValue& settingValue, groupObject[DESCRIPTION_SETTINGS_KEY].toArray()) { QJsonObject settingObject = settingValue.toObject(); if (settingObject[DESCRIPTION_NAME_KEY].toString() == settingName) { @@ -742,7 +1074,7 @@ QJsonObject DomainServerSettingsManager::settingDescriptionFromGroup(const QJson } bool DomainServerSettingsManager::recurseJSONObjectAndOverwriteSettings(const QJsonObject& postedObject) { - auto& settingsVariant = _configMap.getUserConfig(); + auto& settingsVariant = _configMap.getConfig(); bool needRestart = false; // Iterate on the setting groups @@ -843,21 +1175,37 @@ bool permissionVariantLessThan(const QVariant &v1, const QVariant &v2) { !m2.contains("permissions_id")) { return v1.toString() < v2.toString(); } + + if (m1.contains("rank_order") && m2.contains("rank_order") && + m1["permissions_id"].toString() == m2["permissions_id"].toString()) { + return m1["rank_order"].toInt() < m2["rank_order"].toInt(); + } + 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); + QVariant* standardPermissions = _configMap.valueForKeyPath(AGENT_STANDARD_PERMISSIONS_KEYPATH); if (standardPermissions && standardPermissions->canConvert(QMetaType::QVariantList)) { QList* standardPermissionsList = reinterpret_cast(standardPermissions); std::sort((*standardPermissionsList).begin(), (*standardPermissionsList).end(), permissionVariantLessThan); } - QVariant* permissions = valueForKeyPath(_configMap.getUserConfig(), AGENT_PERMISSIONS_KEYPATH); + QVariant* permissions = _configMap.valueForKeyPath(AGENT_PERMISSIONS_KEYPATH); if (permissions && permissions->canConvert(QMetaType::QVariantList)) { QList* permissionsList = reinterpret_cast(permissions); std::sort((*permissionsList).begin(), (*permissionsList).end(), permissionVariantLessThan); } + QVariant* groupPermissions = _configMap.valueForKeyPath(GROUP_PERMISSIONS_KEYPATH); + if (groupPermissions && groupPermissions->canConvert(QMetaType::QVariantList)) { + QList* permissionsList = reinterpret_cast(groupPermissions); + std::sort((*permissionsList).begin(), (*permissionsList).end(), permissionVariantLessThan); + } + QVariant* forbiddenPermissions = _configMap.valueForKeyPath(GROUP_FORBIDDENS_KEYPATH); + if (forbiddenPermissions && forbiddenPermissions->canConvert(QMetaType::QVariantList)) { + QList* permissionsList = reinterpret_cast(forbiddenPermissions); + std::sort((*permissionsList).begin(), (*permissionsList).end(), permissionVariantLessThan); + } } void DomainServerSettingsManager::persistToFile() { @@ -873,8 +1221,338 @@ void DomainServerSettingsManager::persistToFile() { QFile settingsFile(_configMap.getUserConfigFilename()); if (settingsFile.open(QIODevice::WriteOnly)) { - settingsFile.write(QJsonDocument::fromVariant(_configMap.getUserConfig()).toJson()); + settingsFile.write(QJsonDocument::fromVariant(_configMap.getConfig()).toJson()); } else { qCritical("Could not write to JSON settings file. Unable to persist settings."); + + // failed to write, reload whatever the current config state is + _configMap.loadConfig(_argumentList); + } +} + +QStringList DomainServerSettingsManager::getAllKnownGroupNames() { + // extract all the group names from the group-permissions and group-forbiddens settings + QSet result; + + QHashIterator i(_groupPermissions.get()); + while (i.hasNext()) { + i.next(); + NodePermissionsKey key = i.key(); + result += key.first; + } + + QHashIterator j(_groupForbiddens.get()); + while (j.hasNext()) { + j.next(); + NodePermissionsKey key = j.key(); + result += key.first; + } + + return result.toList(); +} + +bool DomainServerSettingsManager::setGroupID(const QString& groupName, const QUuid& groupID) { + bool changed = false; + _groupIDs[groupName.toLower()] = groupID; + _groupNames[groupID] = groupName; + + QHashIterator i(_groupPermissions.get()); + while (i.hasNext()) { + i.next(); + NodePermissionsPointer perms = i.value(); + if (perms->getID().toLower() == groupName.toLower() && !perms->isGroup()) { + changed = true; + perms->setGroupID(groupID); + } + } + + QHashIterator j(_groupForbiddens.get()); + while (j.hasNext()) { + j.next(); + NodePermissionsPointer perms = j.value(); + if (perms->getID().toLower() == groupName.toLower() && !perms->isGroup()) { + changed = true; + perms->setGroupID(groupID); + } + } + + return changed; +} + +void DomainServerSettingsManager::apiRefreshGroupInformation() { + if (!DependencyManager::get()->hasAuthEndpoint()) { + // can't yet. + return; + } + + bool changed = false; + + QStringList groupNames = getAllKnownGroupNames(); + foreach (QString groupName, groupNames) { + QString lowerGroupName = groupName.toLower(); + if (_groupIDs.contains(lowerGroupName)) { + // we already know about this one. recall setGroupID in case the group has been + // added to another section (the same group is found in both groups and blacklists). + changed = setGroupID(groupName, _groupIDs[lowerGroupName]); + continue; + } + apiGetGroupID(groupName); + } + + foreach (QUuid groupID, _groupNames.keys()) { + apiGetGroupRanks(groupID); + } + + changed |= ensurePermissionsForGroupRanks(); + + if (changed) { + packPermissions(); + } + + unpackPermissions(); +} + +void DomainServerSettingsManager::apiGetGroupID(const QString& groupName) { + JSONCallbackParameters callbackParams; + callbackParams.jsonCallbackReceiver = this; + callbackParams.jsonCallbackMethod = "apiGetGroupIDJSONCallback"; + callbackParams.errorCallbackReceiver = this; + callbackParams.errorCallbackMethod = "apiGetGroupIDErrorCallback"; + + const QString GET_GROUP_ID_PATH = "api/v1/groups/names/%1"; + DependencyManager::get()->sendRequest(GET_GROUP_ID_PATH.arg(groupName), + AccountManagerAuth::Required, + QNetworkAccessManager::GetOperation, callbackParams); +} + +void DomainServerSettingsManager::apiGetGroupIDJSONCallback(QNetworkReply& requestReply) { + // { + // "data":{ + // "groups":[{ + // "description":null, + // "id":"fd55479a-265d-4990-854e-3d04214ad1b0", + // "is_list":false, + // "membership":{ + // "permissions":{ + // "custom_1=":false, + // "custom_2=":false, + // "custom_3=":false, + // "custom_4=":false, + // "del_group=":true, + // "invite_member=":true, + // "kick_member=":true, + // "list_members=":true, + // "mv_group=":true, + // "query_members=":true, + // "rank_member=":true + // }, + // "rank":{ + // "name=":"owner", + // "order=":0 + // } + // }, + // "name":"Blerg Blah" + // }] + // }, + // "status":"success" + // } + QJsonObject jsonObject = QJsonDocument::fromJson(requestReply.readAll()).object(); + if (jsonObject["status"].toString() == "success") { + QJsonArray groups = jsonObject["data"].toObject()["groups"].toArray(); + for (int i = 0; i < groups.size(); i++) { + QJsonObject group = groups.at(i).toObject(); + QString groupName = group["name"].toString(); + QUuid groupID = QUuid(group["id"].toString()); + + bool changed = setGroupID(groupName, groupID); + if (changed) { + packPermissions(); + apiGetGroupRanks(groupID); + } + } + } else { + qDebug() << "getGroupID api call returned:" << QJsonDocument(jsonObject).toJson(QJsonDocument::Compact); + } +} + +void DomainServerSettingsManager::apiGetGroupIDErrorCallback(QNetworkReply& requestReply) { + qDebug() << "******************** getGroupID api call failed:" << requestReply.error(); +} + +void DomainServerSettingsManager::apiGetGroupRanks(const QUuid& groupID) { + JSONCallbackParameters callbackParams; + callbackParams.jsonCallbackReceiver = this; + callbackParams.jsonCallbackMethod = "apiGetGroupRanksJSONCallback"; + callbackParams.errorCallbackReceiver = this; + callbackParams.errorCallbackMethod = "apiGetGroupRanksErrorCallback"; + + const QString GET_GROUP_RANKS_PATH = "api/v1/groups/%1/ranks"; + DependencyManager::get()->sendRequest(GET_GROUP_RANKS_PATH.arg(groupID.toString().mid(1,36)), + AccountManagerAuth::Required, + QNetworkAccessManager::GetOperation, callbackParams); +} + +void DomainServerSettingsManager::apiGetGroupRanksJSONCallback(QNetworkReply& requestReply) { + // { + // "data":{ + // "groups":{ + // "d3500f49-0655-4b1b-9846-ff8dd1b03351":{ + // "members_count":1, + // "ranks":[ + // { + // "id":"7979b774-e7f8-436c-9df1-912f1019f32f", + // "members_count":1, + // "name":"owner", + // "order":0, + // "permissions":{ + // "custom_1":false, + // "custom_2":false, + // "custom_3":false, + // "custom_4":false, + // "edit_group":true, + // "edit_member":true, + // "edit_rank":true, + // "list_members":true, + // "list_permissions":true, + // "list_ranks":true, + // "query_member":true + // } + // } + // ] + // } + // } + // },"status":"success" + // } + + bool changed = false; + QJsonObject jsonObject = QJsonDocument::fromJson(requestReply.readAll()).object(); + + if (jsonObject["status"].toString() == "success") { + QJsonObject groups = jsonObject["data"].toObject()["groups"].toObject(); + foreach (auto groupID, groups.keys()) { + QJsonObject group = groups[groupID].toObject(); + QJsonArray ranks = group["ranks"].toArray(); + + QHash& ranksForGroup = _groupRanks[groupID]; + QHash idsFromThisUpdate; + + for (int rankIndex = 0; rankIndex < ranks.size(); rankIndex++) { + QJsonObject rank = ranks[rankIndex].toObject(); + + QUuid rankID = QUuid(rank["id"].toString()); + int rankOrder = rank["order"].toInt(); + QString rankName = rank["name"].toString(); + int rankMembersCount = rank["members_count"].toInt(); + + GroupRank groupRank(rankID, rankOrder, rankName, rankMembersCount); + + if (ranksForGroup[rankID] != groupRank) { + ranksForGroup[rankID] = groupRank; + changed = true; + } + + idsFromThisUpdate[rankID] = true; + } + + // clean up any that went away + foreach (QUuid rankID, ranksForGroup.keys()) { + if (!idsFromThisUpdate.contains(rankID)) { + ranksForGroup.remove(rankID); + } + } + } + + changed |= ensurePermissionsForGroupRanks(); + if (changed) { + packPermissions(); + } + } else { + qDebug() << "getGroupRanks api call returned:" << QJsonDocument(jsonObject).toJson(QJsonDocument::Compact); + } +} + +void DomainServerSettingsManager::apiGetGroupRanksErrorCallback(QNetworkReply& requestReply) { + qDebug() << "******************** getGroupRanks api call failed:" << requestReply.error(); +} + +void DomainServerSettingsManager::recordGroupMembership(const QString& name, const QUuid groupID, QUuid rankID) { + if (rankID != QUuid()) { + _groupMembership[name][groupID] = rankID; + } else { + _groupMembership[name].remove(groupID); + } +} + +QUuid DomainServerSettingsManager::isGroupMember(const QString& name, const QUuid& groupID) { + const QHash& groupsForName = _groupMembership[name]; + if (groupsForName.contains(groupID)) { + return groupsForName[groupID]; + } + return QUuid(); +} + +QList DomainServerSettingsManager::getGroupIDs() { + QSet result; + foreach (NodePermissionsKey groupKey, _groupPermissions.keys()) { + if (_groupPermissions[groupKey]->isGroup()) { + result += _groupPermissions[groupKey]->getGroupID(); + } + } + return result.toList(); +} + +QList DomainServerSettingsManager::getBlacklistGroupIDs() { + QSet result; + foreach (NodePermissionsKey groupKey, _groupForbiddens.keys()) { + if (_groupForbiddens[groupKey]->isGroup()) { + result += _groupForbiddens[groupKey]->getGroupID(); + } + } + return result.toList(); +} + +void DomainServerSettingsManager::debugDumpGroupsState() { + qDebug() << "--------- GROUPS ---------"; + + qDebug() << "_groupPermissions:"; + foreach (NodePermissionsKey groupKey, _groupPermissions.keys()) { + NodePermissionsPointer perms = _groupPermissions[groupKey]; + qDebug() << "| " << groupKey << perms; + } + + qDebug() << "_groupForbiddens:"; + foreach (NodePermissionsKey groupKey, _groupForbiddens.keys()) { + NodePermissionsPointer perms = _groupForbiddens[groupKey]; + qDebug() << "| " << groupKey << perms; + } + + qDebug() << "_groupIDs:"; + foreach (QString groupName, _groupIDs.keys()) { + qDebug() << "| " << groupName << "==>" << _groupIDs[groupName]; + } + + qDebug() << "_groupNames:"; + foreach (QUuid groupID, _groupNames.keys()) { + qDebug() << "| " << groupID << "==>" << _groupNames[groupID]; + } + + qDebug() << "_groupRanks:"; + foreach (QUuid groupID, _groupRanks.keys()) { + QHash& ranksForGroup = _groupRanks[groupID]; + qDebug() << "| " << groupID; + foreach (QUuid rankID, ranksForGroup.keys()) { + QString rankName = ranksForGroup[rankID].name; + qDebug() << "| " << rankID << rankName; + } + } + + qDebug() << "_groupMembership"; + foreach (QString userName, _groupMembership.keys()) { + QHash& groupsForUser = _groupMembership[userName]; + QString line = ""; + foreach (QUuid groupID, groupsForUser.keys()) { + line += " g=" + groupID.toString() + ",r=" + groupsForUser[groupID].toString(); + } + qDebug() << "| " << userName << line; } } diff --git a/domain-server/src/DomainServerSettingsManager.h b/domain-server/src/DomainServerSettingsManager.h index 66f1a83500..144589326c 100644 --- a/domain-server/src/DomainServerSettingsManager.h +++ b/domain-server/src/DomainServerSettingsManager.h @@ -27,6 +27,12 @@ const QString SETTINGS_PATH = "/settings"; const QString SETTINGS_PATH_JSON = SETTINGS_PATH + ".json"; const QString AGENT_STANDARD_PERMISSIONS_KEYPATH = "security.standard_permissions"; const QString AGENT_PERMISSIONS_KEYPATH = "security.permissions"; +const QString IP_PERMISSIONS_KEYPATH = "security.ip_permissions"; +const QString GROUP_PERMISSIONS_KEYPATH = "security.group_permissions"; +const QString GROUP_FORBIDDENS_KEYPATH = "security.group_forbiddens"; + +using GroupByUUIDKey = QPair; // groupID, rankID + class DomainServerSettingsManager : public QObject { Q_OBJECT @@ -38,23 +44,67 @@ public: void setupConfigMap(const QStringList& argumentList); QVariant valueOrDefaultValueForKeyPath(const QString& keyPath); - QVariantMap& getUserSettingsMap() { return _configMap.getUserConfig(); } - QVariantMap& getSettingsMap() { return _configMap.getMergedConfig(); } + QVariantMap& getSettingsMap() { return _configMap.getConfig(); } QVariantMap& getDescriptorsMap(); - 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; + // these give access to anonymous/localhost/logged-in settings from the domain-server settings page + bool haveStandardPermissionsForName(const QString& name) const { return _standardAgentPermissions.contains(name, 0); } + NodePermissions getStandardPermissionsForName(const NodePermissionsKey& name) const; + + // these give access to permissions for specific user-names from the domain-server settings page + bool havePermissionsForName(const QString& name) const { return _agentPermissions.contains(name, 0); } NodePermissions getPermissionsForName(const QString& name) const; - QStringList getAllNames() { return _agentPermissions.keys(); } + NodePermissions getPermissionsForName(const NodePermissionsKey& key) const { return getPermissionsForName(key.first); } + QStringList getAllNames() const; + + // these give access to permissions for specific IPs from the domain-server settings page + bool hasPermissionsForIP(const QHostAddress& address) const { return _ipPermissions.contains(address.toString(), 0); } + NodePermissions getPermissionsForIP(const QHostAddress& address) const; + + // these give access to permissions for specific groups from the domain-server settings page + bool havePermissionsForGroup(const QString& groupName, QUuid rankID) const { + return _groupPermissions.contains(groupName, rankID); + } + NodePermissions getPermissionsForGroup(const QString& groupName, QUuid rankID) const; + NodePermissions getPermissionsForGroup(const QUuid& groupID, QUuid rankID) const; + + // these remove permissions from users in certain groups + bool haveForbiddensForGroup(const QString& groupName, QUuid rankID) const { + return _groupForbiddens.contains(groupName, rankID); + } + NodePermissions getForbiddensForGroup(const QString& groupName, QUuid rankID) const; + NodePermissions getForbiddensForGroup(const QUuid& groupID, QUuid rankID) const; + + QStringList getAllKnownGroupNames(); + bool setGroupID(const QString& groupName, const QUuid& groupID); + GroupRank getGroupRank(QUuid groupID, QUuid rankID) { return _groupRanks[groupID][rankID]; } + + QList getGroupIDs(); + QList getBlacklistGroupIDs(); + + // these are used to locally cache the result of calling "api/v1/groups/.../is_member/..." on metaverse's api + void clearGroupMemberships(const QString& name) { _groupMembership[name].clear(); } + void recordGroupMembership(const QString& name, const QUuid groupID, QUuid rankID); + QUuid isGroupMember(const QString& name, const QUuid& groupID); // returns rank or -1 if not a member + + // calls http api to refresh group information + void apiRefreshGroupInformation(); + + void debugDumpGroupsState(); signals: void updateNodePermissions(); +public slots: + void apiGetGroupIDJSONCallback(QNetworkReply& requestReply); + void apiGetGroupIDErrorCallback(QNetworkReply& requestReply); + void apiGetGroupRanksJSONCallback(QNetworkReply& requestReply); + void apiGetGroupRanksErrorCallback(QNetworkReply& requestReply); private slots: void processSettingsRequestPacket(QSharedPointer message); + void processNodeKickRequestPacket(QSharedPointer message, SharedNodePointer sendingNode); private: QStringList _argumentList; @@ -76,11 +126,37 @@ private: void validateDescriptorsMap(); - void packPermissionsForMap(QString mapName, NodePermissionsMap& agentPermissions, QString keyPath); + // these cause calls to metaverse's group api + void apiGetGroupID(const QString& groupName); + void apiGetGroupRanks(const QUuid& groupID); + + void initializeGroupPermissions(NodePermissionsMap& permissionsRows, QString groupName, NodePermissionsPointer perms); + void packPermissionsForMap(QString mapName, NodePermissionsMap& permissionsRows, QString keyPath); void packPermissions(); void unpackPermissions(); - NodePermissionsMap _standardAgentPermissions; // anonymous, logged-in, localhost + bool unpackPermissionsForKeypath(const QString& keyPath, NodePermissionsMap* destinationMapPointer, + std::function customUnpacker = {}); + bool ensurePermissionsForGroupRanks(); + + NodePermissionsMap _standardAgentPermissions; // anonymous, logged-in, localhost, friend-of-domain-owner NodePermissionsMap _agentPermissions; // specific account-names + + NodePermissionsMap _ipPermissions; // permissions granted by node IP address + + NodePermissionsMap _groupPermissions; // permissions granted by membership to specific groups + NodePermissionsMap _groupForbiddens; // permissions denied due to membership in a specific group + // these are like _groupPermissions and _groupForbiddens but with uuids rather than group-names in the keys + QHash _groupPermissionsByUUID; + QHash _groupForbiddensByUUID; + + QHash _groupIDs; // keep track of group-name to group-id mappings + QHash _groupNames; // keep track of group-id to group-name mappings + + // remember the responses to api/v1/groups/%1/ranks + QHash> _groupRanks; // QHash> + + // keep track of answers to api queries about which users are in which groups + QHash> _groupMembership; // QHash> }; #endif // hifi_DomainServerSettingsManager_h diff --git a/interface/resources/qml/QmlWebWindow.qml b/interface/resources/qml/QmlWebWindow.qml index 542b44b95e..153498e2f7 100644 --- a/interface/resources/qml/QmlWebWindow.qml +++ b/interface/resources/qml/QmlWebWindow.qml @@ -39,6 +39,23 @@ Windows.ScrollingWindow { // missing signal signal sendToScript(var message); + signal moved(vector2d position); + signal resized(size size); + + function notifyMoved() { + moved(Qt.vector2d(x, y)); + } + + function notifyResized() { + resized(Qt.size(width, height)); + } + + onXChanged: notifyMoved(); + onYChanged: notifyMoved(); + + onWidthChanged: notifyResized(); + onHeightChanged: notifyResized(); + Item { width: pane.contentWidth implicitHeight: pane.scrollHeight diff --git a/interface/resources/qml/controls-uit/WebView.qml b/interface/resources/qml/controls-uit/WebView.qml index 4c165fc587..b599e29fe0 100644 --- a/interface/resources/qml/controls-uit/WebView.qml +++ b/interface/resources/qml/controls-uit/WebView.qml @@ -35,7 +35,6 @@ WebEngineView { } onUrlChanged: { - console.log("Url changed to " + url); var originalUrl = url.toString(); newUrl = urlHandler.fixupUrl(originalUrl).toString(); if (newUrl !== originalUrl) { diff --git a/interface/resources/qml/controls/WebView.qml b/interface/resources/qml/controls/WebView.qml index 2f94740fe6..bba91c64f6 100644 --- a/interface/resources/qml/controls/WebView.qml +++ b/interface/resources/qml/controls/WebView.qml @@ -26,7 +26,6 @@ WebEngineView { } onUrlChanged: { - console.log("Url changed to " + url); var originalUrl = url.toString(); newUrl = urlHandler.fixupUrl(originalUrl).toString(); if (newUrl !== originalUrl) { diff --git a/interface/resources/qml/dialogs/FileDialog.qml b/interface/resources/qml/dialogs/FileDialog.qml index fa5be18cd3..6a37886cb3 100644 --- a/interface/resources/qml/dialogs/FileDialog.qml +++ b/interface/resources/qml/dialogs/FileDialog.qml @@ -87,6 +87,15 @@ ModalWindow { currentSelection.text = d.capitalizeDrive(helper.urlToPath(initialFolder)); } + helper.contentsChanged.connect(function() { + if (folderListModel) { + // Make folderListModel refresh. + var save = folderListModel.folder; + folderListModel.folder = ""; + folderListModel.folder = save; + } + }); + fileTableView.forceActiveFocus(); } @@ -343,12 +352,14 @@ ModalWindow { onFolderChanged: { if (folder === rootFolder) { model = driveListModel; + helper.monitorDirectory(""); update(); } else { var needsUpdate = model === driveListModel && folder === folderListModel.folder; model = folderListModel; folderListModel.folder = folder; + helper.monitorDirectory(helper.urlToPath(folder)); if (needsUpdate) { update(); diff --git a/interface/resources/qml/windows/DefaultFrameDecoration.qml b/interface/resources/qml/windows/DefaultFrameDecoration.qml index ce47b818b1..40e32aaa6b 100644 --- a/interface/resources/qml/windows/DefaultFrameDecoration.qml +++ b/interface/resources/qml/windows/DefaultFrameDecoration.qml @@ -78,7 +78,10 @@ Decoration { id: closeClickArea anchors.fill: parent hoverEnabled: true - onClicked: window.shown = false; + onClicked: { + window.shown = false; + window.windowClosed(); + } } } } diff --git a/interface/resources/qml/windows/Window.qml b/interface/resources/qml/windows/Window.qml index c873872692..40ef74c59b 100644 --- a/interface/resources/qml/windows/Window.qml +++ b/interface/resources/qml/windows/Window.qml @@ -30,6 +30,7 @@ Fadable { // // Signals // + signal windowClosed(); signal windowDestroyed(); signal mouseEntered(); signal mouseExited(); diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp index 4e8ecf3054..564a58708f 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.cpp +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.cpp @@ -916,6 +916,16 @@ bool RenderableModelEntityItem::contains(const glm::vec3& point) const { return false; } +bool RenderableModelEntityItem::shouldBePhysical() const { + // If we have a model, make sure it hasn't failed to download. + // If it has, we'll report back that we shouldn't be physical so that physics aren't held waiting for us to be ready. + if (_model && _model->didGeometryRequestFail()) { + return false; + } else { + return ModelEntityItem::shouldBePhysical(); + } +} + glm::quat RenderableModelEntityItem::getAbsoluteJointRotationInObjectFrame(int index) const { if (_model) { glm::quat result; diff --git a/libraries/entities-renderer/src/RenderableModelEntityItem.h b/libraries/entities-renderer/src/RenderableModelEntityItem.h index 339c907532..f487e79880 100644 --- a/libraries/entities-renderer/src/RenderableModelEntityItem.h +++ b/libraries/entities-renderer/src/RenderableModelEntityItem.h @@ -65,6 +65,8 @@ public: virtual bool contains(const glm::vec3& point) const override; + virtual bool shouldBePhysical() const override; + // these are in the frame of this object (model space) virtual glm::quat getAbsoluteJointRotationInObjectFrame(int index) const override; virtual glm::vec3 getAbsoluteJointTranslationInObjectFrame(int index) const override; diff --git a/libraries/model-networking/src/model-networking/ModelCache.cpp b/libraries/model-networking/src/model-networking/ModelCache.cpp index 26798070a6..c4b2a6dd22 100644 --- a/libraries/model-networking/src/model-networking/ModelCache.cpp +++ b/libraries/model-networking/src/model-networking/ModelCache.cpp @@ -403,6 +403,8 @@ void GeometryResourceWatcher::setResource(GeometryResource::Pointer resource) { void GeometryResourceWatcher::resourceFinished(bool success) { if (success) { _geometryRef = std::make_shared(*_resource); + } else { + emit resourceFailed(); } } diff --git a/libraries/model-networking/src/model-networking/ModelCache.h b/libraries/model-networking/src/model-networking/ModelCache.h index 4a0a921a04..62037d67bc 100644 --- a/libraries/model-networking/src/model-networking/ModelCache.h +++ b/libraries/model-networking/src/model-networking/ModelCache.h @@ -111,6 +111,9 @@ public: QUrl getURL() const { return (bool)_resource ? _resource->getURL() : QUrl(); } +signals: + void resourceFailed(); + private: void startWatching(); void stopWatching(); diff --git a/libraries/networking/src/AccountManager.cpp b/libraries/networking/src/AccountManager.cpp index ed325b8d69..c4bfae7cac 100644 --- a/libraries/networking/src/AccountManager.cpp +++ b/libraries/networking/src/AccountManager.cpp @@ -200,8 +200,9 @@ void AccountManager::sendRequest(const QString& path, const JSONCallbackParameters& callbackParams, const QByteArray& dataByteArray, QHttpMultiPart* dataMultiPart, - const QVariantMap& propertyMap) { - + const QVariantMap& propertyMap, + QUrlQuery query) { + if (thread() != QThread::currentThread()) { QMetaObject::invokeMethod(this, "sendRequest", Q_ARG(const QString&, path), @@ -213,9 +214,9 @@ void AccountManager::sendRequest(const QString& path, Q_ARG(QVariantMap, propertyMap)); return; } - + QNetworkAccessManager& networkAccessManager = NetworkAccessManager::getInstance(); - + QNetworkRequest networkRequest; networkRequest.setHeader(QNetworkRequest::UserAgentHeader, _userAgentGetter()); @@ -224,13 +225,17 @@ void AccountManager::sendRequest(const QString& path, uuidStringWithoutCurlyBraces(_sessionID).toLocal8Bit()); QUrl requestURL = _authURL; - + if (path.startsWith("/")) { requestURL.setPath(path); } else { requestURL.setPath("/" + path); } - + + if (!query.isEmpty()) { + requestURL.setQuery(query); + } + if (authType != AccountManagerAuth::None ) { if (hasValidAccessToken()) { networkRequest.setRawHeader(ACCESS_TOKEN_AUTHORIZATION_HEADER, @@ -241,22 +246,21 @@ void AccountManager::sendRequest(const QString& path, << path << "that requires authentication"; return; } - } } - + networkRequest.setUrl(requestURL); - + if (VERBOSE_HTTP_REQUEST_DEBUGGING) { qCDebug(networking) << "Making a request to" << qPrintable(requestURL.toString()); - + if (!dataByteArray.isEmpty()) { qCDebug(networking) << "The POST/PUT body -" << QString(dataByteArray); } } - + QNetworkReply* networkReply = NULL; - + switch (operation) { case QNetworkAccessManager::GetOperation: networkReply = networkAccessManager.get(networkRequest); @@ -269,7 +273,7 @@ void AccountManager::sendRequest(const QString& path, } else { networkReply = networkAccessManager.put(networkRequest, dataMultiPart); } - + // make sure dataMultiPart is destroyed when the reply is connect(networkReply, &QNetworkReply::destroyed, dataMultiPart, &QHttpMultiPart::deleteLater); } else { @@ -280,7 +284,7 @@ void AccountManager::sendRequest(const QString& path, networkReply = networkAccessManager.put(networkRequest, dataByteArray); } } - + break; case QNetworkAccessManager::DeleteOperation: networkReply = networkAccessManager.sendCustomRequest(networkRequest, "DELETE"); @@ -289,7 +293,7 @@ void AccountManager::sendRequest(const QString& path, // other methods not yet handled break; } - + if (networkReply) { if (!propertyMap.isEmpty()) { // we have properties to set on the reply so the user can check them after @@ -297,18 +301,18 @@ void AccountManager::sendRequest(const QString& path, networkReply->setProperty(qPrintable(propertyKey), propertyMap.value(propertyKey)); } } - - + + if (!callbackParams.isEmpty()) { // if we have information for a callback, insert the callbackParams into our local map _pendingCallbackMap.insert(networkReply, callbackParams); - + if (callbackParams.updateReciever && !callbackParams.updateSlot.isEmpty()) { callbackParams.updateReciever->connect(networkReply, SIGNAL(uploadProgress(qint64, qint64)), callbackParams.updateSlot.toStdString().c_str()); } } - + // if we ended up firing of a request, hook up to it now connect(networkReply, SIGNAL(finished()), SLOT(processReply())); } diff --git a/libraries/networking/src/AccountManager.h b/libraries/networking/src/AccountManager.h index c12f663d3e..846cdb6220 100644 --- a/libraries/networking/src/AccountManager.h +++ b/libraries/networking/src/AccountManager.h @@ -16,6 +16,7 @@ #include #include #include +#include #include "NetworkAccessManager.h" @@ -67,7 +68,8 @@ public: const JSONCallbackParameters& callbackParams = JSONCallbackParameters(), const QByteArray& dataByteArray = QByteArray(), QHttpMultiPart* dataMultiPart = NULL, - const QVariantMap& propertyMap = QVariantMap()); + const QVariantMap& propertyMap = QVariantMap(), + QUrlQuery query = QUrlQuery()); void setIsAgent(bool isAgent) { _isAgent = isAgent; } diff --git a/libraries/networking/src/GroupRank.h b/libraries/networking/src/GroupRank.h new file mode 100644 index 0000000000..92dadfd220 --- /dev/null +++ b/libraries/networking/src/GroupRank.h @@ -0,0 +1,36 @@ +// +// GroupRank.h +// libraries/networking/src/ +// +// Created by Seth Alves on 2016-7-21. +// 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_GroupRank_h +#define hifi_GroupRank_h + +class GroupRank { +public: + GroupRank() {} + GroupRank(QUuid id, unsigned int order, QString name, unsigned int membersCount) : + id(id), order(order), name(name), membersCount(membersCount) {} + + QUuid id; + int order { -1 }; + QString name; + int membersCount { -1 }; +}; + +inline bool operator==(const GroupRank& lhs, const GroupRank& rhs) { + return + lhs.id == rhs.id && + lhs.order == rhs.order && + lhs.name == rhs.name && + lhs.membersCount == rhs.membersCount; +} +inline bool operator!=(const GroupRank& lhs, const GroupRank& rhs) { return !(lhs == rhs); } + +#endif // hifi_GroupRank_h diff --git a/libraries/networking/src/LimitedNodeList.cpp b/libraries/networking/src/LimitedNodeList.cpp index d83046bc1b..431d372089 100644 --- a/libraries/networking/src/LimitedNodeList.cpp +++ b/libraries/networking/src/LimitedNodeList.cpp @@ -135,17 +135,25 @@ void LimitedNodeList::setPermissions(const NodePermissions& newPermissions) { _permissions = newPermissions; - if (originalPermissions.canAdjustLocks != newPermissions.canAdjustLocks) { - emit isAllowedEditorChanged(_permissions.canAdjustLocks); + if (originalPermissions.can(NodePermissions::Permission::canAdjustLocks) != + newPermissions.can(NodePermissions::Permission::canAdjustLocks)) { + emit isAllowedEditorChanged(_permissions.can(NodePermissions::Permission::canAdjustLocks)); } - if (originalPermissions.canRezPermanentEntities != newPermissions.canRezPermanentEntities) { - emit canRezChanged(_permissions.canRezPermanentEntities); + if (originalPermissions.can(NodePermissions::Permission::canRezPermanentEntities) != + newPermissions.can(NodePermissions::Permission::canRezPermanentEntities)) { + emit canRezChanged(_permissions.can(NodePermissions::Permission::canRezPermanentEntities)); } - if (originalPermissions.canRezTemporaryEntities != newPermissions.canRezTemporaryEntities) { - emit canRezTmpChanged(_permissions.canRezTemporaryEntities); + if (originalPermissions.can(NodePermissions::Permission::canRezTemporaryEntities) != + newPermissions.can(NodePermissions::Permission::canRezTemporaryEntities)) { + emit canRezTmpChanged(_permissions.can(NodePermissions::Permission::canRezTemporaryEntities)); } - if (originalPermissions.canWriteToAssetServer != newPermissions.canWriteToAssetServer) { - emit canWriteAssetsChanged(_permissions.canWriteToAssetServer); + if (originalPermissions.can(NodePermissions::Permission::canWriteToAssetServer) != + newPermissions.can(NodePermissions::Permission::canWriteToAssetServer)) { + emit canWriteAssetsChanged(_permissions.can(NodePermissions::Permission::canWriteToAssetServer)); + } + if (originalPermissions.can(NodePermissions::Permission::canKick) != + newPermissions.can(NodePermissions::Permission::canKick)) { + emit canKickChanged(_permissions.can(NodePermissions::Permission::canKick)); } } diff --git a/libraries/networking/src/LimitedNodeList.h b/libraries/networking/src/LimitedNodeList.h index d599fbcc37..48379b5e39 100644 --- a/libraries/networking/src/LimitedNodeList.h +++ b/libraries/networking/src/LimitedNodeList.h @@ -106,10 +106,11 @@ public: void setSessionUUID(const QUuid& sessionUUID); 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 isAllowedEditor() const { return _permissions.can(NodePermissions::Permission::canAdjustLocks); } + bool getThisNodeCanRez() const { return _permissions.can(NodePermissions::Permission::canRezPermanentEntities); } + bool getThisNodeCanRezTmp() const { return _permissions.can(NodePermissions::Permission::canRezTemporaryEntities); } + bool getThisNodeCanWriteAssets() const { return _permissions.can(NodePermissions::Permission::canWriteToAssetServer); } + bool getThisNodeCanKick() const { return _permissions.can(NodePermissions::Permission::canKick); } quint16 getSocketLocalPort() const { return _nodeSocket.localPort(); } QUdpSocket& getDTLSSocket(); @@ -258,6 +259,7 @@ signals: void canRezChanged(bool canRez); void canRezTmpChanged(bool canRezTmp); void canWriteAssetsChanged(bool canWriteAssets); + void canKickChanged(bool canKick); protected slots: void connectedForLocalSocketTest(); diff --git a/libraries/networking/src/Node.h b/libraries/networking/src/Node.h index 469a0c9755..18088c6cea 100644 --- a/libraries/networking/src/Node.h +++ b/libraries/networking/src/Node.h @@ -65,10 +65,11 @@ public: 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; } + bool isAllowedEditor() const { return _permissions.can(NodePermissions::Permission::canAdjustLocks); } + bool getCanRez() const { return _permissions.can(NodePermissions::Permission::canRezPermanentEntities); } + bool getCanRezTmp() const { return _permissions.can(NodePermissions::Permission::canRezTemporaryEntities); } + bool getCanWriteToAssetServer() const { return _permissions.can(NodePermissions::Permission::canWriteToAssetServer); } + bool getCanKick() const { return _permissions.can(NodePermissions::Permission::canKick); } void parseIgnoreRequestMessage(QSharedPointer message); void addIgnoredNode(const QUuid& otherNodeID); diff --git a/libraries/networking/src/NodeList.cpp b/libraries/networking/src/NodeList.cpp index a73537aad0..781cc00c1c 100644 --- a/libraries/networking/src/NodeList.cpp +++ b/libraries/networking/src/NodeList.cpp @@ -283,12 +283,7 @@ void NodeList::sendDomainServerCheckIn() { auto accountManager = DependencyManager::get(); const QUuid& connectionToken = _domainHandler.getConnectionToken(); - // we assume that we're on the same box as the DS if it has the same local address and - // it didn't present us with a connection token to use for username signature - bool localhostDomain = _domainHandler.getSockAddr().getAddress() == QHostAddress::LocalHost - || (_domainHandler.getSockAddr().getAddress() == _localSockAddr.getAddress() && connectionToken.isNull()); - - bool requiresUsernameSignature = !_domainHandler.isConnected() && !connectionToken.isNull() && !localhostDomain; + bool requiresUsernameSignature = !_domainHandler.isConnected() && !connectionToken.isNull(); if (requiresUsernameSignature && !accountManager->getAccountInfo().hasPrivateKey()) { qWarning() << "A keypair is required to present a username signature to the domain-server" @@ -732,7 +727,7 @@ void NodeList::ignoreNodeBySessionID(const QUuid& nodeID) { emit ignoredNode(nodeID); } else { - qWarning() << "UsersScriptingInterface::ignore called with an invalid ID or an ID which matches the current session ID."; + qWarning() << "NodeList::ignoreNodeBySessionID called with an invalid ID or an ID which matches the current session ID."; } } @@ -764,3 +759,28 @@ void NodeList::maybeSendIgnoreSetToNode(SharedNodePointer newNode) { } } } + +void NodeList::kickNodeBySessionID(const QUuid& nodeID) { + // send a request to domain-server to kick the node with the given session ID + // the domain-server will handle the persistence of the kick (via username or IP) + + if (!nodeID.isNull() && _sessionUUID != nodeID ) { + if (getThisNodeCanKick()) { + // setup the packet + auto kickPacket = NLPacket::create(PacketType::NodeKickRequest, NUM_BYTES_RFC4122_UUID, true); + + // write the node ID to the packet + kickPacket->write(nodeID.toRfc4122()); + + qDebug() << "Sending packet to kick node" << uuidStringWithoutCurlyBraces(nodeID); + + sendPacket(std::move(kickPacket), _domainHandler.getSockAddr()); + } else { + qWarning() << "You do not have permissions to kick in this domain." + << "Request to kick node" << uuidStringWithoutCurlyBraces(nodeID) << "will not be sent"; + } + } else { + qWarning() << "NodeList::kickNodeBySessionID called with an invalid ID or an ID which matches the current session ID."; + + } +} diff --git a/libraries/networking/src/NodeList.h b/libraries/networking/src/NodeList.h index ff994ce612..f3cd5bed0d 100644 --- a/libraries/networking/src/NodeList.h +++ b/libraries/networking/src/NodeList.h @@ -73,6 +73,8 @@ public: void ignoreNodeBySessionID(const QUuid& nodeID); bool isIgnoringNode(const QUuid& nodeID) const; + void kickNodeBySessionID(const QUuid& nodeID); + public slots: void reset(); void sendDomainServerCheckIn(); diff --git a/libraries/networking/src/NodePermissions.cpp b/libraries/networking/src/NodePermissions.cpp index fb74ccdc94..a1d4fc182e 100644 --- a/libraries/networking/src/NodePermissions.cpp +++ b/libraries/networking/src/NodePermissions.cpp @@ -13,78 +13,121 @@ #include #include "NodePermissions.h" -QString NodePermissions::standardNameLocalhost = QString("localhost"); -QString NodePermissions::standardNameLoggedIn = QString("logged-in"); -QString NodePermissions::standardNameAnonymous = QString("anonymous"); +NodePermissionsKey NodePermissions::standardNameLocalhost = NodePermissionsKey("localhost", 0); +NodePermissionsKey NodePermissions::standardNameLoggedIn = NodePermissionsKey("logged-in", 0); +NodePermissionsKey NodePermissions::standardNameAnonymous = NodePermissionsKey("anonymous", 0); +NodePermissionsKey NodePermissions::standardNameFriends = NodePermissionsKey("friends", 0); QStringList NodePermissions::standardNames = QList() - << NodePermissions::standardNameLocalhost - << NodePermissions::standardNameLoggedIn - << NodePermissions::standardNameAnonymous; + << NodePermissions::standardNameLocalhost.first + << NodePermissions::standardNameLoggedIn.first + << NodePermissions::standardNameAnonymous.first + << NodePermissions::standardNameFriends.first; + +NodePermissions::NodePermissions(QMap perms) { + _id = perms["permissions_id"].toString().toLower(); + if (perms.contains("group_id")) { + _groupID = perms["group_id"].toUuid(); + if (!_groupID.isNull()) { + _groupIDSet = true; + } + } + if (perms.contains("rank_id")) { + _rankID = QUuid(perms["rank_id"].toString()); + } + + permissions = NodePermissions::Permissions(); + permissions |= perms["id_can_connect"].toBool() ? Permission::canConnectToDomain : Permission::none; + permissions |= perms["id_can_adjust_locks"].toBool() ? Permission::canAdjustLocks : Permission::none; + permissions |= perms["id_can_rez"].toBool() ? Permission::canRezPermanentEntities : Permission::none; + permissions |= perms["id_can_rez_tmp"].toBool() ? Permission::canRezTemporaryEntities : Permission::none; + permissions |= perms["id_can_write_to_asset_server"].toBool() ? Permission::canWriteToAssetServer : Permission::none; + permissions |= perms["id_can_connect_past_max_capacity"].toBool() ? + Permission::canConnectPastMaxCapacity : Permission::none; + permissions |= perms["id_can_kick"].toBool() ? Permission::canKick : Permission::none; +} + +QVariant NodePermissions::toVariant(QHash groupRanks) { + QMap values; + values["permissions_id"] = _id; + if (_groupIDSet) { + values["group_id"] = _groupID; + if (groupRanks.contains(_rankID)) { + values["rank_id"] = _rankID; + values["rank_name"] = groupRanks[_rankID].name; + values["rank_order"] = groupRanks[_rankID].order; + } + } + values["id_can_connect"] = can(Permission::canConnectToDomain); + values["id_can_adjust_locks"] = can(Permission::canAdjustLocks); + values["id_can_rez"] = can(Permission::canRezPermanentEntities); + values["id_can_rez_tmp"] = can(Permission::canRezTemporaryEntities); + values["id_can_write_to_asset_server"] = can(Permission::canWriteToAssetServer); + values["id_can_connect_past_max_capacity"] = can(Permission::canConnectPastMaxCapacity); + values["id_can_kick"] = can(Permission::canKick); + return QVariant(values); +} + +void NodePermissions::setAll(bool value) { + permissions = NodePermissions::Permissions(); + if (value) { + permissions = ~permissions; + } +} 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; + permissions |= rhs.permissions; 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; -} +NodePermissions& NodePermissions::operator&=(const NodePermissions& rhs) { + permissions &= rhs.permissions; + return *this; +} + +NodePermissions NodePermissions::operator~() { + NodePermissions result = *this; + result.permissions = ~permissions; + return result; +} 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; + out << (uint)perms.permissions; 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; + uint permissionsInt; + in >> permissionsInt; + perms.permissions = (NodePermissions::Permissions)permissionsInt; return in; } QDebug operator<<(QDebug debug, const NodePermissions& perms) { - debug.nospace() << "[permissions: " << perms.getID() << " --"; - if (perms.canConnectToDomain) { + debug.nospace() << "[permissions: " << perms.getID() << "/" << perms.getVerifiedUserName() << " -- "; + debug.nospace() << "rank=" << perms.getRankID() + << ", groupID=" << perms.getGroupID() << "/" << (perms.isGroup() ? "y" : "n"); + if (perms.can(NodePermissions::Permission::canConnectToDomain)) { debug << " connect"; } - if (perms.canAdjustLocks) { + if (perms.can(NodePermissions::Permission::canAdjustLocks)) { debug << " locks"; } - if (perms.canRezPermanentEntities) { + if (perms.can(NodePermissions::Permission::canRezPermanentEntities)) { debug << " rez"; } - if (perms.canRezTemporaryEntities) { + if (perms.can(NodePermissions::Permission::canRezTemporaryEntities)) { debug << " rez-tmp"; } - if (perms.canWriteToAssetServer) { + if (perms.can(NodePermissions::Permission::canWriteToAssetServer)) { debug << " asset-server"; } - if (perms.canConnectPastMaxCapacity) { + if (perms.can(NodePermissions::Permission::canConnectPastMaxCapacity)) { debug << " ignore-max-cap"; } + if (perms.can(NodePermissions::Permission::canKick)) { + debug << " kick"; + } debug.nospace() << "]"; return debug.nospace(); } diff --git a/libraries/networking/src/NodePermissions.h b/libraries/networking/src/NodePermissions.h index de46a0dae5..5d2755f9b5 100644 --- a/libraries/networking/src/NodePermissions.h +++ b/libraries/networking/src/NodePermissions.h @@ -18,90 +18,109 @@ #include #include +#include "GroupRank.h" + class NodePermissions; using NodePermissionsPointer = std::shared_ptr; +using NodePermissionsKey = QPair; // name, rankID +using NodePermissionsKeyList = QList>; + class NodePermissions { public: - NodePermissions() { _id = QUuid::createUuid().toString(); } - NodePermissions(const QString& name) { _id = name.toLower(); } - NodePermissions(QMap perms) { - _id = perms["permissions_id"].toString().toLower(); - 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(); - } + NodePermissions() { _id = QUuid::createUuid().toString(); _rankID = QUuid(); } + NodePermissions(const QString& name) { _id = name.toLower(); _rankID = QUuid(); } + NodePermissions(const NodePermissionsKey& key) { _id = key.first.toLower(); _rankID = key.second; } + NodePermissions(QMap perms); - QString getID() const { return _id; } + const QString& getID() const { return _id; } // a user-name or a group-name, not verified + void setID(const QString& id) { _id = id; } + void setRankID(QUuid& rankID) { _rankID = rankID; } + const QUuid& getRankID() const { return _rankID; } + NodePermissionsKey getKey() const { return NodePermissionsKey(_id, _rankID); } - // the _id member isn't authenticated and _username is. - void setUserName(QString userName) { _userName = userName.toLower(); } - QString getUserName() { return _userName; } + // the _id member isn't authenticated/verified and _username is. + void setVerifiedUserName(QString userName) { _verifiedUserName = userName.toLower(); } + const QString& getVerifiedUserName() const { return _verifiedUserName; } + + void setGroupID(QUuid groupID) { _groupID = groupID; if (!groupID.isNull()) { _groupIDSet = true; }} + const QUuid& getGroupID() const { return _groupID; } + bool isGroup() const { return _groupIDSet; } bool isAssignment { false }; // these 3 names have special meaning. - static QString standardNameLocalhost; - static QString standardNameLoggedIn; - static QString standardNameAnonymous; + static NodePermissionsKey standardNameLocalhost; + static NodePermissionsKey standardNameLoggedIn; + static NodePermissionsKey standardNameAnonymous; + static NodePermissionsKey standardNameFriends; 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 }; + enum class Permission { + none = 0, + canConnectToDomain = 1, + canAdjustLocks = 2, + canRezPermanentEntities = 4, + canRezTemporaryEntities = 8, + canWriteToAssetServer = 16, + canConnectPastMaxCapacity = 32, + canKick = 64 + }; + Q_DECLARE_FLAGS(Permissions, Permission) + Permissions permissions; - void setAll(bool value) { - canConnectToDomain = value; - canAdjustLocks = value; - canRezPermanentEntities = value; - canRezTemporaryEntities = value; - canWriteToAssetServer = value; - canConnectPastMaxCapacity = value; - } + QVariant toVariant(QHash groupRanks = QHash()); - QVariant toVariant() { - QMap 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); - } + void setAll(bool value); NodePermissions& operator|=(const NodePermissions& rhs); - NodePermissions& operator|=(const NodePermissionsPointer& rhs); + NodePermissions& operator&=(const NodePermissions& rhs); + NodePermissions operator~(); friend QDataStream& operator<<(QDataStream& out, const NodePermissions& perms); friend QDataStream& operator>>(QDataStream& in, NodePermissions& perms); + void clear(Permission p) { permissions &= (Permission) (~(uint)p); } + void set(Permission p) { permissions |= p; } + bool can(Permission p) const { return permissions.testFlag(p); } + protected: QString _id; - QString _userName; + QUuid _rankID { QUuid() }; // 0 unless this is for a group + QString _verifiedUserName; + + bool _groupIDSet { false }; + QUuid _groupID; }; +Q_DECLARE_OPERATORS_FOR_FLAGS(NodePermissions::Permissions) // wrap QHash in a class that forces all keys to be lowercase class NodePermissionsMap { public: NodePermissionsMap() { } - NodePermissionsPointer& operator[](const QString& key) { return _data[key.toLower()]; } - NodePermissionsPointer operator[](const QString& key) const { return _data.value(key.toLower()); } - bool contains(const QString& key) const { return _data.contains(key.toLower()); } - QList keys() const { return _data.keys(); } - QHash get() { return _data; } + NodePermissionsPointer& operator[](const NodePermissionsKey& key) { + NodePermissionsKey dataKey(key.first.toLower(), key.second); + if (!_data.contains(dataKey)) { + _data[dataKey] = NodePermissionsPointer(new NodePermissions(key)); + } + return _data[dataKey]; + } + NodePermissionsPointer operator[](const NodePermissionsKey& key) const { + return _data.value(NodePermissionsKey(key.first.toLower(), key.second)); + } + bool contains(const NodePermissionsKey& key) const { + return _data.contains(NodePermissionsKey(key.first.toLower(), key.second)); + } + bool contains(const QString& keyFirst, QUuid keySecond) const { + return _data.contains(NodePermissionsKey(keyFirst.toLower(), keySecond)); + } + QList keys() const { return _data.keys(); } + QHash get() { return _data; } void clear() { _data.clear(); } + void remove(const NodePermissionsKey& key) { _data.remove(key); } private: - QHash _data; + QHash _data; }; @@ -109,6 +128,8 @@ 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); +NodePermissionsPointer& operator&=(NodePermissionsPointer& lhs, const NodePermissionsPointer& rhs); +NodePermissionsPointer& operator&=(NodePermissionsPointer& lhs, NodePermissions::Permission rhs); +NodePermissionsPointer operator~(NodePermissionsPointer& lhs); #endif // hifi_NodePermissions_h diff --git a/libraries/networking/src/udt/PacketHeaders.cpp b/libraries/networking/src/udt/PacketHeaders.cpp index ce1f25d45d..e9d61a827a 100644 --- a/libraries/networking/src/udt/PacketHeaders.cpp +++ b/libraries/networking/src/udt/PacketHeaders.cpp @@ -26,7 +26,7 @@ const QSet NON_VERIFIED_PACKETS = QSet() << PacketType::NodeJsonStats << PacketType::EntityQuery << PacketType::OctreeDataNack << PacketType::EntityEditNack << PacketType::DomainListRequest << PacketType::StopNode - << PacketType::DomainDisconnectRequest; + << PacketType::DomainDisconnectRequest << PacketType::NodeKickRequest; const QSet NON_SOURCED_PACKETS = QSet() << PacketType::StunResponse << PacketType::CreateAssignment << PacketType::RequestAssignment diff --git a/libraries/networking/src/udt/PacketHeaders.h b/libraries/networking/src/udt/PacketHeaders.h index 7281e24fa9..40524e2288 100644 --- a/libraries/networking/src/udt/PacketHeaders.h +++ b/libraries/networking/src/udt/PacketHeaders.h @@ -98,7 +98,8 @@ public: NegotiateAudioFormat, SelectedAudioFormat, MoreEntityShapes, - LAST_PACKET_TYPE = MoreEntityShapes + NodeKickRequest, + LAST_PACKET_TYPE = NodeKickRequest }; }; diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index b04a1d8023..581bd285e2 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -102,13 +102,17 @@ Model::Model(RigPointer rig, QObject* parent) : _calculatedMeshTrianglesValid(false), _meshGroupsKnown(false), _isWireframe(false), - _rig(rig) { + _rig(rig) +{ // we may have been created in the network thread, but we live in the main thread if (_viewState) { moveToThread(_viewState->getMainThread()); } setSnapModelToRegistrationPoint(true, glm::vec3(0.5f)); + + // handle download failure reported by the GeometryResourceWatcher + connect(&_renderWatcher, &GeometryResourceWatcher::resourceFailed, this, &Model::handleGeometryResourceFailure); } Model::~Model() { @@ -818,6 +822,7 @@ void Model::setURL(const QUrl& url) { _needsReload = true; _needsUpdateTextures = true; _meshGroupsKnown = false; + _geometryRequestFailed = false; invalidCalculatedMeshBoxes(); deleteGeometry(); diff --git a/libraries/render-utils/src/Model.h b/libraries/render-utils/src/Model.h index 98e50c66f4..b95c0318b4 100644 --- a/libraries/render-utils/src/Model.h +++ b/libraries/render-utils/src/Model.h @@ -147,8 +147,9 @@ public: Q_INVOKABLE void setCollisionModelURL(const QUrl& url); const QUrl& getCollisionURL() const { return _collisionUrl; } - bool isActive() const { return isLoaded(); } + + bool didGeometryRequestFail() const { return _geometryRequestFailed; } bool convexHullContains(glm::vec3 point); @@ -392,6 +393,11 @@ protected: RigPointer _rig; uint32_t _deleteGeometryCounter { 0 }; + + bool _geometryRequestFailed { false }; + +private slots: + void handleGeometryResourceFailure() { _geometryRequestFailed = true; } }; Q_DECLARE_METATYPE(ModelPointer) diff --git a/libraries/script-engine/src/UsersScriptingInterface.cpp b/libraries/script-engine/src/UsersScriptingInterface.cpp index ff7ccb0164..69ad8e04ad 100644 --- a/libraries/script-engine/src/UsersScriptingInterface.cpp +++ b/libraries/script-engine/src/UsersScriptingInterface.cpp @@ -13,7 +13,23 @@ #include +UsersScriptingInterface::UsersScriptingInterface() { + // emit a signal when kick permissions have changed + auto nodeList = DependencyManager::get(); + connect(nodeList.data(), &LimitedNodeList::canKickChanged, this, &UsersScriptingInterface::canKickChanged); +} + void UsersScriptingInterface::ignore(const QUuid& nodeID) { // ask the NodeList to ignore this user (based on the session ID of their node) DependencyManager::get()->ignoreNodeBySessionID(nodeID); } + +void UsersScriptingInterface::kick(const QUuid& nodeID) { + // ask the NodeList to kick the user with the given session ID + DependencyManager::get()->kickNodeBySessionID(nodeID); +} + +bool UsersScriptingInterface::getCanKick() { + // ask the NodeList to return our ability to kick + return DependencyManager::get()->getThisNodeCanKick(); +} diff --git a/libraries/script-engine/src/UsersScriptingInterface.h b/libraries/script-engine/src/UsersScriptingInterface.h index 0dc62c088c..712eeedeb6 100644 --- a/libraries/script-engine/src/UsersScriptingInterface.h +++ b/libraries/script-engine/src/UsersScriptingInterface.h @@ -20,8 +20,19 @@ class UsersScriptingInterface : public QObject, public Dependency { Q_OBJECT SINGLETON_DEPENDENCY + Q_PROPERTY(bool canKick READ getCanKick) + +public: + UsersScriptingInterface(); + public slots: void ignore(const QUuid& nodeID); + void kick(const QUuid& nodeID); + + bool getCanKick(); + +signals: + void canKickChanged(bool canKick); }; diff --git a/libraries/shared/src/HifiConfigVariantMap.cpp b/libraries/shared/src/HifiConfigVariantMap.cpp index 5ae5ff740d..252079f182 100644 --- a/libraries/shared/src/HifiConfigVariantMap.cpp +++ b/libraries/shared/src/HifiConfigVariantMap.cpp @@ -111,6 +111,13 @@ void HifiConfigVariantMap::loadMasterAndUserConfig(const QStringList& argumentLi loadMapFromJSONFile(_masterConfig, masterConfigFilepath); } + // load the user config - that method replace loadMasterAndUserConfig after the 1.7 migration + loadConfig(argumentList); + + mergeMasterAndUserConfigs(); +} + +void HifiConfigVariantMap::loadConfig(const QStringList& argumentList) { // load the user config const QString USER_CONFIG_FILE_OPTION = "--user-config"; static const QString USER_CONFIG_FILE_NAME = "config.json"; @@ -159,12 +166,10 @@ void HifiConfigVariantMap::loadMasterAndUserConfig(const QStringList& argumentLi } } } - + } - + loadMapFromJSONFile(_userConfig, _userConfigFilename); - - mergeMasterAndUserConfigs(); } void HifiConfigVariantMap::mergeMasterAndUserConfigs() { diff --git a/libraries/shared/src/HifiConfigVariantMap.h b/libraries/shared/src/HifiConfigVariantMap.h index e92561cff5..cb6e92df96 100644 --- a/libraries/shared/src/HifiConfigVariantMap.h +++ b/libraries/shared/src/HifiConfigVariantMap.h @@ -15,16 +15,22 @@ #include #include +QVariant* valueForKeyPath(QVariantMap& variantMap, const QString& keyPath, bool shouldCreateIfMissing = false); + class HifiConfigVariantMap { public: static QVariantMap mergeCLParametersWithJSONConfig(const QStringList& argumentList); HifiConfigVariantMap(); void loadMasterAndUserConfig(const QStringList& argumentList); + void loadConfig(const QStringList& argumentList); + + const QVariant value(const QString& key) const { return _userConfig.value(key); } + QVariant* valueForKeyPath(const QString& keyPath, bool shouldCreateIfMissing = false) + { return ::valueForKeyPath(_userConfig, keyPath, shouldCreateIfMissing); } - const QVariantMap& getMasterConfig() const { return _masterConfig; } - QVariantMap& getUserConfig() { return _userConfig; } QVariantMap& getMergedConfig() { return _mergedConfig; } + QVariantMap& getConfig() { return _userConfig; } void mergeMasterAndUserConfigs(); @@ -40,6 +46,4 @@ private: void addMissingValuesToExistingMap(QVariantMap& existingMap, const QVariantMap& newMap); }; -QVariant* valueForKeyPath(QVariantMap& variantMap, const QString& keyPath, bool shouldCreateIfMissing = false); - #endif // hifi_HifiConfigVariantMap_h diff --git a/libraries/ui/src/FileDialogHelper.cpp b/libraries/ui/src/FileDialogHelper.cpp index 3a12e054df..9d791ec562 100644 --- a/libraries/ui/src/FileDialogHelper.cpp +++ b/libraries/ui/src/FileDialogHelper.cpp @@ -115,3 +115,15 @@ QList FileDialogHelper::urlToList(const QUrl& url) { return results; } +void FileDialogHelper::monitorDirectory(const QString& path) { + if (!_fsWatcherPath.isEmpty()) { + _fsWatcher.removePath(_fsWatcherPath); + _fsWatcherPath = ""; + } + + if (!path.isEmpty()) { + _fsWatcher.addPath(path); + _fsWatcherPath = path; + connect(&_fsWatcher, &QFileSystemWatcher::directoryChanged, this, &FileDialogHelper::contentsChanged); + } +} diff --git a/libraries/ui/src/FileDialogHelper.h b/libraries/ui/src/FileDialogHelper.h index 6058f8f7bb..6c352ecdfc 100644 --- a/libraries/ui/src/FileDialogHelper.h +++ b/libraries/ui/src/FileDialogHelper.h @@ -9,11 +9,12 @@ #ifndef hifi_ui_FileDialogHelper_h #define hifi_ui_FileDialogHelper_h +#include #include #include -#include #include #include +#include class FileDialogHelper : public QObject { @@ -61,6 +62,15 @@ public: Q_INVOKABLE QList urlToList(const QUrl& url); Q_INVOKABLE void openDirectory(const QString& path); + + Q_INVOKABLE void monitorDirectory(const QString& path); + +signals: + void contentsChanged(); + +private: + QFileSystemWatcher _fsWatcher; + QString _fsWatcherPath; }; diff --git a/libraries/ui/src/OffscreenUi.h b/libraries/ui/src/OffscreenUi.h index 36297d76c7..bc0a6bf544 100644 --- a/libraries/ui/src/OffscreenUi.h +++ b/libraries/ui/src/OffscreenUi.h @@ -107,7 +107,7 @@ public: QMessageBox::StandardButtons buttons = QMessageBox::Ok, QMessageBox::StandardButton defaultButton = QMessageBox::NoButton); static QMessageBox::StandardButton question(const QString& title, const QString& text, - QMessageBox::StandardButtons buttons = QMessageBox::Ok, + QMessageBox::StandardButtons buttons = QMessageBox::Yes | QMessageBox::No, QMessageBox::StandardButton defaultButton = QMessageBox::NoButton); static QMessageBox::StandardButton warning(const QString& title, const QString& text, QMessageBox::StandardButtons buttons = QMessageBox::Ok, diff --git a/libraries/ui/src/QmlWindowClass.cpp b/libraries/ui/src/QmlWindowClass.cpp index 7d56e51495..c3ca5f54d9 100644 --- a/libraries/ui/src/QmlWindowClass.cpp +++ b/libraries/ui/src/QmlWindowClass.cpp @@ -124,6 +124,10 @@ void QmlWindowClass::initQml(QVariantMap properties) { // Forward messages received from QML on to the script connect(_qmlWindow, SIGNAL(sendToScript(QVariant)), this, SLOT(qmlToScript(const QVariant&)), Qt::QueuedConnection); connect(_qmlWindow, SIGNAL(visibleChanged()), this, SIGNAL(visibleChanged()), Qt::QueuedConnection); + + connect(_qmlWindow, SIGNAL(resized(QSizeF)), this, SIGNAL(resized(QSizeF)), Qt::QueuedConnection); + connect(_qmlWindow, SIGNAL(moved(QVector2D)), this, SLOT(hasMoved(QVector2D)), Qt::QueuedConnection); + connect(_qmlWindow, SIGNAL(windowClosed()), this, SLOT(hasClosed()), Qt::QueuedConnection); }); } Q_ASSERT(_qmlWindow); @@ -259,7 +263,12 @@ void QmlWindowClass::close() { } } +void QmlWindowClass::hasMoved(QVector2D position) { + emit moved(glm::vec2(position.x(), position.y())); +} + void QmlWindowClass::hasClosed() { + emit closed(); } void QmlWindowClass::raise() { diff --git a/libraries/ui/src/QmlWindowClass.h b/libraries/ui/src/QmlWindowClass.h index c30027df6e..07cf736334 100644 --- a/libraries/ui/src/QmlWindowClass.h +++ b/libraries/ui/src/QmlWindowClass.h @@ -62,6 +62,7 @@ signals: void fromQml(const QVariant& message); protected slots: + void hasMoved(QVector2D); void hasClosed(); void qmlToScript(const QVariant& message); diff --git a/script-archive/example/games/planky.js b/script-archive/example/games/planky.js index 742cc3b7d0..9a88c81384 100644 --- a/script-archive/example/games/planky.js +++ b/script-archive/example/games/planky.js @@ -12,7 +12,7 @@ // HIFI_PUBLIC_BUCKET = 'http://s3.amazonaws.com/hifi-public/'; -Script.include('../../libraries/toolBars.js'); +Script.include('/~/system/libraries/toolBars.js'); const DEFAULT_NUM_LAYERS = 16; const DEFAULT_BASE_DIMENSION = { x: 7, y: 2, z: 7 }; diff --git a/scripts/defaultScripts.js b/scripts/defaultScripts.js index 817d63582d..0efcd0c140 100644 --- a/scripts/defaultScripts.js +++ b/scripts/defaultScripts.js @@ -17,7 +17,7 @@ Script.load("system/goto.js"); Script.load("system/hmd.js"); Script.load("system/marketplace.js"); Script.load("system/edit.js"); -Script.load("system/ignore.js"); +Script.load("system/mod.js"); Script.load("system/selectAudioDevice.js"); Script.load("system/notifications.js"); Script.load("system/controllers/handControllerGrab.js"); diff --git a/scripts/system/assets/images/ignore-target-01.svg b/scripts/system/assets/images/ignore-target-01.svg deleted file mode 100644 index 98cee89ca1..0000000000 --- a/scripts/system/assets/images/ignore-target-01.svg +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/scripts/system/assets/images/ignore-target.svg b/scripts/system/assets/images/ignore-target.svg new file mode 100644 index 0000000000..3d685139ec --- /dev/null +++ b/scripts/system/assets/images/ignore-target.svg @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/scripts/system/assets/images/kick-target.svg b/scripts/system/assets/images/kick-target.svg new file mode 100644 index 0000000000..21cb3a5462 --- /dev/null +++ b/scripts/system/assets/images/kick-target.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + diff --git a/scripts/system/assets/images/progress-bar-background.svg b/scripts/system/assets/images/progress-bar-background.svg index 732acd05ad..a8b4e1aab5 100644 --- a/scripts/system/assets/images/progress-bar-background.svg +++ b/scripts/system/assets/images/progress-bar-background.svg @@ -1,5 +1,5 @@  - - + + diff --git a/scripts/system/assets/images/progress-bar.svg b/scripts/system/assets/images/progress-bar.svg index 0df6f98686..e24a2cbff4 100644 --- a/scripts/system/assets/images/progress-bar.svg +++ b/scripts/system/assets/images/progress-bar.svg @@ -1,7 +1,12 @@ - - - + viewBox="0 0 960 10" enable-background="new -159 536 960 30" xml:space="preserve"> + + + + + + + + diff --git a/scripts/system/assets/images/tools/kick.svg b/scripts/system/assets/images/tools/kick.svg new file mode 100644 index 0000000000..1eed6e7f43 --- /dev/null +++ b/scripts/system/assets/images/tools/kick.svg @@ -0,0 +1,140 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/scripts/system/ignore.js b/scripts/system/mod.js similarity index 62% rename from scripts/system/ignore.js rename to scripts/system/mod.js index 1c996a7fcc..035d7726a1 100644 --- a/scripts/system/ignore.js +++ b/scripts/system/mod.js @@ -1,5 +1,5 @@ // -// ignore.js +// mod.js // scripts/system/ // // Created by Stephen Birarda on 07/11/2016 @@ -12,10 +12,14 @@ // grab the toolbar var toolbar = Toolbars.getToolbar("com.highfidelity.interface.toolbar.system"); -// setup the ignore button and add it to the toolbar +function buttonImageURL() { + return Script.resolvePath("assets/images/tools/" + (Users.canKick ? 'kick.svg' : 'ignore.svg')); +} + +// setup the mod button and add it to the toolbar var button = toolbar.addButton({ - objectName: 'ignore', - imageURL: Script.resolvePath("assets/images/tools/ignore.svg"), + objectName: 'mod', + imageURL: buttonImageURL(), visible: true, buttonState: 1, defaultState: 2, @@ -23,19 +27,24 @@ var button = toolbar.addButton({ alpha: 0.9 }); +// if this user's kick permissions change, change the state of the button in the HUD +Users.canKickChanged.connect(function(canKick){ + button.writeProperty('imageURL', buttonImageURL()); +}); + var isShowingOverlays = false; -var ignoreOverlays = {}; +var modOverlays = {}; function removeOverlays() { // enumerate the overlays and remove them - var ignoreOverlayKeys = Object.keys(ignoreOverlays); + var modOverlayKeys = Object.keys(modOverlays); - for (i = 0; i < ignoreOverlayKeys.length; ++i) { - var avatarID = ignoreOverlayKeys[i]; - Overlays.deleteOverlay(ignoreOverlays[avatarID]); + for (i = 0; i < modOverlayKeys.length; ++i) { + var avatarID = modOverlayKeys[i]; + Overlays.deleteOverlay(modOverlays[avatarID]); } - ignoreOverlays = {}; + modOverlays = {}; } // handle clicks on the toolbar button @@ -54,6 +63,10 @@ function buttonClicked(){ button.clicked.connect(buttonClicked); +function overlayURL() { + return Script.resolvePath("assets") + "/images/" + (Users.canKick ? "kick-target.svg" : "ignore-target.svg"); +} + function updateOverlays() { if (isShowingOverlays) { @@ -80,17 +93,18 @@ function updateOverlays() { var overlayPosition = avatar.getJointPosition("Head"); overlayPosition.y += 0.45; - if (avatarID in ignoreOverlays) { + if (avatarID in modOverlays) { // keep the overlay above the current position of this avatar - Overlays.editOverlay(ignoreOverlays[avatarID], { - position: overlayPosition + Overlays.editOverlay(modOverlays[avatarID], { + position: overlayPosition, + url: overlayURL() }); } else { // add the overlay above this avatar var newOverlay = Overlays.addOverlay("image3d", { - url: Script.resolvePath("assets/images/ignore-target-01.svg"), + url: overlayURL(), position: overlayPosition, - size: 0.4, + size: 1, scale: 0.4, color: { red: 255, green: 255, blue: 255}, alpha: 1, @@ -100,7 +114,7 @@ function updateOverlays() { }); // push this overlay to our array of overlays - ignoreOverlays[avatarID] = newOverlay; + modOverlays[avatarID] = newOverlay; } } } @@ -113,24 +127,28 @@ AvatarList.avatarRemovedEvent.connect(function(avatarID){ // we are currently showing overlays and an avatar just went away // first remove the rendered overlay - Overlays.deleteOverlay(ignoreOverlays[avatarID]); + Overlays.deleteOverlay(modOverlays[avatarID]); - // delete the saved ID of the overlay from our ignored overlays object - delete ignoreOverlays[avatarID]; + // delete the saved ID of the overlay from our mod overlays object + delete modOverlays[avatarID]; } }); function handleSelectedOverlay(clickedOverlay) { - // see this is one of our ignore overlays + // see this is one of our mod overlays - var ignoreOverlayKeys = Object.keys(ignoreOverlays) - for (i = 0; i < ignoreOverlayKeys.length; ++i) { - var avatarID = ignoreOverlayKeys[i]; - var ignoreOverlay = ignoreOverlays[avatarID]; + var modOverlayKeys = Object.keys(modOverlays) + for (i = 0; i < modOverlayKeys.length; ++i) { + var avatarID = modOverlayKeys[i]; + var modOverlay = modOverlays[avatarID]; - if (clickedOverlay.overlayID == ignoreOverlay) { - // matched to an overlay, ask for the matching avatar to be ignored - Users.ignore(avatarID); + if (clickedOverlay.overlayID == modOverlay) { + // matched to an overlay, ask for the matching avatar to be kicked or ignored + if (Users.canKick) { + Users.kick(avatarID); + } else { + Users.ignore(avatarID); + } // cleanup of the overlay is handled by the connection to avatarRemovedEvent } @@ -161,15 +179,9 @@ Controller.mousePressEvent.connect(function(event){ // But we dont' get mousePressEvents. var triggerMapping = Controller.newMapping(Script.resolvePath('') + '-click'); -var TRIGGER_GRAB_VALUE = 0.85; // From handControllerGrab/Pointer.js. Should refactor. -var TRIGGER_ON_VALUE = 0.4; -var TRIGGER_OFF_VALUE = 0.15; -var triggered = false; -var activeHand = Controller.Standard.RightHand; - -function controllerComputePickRay() { - var controllerPose = Controller.getPoseValue(activeHand); - if (controllerPose.valid && triggered) { +function controllerComputePickRay(hand) { + var controllerPose = Controller.getPoseValue(hand); + if (controllerPose.valid) { var controllerPosition = Vec3.sum(Vec3.multiplyQbyV(MyAvatar.orientation, controllerPose.translation), MyAvatar.position); // This gets point direction right, but if you want general quaternion it would be more complicated: @@ -178,37 +190,27 @@ function controllerComputePickRay() { } } -function makeTriggerHandler(hand) { - return function (value) { - if (isShowingOverlays) { - if (!triggered && (value > TRIGGER_GRAB_VALUE)) { // should we smooth? - triggered = true; - if (activeHand !== hand) { - // No switching while the other is already triggered, so no need to release. - activeHand = (activeHand === Controller.Standard.RightHand) ? Controller.Standard.LeftHand : Controller.Standard.RightHand; +function makeClickHandler(hand) { + return function(clicked) { + if (clicked == 1.0 && isShowingOverlays) { + var pickRay = controllerComputePickRay(hand); + if (pickRay) { + var overlayIntersection = Overlays.findRayIntersection(pickRay); + if (overlayIntersection.intersects) { + handleSelectedOverlay(overlayIntersection); } - - var pickRay = controllerComputePickRay(); - if (pickRay) { - var overlayIntersection = Overlays.findRayIntersection(pickRay); - if (overlayIntersection.intersects) { - handleSelectedOverlay(overlayIntersection); - } - } - } else if (triggered && (value < TRIGGER_OFF_VALUE)) { - triggered = false; } } }; } -triggerMapping.from(Controller.Standard.RT).peek().to(makeTriggerHandler(Controller.Standard.RightHand)); -triggerMapping.from(Controller.Standard.LT).peek().to(makeTriggerHandler(Controller.Standard.LeftHand)); +triggerMapping.from(Controller.Standard.RTClick).peek().to(makeClickHandler(Controller.Standard.RightHand)); +triggerMapping.from(Controller.Standard.LTClick).peek().to(makeClickHandler(Controller.Standard.LeftHand)); triggerMapping.enable(); // cleanup the toolbar button and overlays when script is stopped Script.scriptEnding.connect(function() { - toolbar.removeButton('ignore'); + toolbar.removeButton('mod'); removeOverlays(); triggerMapping.disable(); }); diff --git a/scripts/system/progress.js b/scripts/system/progress.js index c6537eef52..7c1c475e99 100644 --- a/scripts/system/progress.js +++ b/scripts/system/progress.js @@ -13,6 +13,11 @@ (function() { + function debug() { + return; + print.apply(null, arguments); + } + var rawProgress = 100, // % raw value. displayProgress = 100, // % smoothed value to display. DISPLAY_PROGRESS_MINOR_MAXIMUM = 8, // % displayed progress bar goes up to while 0% raw progress. @@ -28,35 +33,18 @@ FADE_OUT_WAIT = 1000, // Wait before starting to fade out after progress 100%. visible = false, BAR_WIDTH = 480, // Dimension of SVG in pixels of visible portion (half) of the bar. - BAR_HEIGHT = 30, + BAR_HEIGHT = 10, + BAR_Y_OFFSET_2D = -10, // Offset of progress bar while in desktop mode + BAR_Y_OFFSET_HMD = -300, // Offset of progress bar while in HMD BAR_URL = Script.resolvePath("assets/images/progress-bar.svg"), - BACKGROUND_WIDTH = 540, - BACKGROUND_HEIGHT = 90, + BACKGROUND_WIDTH = 520, + BACKGROUND_HEIGHT = 50, BACKGROUND_URL = Script.resolvePath("assets/images/progress-bar-background.svg"), - isOnHMD = false, windowWidth = 0, windowHeight = 0, background2D = {}, bar2D = {}, - SCALE_2D = 0.35, // Scale the SVGs for 2D display. - background3D = {}, - bar3D = {}, - PROGRESS_3D_DIRECTION = 0.0, // Degrees from avatar orientation. - PROGRESS_3D_DISTANCE = 0.602, // Horizontal distance from avatar position. - PROGRESS_3D_ELEVATION = -0.8, // Height of top middle of top notification relative to avatar eyes. - PROGRESS_3D_YAW = 0.0, // Degrees relative to notifications direction. - PROGRESS_3D_PITCH = -60.0, // Degrees from vertical. - SCALE_3D = 0.0011, // Scale the bar SVG for 3D display. - BACKGROUND_3D_SIZE = { - x: 0.76, - y: 0.08 - }, // Match up with the 3D background with those of notifications.js notices. - BACKGROUND_3D_COLOR = { - red: 2, - green: 2, - blue: 2 - }, - BACKGROUND_3D_ALPHA = 0.7; + SCALE_2D = 0.35; // Scale the SVGs for 2D display. function fade() { @@ -64,9 +52,7 @@ if (alpha < 0) { alpha = 0; - } - - if (alpha > 1) { + } else if (alpha > 1) { alpha = 1; } @@ -79,115 +65,123 @@ visible = false; } - if (isOnHMD) { - Overlays.editOverlay(background3D.overlay, { - backgroundAlpha: alpha * BACKGROUND_3D_ALPHA, - visible: visible - }); - } else { - Overlays.editOverlay(background2D.overlay, { - alpha: alpha, - visible: visible - }); - } - Overlays.editOverlay(isOnHMD ? bar3D.overlay : bar2D.overlay, { + Overlays.editOverlay(background2D.overlay, { + alpha: alpha, + visible: visible + }); + Overlays.editOverlay(bar2D.overlay, { alpha: alpha, visible: visible }); } + Window.domainChanged.connect(function() { + isDownloading = false; + bestRawProgress = 100; + rawProgress = 100; + displayProgress = 100; + }); + + // Max seen since downloads started. This is reset when all downloads have completed. + var maxSeen = 0; + + // Progress is defined as: (pending_downloads + active_downloads) / max_seen + // We keep track of both the current progress (rawProgress) and the + // best progress we've seen (bestRawProgress). As you are downloading, you may + // encounter new assets that require downloads, increasing the number of + // pending downloads and thus decreasing your overall progress. + var bestRawProgress = 0; + + // True if we have known active downloads + var isDownloading = false; + + // Entities are streamed to users, so you don't receive them all at once; instead, you + // receive them over a period of time. In many cases we end up in a situation where + // + // The initial delay cooldown keeps us from tracking progress before the allotted time + // has passed. + var INITIAL_DELAY_COOLDOWN_TIME = 1000; + var initialDelayCooldown = 0; function onDownloadInfoChanged(info) { var i; + debug("PROGRESS: Download info changed ", info.downloading.length, info.pending, maxSeen); + // Update raw progress value if (info.downloading.length + info.pending === 0) { + isDownloading = false; rawProgress = 100; + bestRawProgress = 100; + initialDelayCooldown = INITIAL_DELAY_COOLDOWN_TIME; } else { - rawProgress = 0; - for (i = 0; i < info.downloading.length; i += 1) { - rawProgress += info.downloading[i]; + var count = info.downloading.length + info.pending; + if (!isDownloading) { + isDownloading = true; + bestRawProgress = 0; + rawProgress = 0; + initialDelayCooldown = INITIAL_DELAY_COOLDOWN_TIME; + displayProgress = 0; + maxSeen = count; + } + if (count > maxSeen) { + maxSeen = count; + } + if (initialDelayCooldown <= 0) { + rawProgress = ((maxSeen - count) / maxSeen) * 100; + + if (rawProgress > bestRawProgress) { + bestRawProgress = rawProgress; + } } - rawProgress = rawProgress / (info.downloading.length + info.pending); } + debug("PROGRESS:", rawProgress, bestRawProgress, maxSeen); } function createOverlays() { - if (isOnHMD) { - - background3D.overlay = Overlays.addOverlay("rectangle3d", { - size: BACKGROUND_3D_SIZE, - color: BACKGROUND_3D_COLOR, - alpha: BACKGROUND_3D_ALPHA, - solid: true, - isFacingAvatar: false, - visible: false, - ignoreRayIntersection: true - }); - bar3D.overlay = Overlays.addOverlay("image3d", { - url: BAR_URL, - subImage: { - x: BAR_WIDTH, - y: 0, - width: BAR_WIDTH, - height: BAR_HEIGHT - }, - scale: SCALE_3D * BAR_WIDTH, - isFacingAvatar: false, - visible: false, - alpha: 0.0, - ignoreRayIntersection: true - }); - - } else { - - background2D.overlay = Overlays.addOverlay("image", { - imageURL: BACKGROUND_URL, - width: background2D.width, - height: background2D.height, - visible: false, - alpha: 0.0 - }); - bar2D.overlay = Overlays.addOverlay("image", { - imageURL: BAR_URL, - subImage: { - x: BAR_WIDTH, - y: 0, - width: BAR_WIDTH, - height: BAR_HEIGHT - }, - width: bar2D.width, - height: bar2D.height, - visible: false, - alpha: 0.0 - }); - } + background2D.overlay = Overlays.addOverlay("image", { + imageURL: BACKGROUND_URL, + width: background2D.width, + height: background2D.height, + visible: false, + alpha: 0.0 + }); + bar2D.overlay = Overlays.addOverlay("image", { + imageURL: BAR_URL, + subImage: { + x: 0, + y: 0, + width: BAR_WIDTH, + height: BAR_HEIGHT + }, + width: bar2D.width, + height: bar2D.height, + visible: false, + alpha: 0.0 + }); } function deleteOverlays() { - Overlays.deleteOverlay(isOnHMD ? background3D.overlay : background2D.overlay); - Overlays.deleteOverlay(isOnHMD ? bar3D.overlay : bar2D.overlay); + Overlays.deleteOverlay(background2D.overlay); + Overlays.deleteOverlay(bar2D.overlay); } + var b = 0; + var currentOrientation = null; function update() { + initialDelayCooldown -= 30; var viewport, eyePosition, avatarOrientation; - if (isOnHMD !== HMD.active) { - deleteOverlays(); - isOnHMD = !isOnHMD; - createOverlays(); + if (displayProgress < rawProgress) { + var diff = rawProgress - displayProgress; + if (diff < 0.5) { + displayProgress = rawProgress; + } else { + displayProgress += diff * 0.05; + } } - // Calculate progress value to display - if (rawProgress === 0 && displayProgress <= DISPLAY_PROGRESS_MINOR_MAXIMUM) { - displayProgress = Math.min(displayProgress + DISPLAY_PROGRESS_MINOR_INCREMENT, DISPLAY_PROGRESS_MINOR_MAXIMUM); - } else if (rawProgress < displayProgress) { - displayProgress = rawProgress; - } else if (rawProgress > displayProgress) { - displayProgress = Math.min(rawProgress, displayProgress + DISPLAY_PROGRESS_MAJOR_INCREMENT); - } // else (rawProgress === displayProgress); do nothing. - // Update state if (!visible) { // Not visible because no recent downloads if (displayProgress < 100) { // Have started downloading so fade in @@ -197,7 +191,7 @@ } } else if (alphaDelta !== 0.0) { // Fading in or out if (alphaDelta > 0) { - if (displayProgress === 100) { // Was downloading but now have finished so fade out + if (rawProgress === 100) { // Was downloading but now have finished so fade out alphaDelta = ALPHA_DELTA_OUT; } } else { @@ -207,7 +201,7 @@ } } else { // Fully visible because downloading or recently so if (fadeWaitTimer === null) { - if (displayProgress === 100) { // Was downloading but have finished so fade out soon + if (rawProgress === 100) { // Was downloading but have finished so fade out soon fadeWaitTimer = Script.setTimeout(function() { alphaDelta = ALPHA_DELTA_OUT; fadeTimer = Script.setInterval(fade, FADE_INTERVAL); @@ -225,73 +219,58 @@ if (visible) { // Update progress bar - Overlays.editOverlay(isOnHMD ? bar3D.overlay : bar2D.overlay, { - visible: visible, + Overlays.editOverlay(bar2D.overlay, { + visible: true, subImage: { x: BAR_WIDTH * (1 - displayProgress / 100), y: 0, width: BAR_WIDTH, height: BAR_HEIGHT - } + }, }); - // Update position - if (isOnHMD) { - // Update 3D overlays to maintain positions relative to avatar - eyePosition = MyAvatar.getDefaultEyePosition(); - avatarOrientation = MyAvatar.orientation; + Overlays.editOverlay(background2D.overlay, { + visible: true, + }); - Overlays.editOverlay(background3D.overlay, { - position: Vec3.sum(eyePosition, Vec3.multiplyQbyV(avatarOrientation, background3D.offset)), - rotation: Quat.multiply(avatarOrientation, background3D.orientation) - }); - Overlays.editOverlay(bar3D.overlay, { - position: Vec3.sum(eyePosition, Vec3.multiplyQbyV(avatarOrientation, bar3D.offset)), - rotation: Quat.multiply(avatarOrientation, bar3D.orientation) - }); + // Update 2D overlays to maintain positions at bottom middle of window + viewport = Controller.getViewportDimensions(); - } else { - // Update 2D overlays to maintain positions at bottom middle of window - viewport = Controller.getViewportDimensions(); - - if (viewport.x !== windowWidth || viewport.y !== windowHeight) { - windowWidth = viewport.x; - windowHeight = viewport.y; - - Overlays.editOverlay(background2D.overlay, { - x: windowWidth / 2 - background2D.width / 2, - y: windowHeight - background2D.height - bar2D.height - }); - - Overlays.editOverlay(bar2D.overlay, { - x: windowWidth / 2 - bar2D.width / 2, - y: windowHeight - background2D.height - bar2D.height + (background2D.height - bar2D.height) / 2 - }); - } + if (viewport.x !== windowWidth || viewport.y !== windowHeight) { + updateProgressBarLocation(); } } } + function updateProgressBarLocation() { + viewport = Controller.getViewportDimensions(); + windowWidth = viewport.x; + windowHeight = viewport.y; + + var yOffset = HMD.active ? BAR_Y_OFFSET_HMD : BAR_Y_OFFSET_2D; + + background2D.width = SCALE_2D * BACKGROUND_WIDTH; + background2D.height = SCALE_2D * BACKGROUND_HEIGHT; + bar2D.width = SCALE_2D * BAR_WIDTH; + bar2D.height = SCALE_2D * BAR_HEIGHT; + + Overlays.editOverlay(background2D.overlay, { + x: windowWidth / 2 - background2D.width / 2, + y: windowHeight - background2D.height - bar2D.height + yOffset + }); + + Overlays.editOverlay(bar2D.overlay, { + x: windowWidth / 2 - bar2D.width / 2, + y: windowHeight - background2D.height - bar2D.height + (background2D.height - bar2D.height) / 2 + yOffset + }); + } + function setUp() { background2D.width = SCALE_2D * BACKGROUND_WIDTH; background2D.height = SCALE_2D * BACKGROUND_HEIGHT; bar2D.width = SCALE_2D * BAR_WIDTH; bar2D.height = SCALE_2D * BAR_HEIGHT; - background3D.offset = Vec3.multiplyQbyV(Quat.fromPitchYawRollDegrees(0, PROGRESS_3D_DIRECTION, 0), { - x: 0, - y: 0, - z: -PROGRESS_3D_DISTANCE - }); - background3D.offset.y += PROGRESS_3D_ELEVATION; - background3D.orientation = Quat.fromPitchYawRollDegrees(PROGRESS_3D_PITCH, PROGRESS_3D_DIRECTION + PROGRESS_3D_YAW, 0); - bar3D.offset = Vec3.sum(background3D.offset, { - x: 0, - y: 0, - z: 0.001 - }); // Just in front of background - bar3D.orientation = background3D.orientation; - createOverlays(); } @@ -302,6 +281,6 @@ setUp(); GlobalServices.downloadInfoChanged.connect(onDownloadInfoChanged); GlobalServices.updateDownloadInfo(); - Script.update.connect(update); + Script.setInterval(update, 1000/60); Script.scriptEnding.connect(tearDown); -}()); \ No newline at end of file +}());