Merge pull request #8009 from sethalves/permissions-grid

Permissions grid
This commit is contained in:
Brad Hefta-Gaub 2016-06-10 20:39:26 -07:00 committed by GitHub
commit 27c6df660c
28 changed files with 1200 additions and 413 deletions

View file

@ -286,8 +286,8 @@ void AssignmentClientMonitor::handleChildStatusPacket(QSharedPointer<ReceivedMes
if (!senderID.isNull()) {
// We don't have this node yet - we should add it
matchingNode = DependencyManager::get<LimitedNodeList>()->addOrUpdateNode
(senderID, NodeType::Unassigned, senderSockAddr, senderSockAddr, false, false);
matchingNode = DependencyManager::get<LimitedNodeList>()->addOrUpdateNode(senderID, NodeType::Unassigned,
senderSockAddr, senderSockAddr);
auto childData = std::unique_ptr<AssignmentClientChildData>
{ new AssignmentClientChildData(Assignment::Type::AllTypes) };

View file

@ -235,7 +235,7 @@ void AssetServer::handleGetAllMappingOperation(ReceivedMessage& message, SharedN
}
void AssetServer::handleSetMappingOperation(ReceivedMessage& message, SharedNodePointer senderNode, NLPacketList& replyPacket) {
if (senderNode->getCanRez()) {
if (senderNode->getCanWriteToAssetServer()) {
QString assetPath = message.readString();
auto assetHash = message.read(SHA256_HASH_LENGTH).toHex();
@ -251,7 +251,7 @@ void AssetServer::handleSetMappingOperation(ReceivedMessage& message, SharedNode
}
void AssetServer::handleDeleteMappingsOperation(ReceivedMessage& message, SharedNodePointer senderNode, NLPacketList& replyPacket) {
if (senderNode->getCanRez()) {
if (senderNode->getCanWriteToAssetServer()) {
int numberOfDeletedMappings { 0 };
message.readPrimitive(&numberOfDeletedMappings);
@ -272,7 +272,7 @@ void AssetServer::handleDeleteMappingsOperation(ReceivedMessage& message, Shared
}
void AssetServer::handleRenameMappingOperation(ReceivedMessage& message, SharedNodePointer senderNode, NLPacketList& replyPacket) {
if (senderNode->getCanRez()) {
if (senderNode->getCanWriteToAssetServer()) {
QString oldPath = message.readString();
QString newPath = message.readString();
@ -337,7 +337,7 @@ void AssetServer::handleAssetGet(QSharedPointer<ReceivedMessage> message, Shared
void AssetServer::handleAssetUpload(QSharedPointer<ReceivedMessage> message, SharedNodePointer senderNode) {
if (senderNode->getCanRez()) {
if (senderNode->getCanWriteToAssetServer()) {
qDebug() << "Starting an UploadAssetTask for upload from" << uuidStringWithoutCurlyBraces(senderNode->getUUID());
auto task = new UploadAssetTask(message, senderNode, _filesDirectory);

View file

@ -268,6 +268,14 @@ void EntityServer::readAdditionalConfiguration(const QJsonObject& settingsSectio
qDebug("wantTerseEditLogging=%s", debug::valueOf(wantTerseEditLogging));
EntityTreePointer tree = std::static_pointer_cast<EntityTree>(_tree);
int maxTmpEntityLifetime;
if (readOptionInt("maxTmpLifetime", settingsSectionObject, maxTmpEntityLifetime)) {
tree->setEntityMaxTmpLifetime(maxTmpEntityLifetime);
} else {
tree->setEntityMaxTmpLifetime(EntityTree::DEFAULT_MAX_TMP_ENTITY_LIFETIME);
}
tree->setWantEditLogging(wantEditLogging);
tree->setWantTerseEditLogging(wantTerseEditLogging);
}

View file

@ -1,5 +1,5 @@
{
"version": 1.3,
"version": 1.4,
"settings": [
{
"name": "metaverse",
@ -56,6 +56,7 @@
"label": "Paths",
"help": "Clients can enter a path to reach an exact viewpoint in your domain.<br/>Add rows to the table below to map a path to a viewpoint.<br/>The index path ( / ) is where clients will enter if they do not enter an explicit path.",
"type": "table",
"can_add_new_rows": true,
"key": {
"name": "path",
"label": "Path",
@ -157,27 +158,6 @@
"help": "Password used for basic HTTP authentication. Leave this blank if you do not want to change it.",
"value-hidden": true
},
{
"name": "restricted_access",
"type": "checkbox",
"label": "Restricted Access",
"default": false,
"help": "Only users listed in \"Allowed Users\" can enter your domain."
},
{
"name": "allowed_users",
"type": "table",
"label": "Allowed Users",
"help": "You can always connect from the domain-server machine.",
"numbered": false,
"columns": [
{
"name": "username",
"label": "Username",
"can_set": true
}
]
},
{
"name": "maximum_user_capacity",
"label": "Maximum User Capacity",
@ -187,25 +167,141 @@
"advanced": false
},
{
"name": "allowed_editors",
"name": "standard_permissions",
"type": "table",
"label": "Allowed Editors",
"help": "List the High Fidelity names for people you want to be able lock or unlock entities in this domain.<br/>An empty list means everyone.",
"numbered": false,
"label": "Domain-Wide User Permissions",
"help": "Indicate which users or groups can have which <a data-toggle='tooltip' data-html=true title='<p><strong>Domain-Wide User Permissions</strong></p><ul><li><strong>Connect</strong><br />Sets whether a user can connect to the the domain.</li><li><strong>Lock / Unlock</strong><br />Sets whether a user change the &ldquo;locked&rdquo; property of an entity (either from on to off or off to on).</li><li><strong>Rez</strong><br />Sets whether a user can create new entities.</li><li><strong>Rez Temporary</strong><br />Sets whether a user can create new entities with a finite lifetime.</li><li><strong>Write Assets</strong><br />Sets whether a user can make changes to the domain&rsquo;s asset-server assets.</li><li><strong>Ignore Max Capacity</strong><br />Sets whether a user can connect even if the domain has reached or exceeded its maximum allowed agents.</li></ul><p>Note that permissions assigned to a specific user will supersede any parameter-level permissions that might otherwise apply to that user. Additionally, if more than one parameter is applicable to a given user, the permissions given to that user will be the sum of all applicable parameters. For example, let&rsquo;s say only localhost users can connect and only logged in users can lock and unlock entities. If a user is both logged in and on localhost then they will be able to both connect and lock/unlock entities.</p>'>domain-wide permissions</a>.",
"caption": "Standard Permissions",
"can_add_new_rows": false,
"groups": [
{
"label": "User / Group",
"span": 1
},
{
"label": "Permissions <a data-toggle='tooltip' data-html='true' title='<p><strong>Domain-Wide User Permissions</strong></p><ul><li><strong>Connect</strong><br />Sets whether a user can connect to the the domain.</li><li><strong>Lock / Unlock</strong><br />Sets whether a user change the &ldquo;locked&rdquo; property of an entity (either from on to off or off to on).</li><li><strong>Rez</strong><br />Sets whether a user can create new entities.</li><li><strong>Rez Temporary</strong><br />Sets whether a user can create new entities with a finite lifetime.</li><li><strong>Write Assets</strong><br />Sets whether a user can make changes to the domain&rsquo;s asset-server assets.</li><li><strong>Ignore Max Capacity</strong><br />Sets whether a user can connect even if the domain has reached or exceeded its maximum allowed agents.</li></ul><p>Note that permissions assigned to a specific user will supersede any parameter-level permissions that might otherwise apply to that user. Additionally, if more than one parameter is applicable to a given user, the permissions given to that user will be the sum of all applicable parameters. For example, let&rsquo;s say only localhost users can connect and only logged in users can lock and unlock entities. If a user is both logged in and on localhost then they will be able to both connect and lock/unlock entities.</p>'>?</a>",
"span": 6
}
],
"columns": [
{
"name": "username",
"label": "Username",
"can_set": true
"name": "permissions_id",
"label": ""
},
{
"name": "id_can_connect",
"label": "Connect",
"type": "checkbox",
"editable": true,
"default": true
},
{
"name": "id_can_adjust_locks",
"label": "Lock / Unlock",
"type": "checkbox",
"editable": true,
"default": false
},
{
"name": "id_can_rez",
"label": "Rez",
"type": "checkbox",
"editable": true,
"default": false
},
{
"name": "id_can_rez_tmp",
"label": "Rez Temporary",
"type": "checkbox",
"editable": true,
"default": false
},
{
"name": "id_can_write_to_asset_server",
"label": "Write Assets",
"type": "checkbox",
"editable": true,
"default": false
},
{
"name": "id_can_connect_past_max_capacity",
"label": "Ignore Max Capacity",
"type": "checkbox",
"editable": true,
"default": false
}
]
],
"non-deletable-row-key": "permissions_id",
"non-deletable-row-values": ["localhost", "anonymous", "logged-in"]
},
{
"name": "editors_are_rezzers",
"type": "checkbox",
"label": "Only Editors Can Create Entities",
"help": "Only users listed in \"Allowed Editors\" can create new entites.",
"default": false
"name": "permissions",
"type": "table",
"caption": "Permissions for Specific Users",
"can_add_new_rows": true,
"groups": [
{
"label": "User / Group",
"span": 1
},
{
"label": "Permissions <a data-toggle='tooltip' data-html='true' title='<p><strong>Domain-Wide User Permissions</strong></p><ul><li><strong>Connect</strong><br />Sets whether a user can connect to the the domain.</li><li><strong>Lock / Unlock</strong><br />Sets whether a user change the &ldquo;locked&rdquo; property of an entity (either from on to off or off to on).</li><li><strong>Rez</strong><br />Sets whether a user can create new entities.</li><li><strong>Rez Temporary</strong><br />Sets whether a user can create new entities with a finite lifetime.</li><li><strong>Write Assets</strong><br />Sets whether a user can make changes to the domain&rsquo;s asset-server assets.</li><li><strong>Ignore Max Capacity</strong><br />Sets whether a user can connect even if the domain has reached or exceeded its maximum allowed agents.</li></ul><p>Note that permissions assigned to a specific user will supersede any parameter-level permissions that might otherwise apply to that user. Additionally, if more than one parameter is applicable to a given user, the permissions given to that user will be the sum of all applicable parameters. For example, let&rsquo;s say only localhost users can connect and only logged in users can lock and unlock entities. If a user is both logged in and on localhost then they will be able to both connect and lock/unlock entities.</p>'>?</a>",
"span": 6
}
],
"columns": [
{
"name": "permissions_id",
"label": ""
},
{
"name": "id_can_connect",
"label": "Connect",
"type": "checkbox",
"editable": true,
"default": true
},
{
"name": "id_can_adjust_locks",
"label": "Lock / Unlock",
"type": "checkbox",
"editable": true,
"default": false
},
{
"name": "id_can_rez",
"label": "Rez",
"type": "checkbox",
"editable": true,
"default": false
},
{
"name": "id_can_rez_tmp",
"label": "Rez Temporary",
"type": "checkbox",
"editable": true,
"default": false
},
{
"name": "id_can_write_to_asset_server",
"label": "Write Assets",
"type": "checkbox",
"editable": true,
"default": false
},
{
"name": "id_can_connect_past_max_capacity",
"label": "Ignore Max Capacity",
"type": "checkbox",
"editable": true,
"default": false
}
]
}
]
},
@ -218,6 +314,8 @@
"type": "table",
"label": "Persistent Scripts",
"help": "Add the URLs for scripts that you would like to ensure are always running in your domain.",
"can_add_new_rows": true,
"columns": [
{
"name": "url",
@ -302,6 +400,8 @@
"label": "Zones",
"help": "In this table you can define a set of zones in which you can specify various audio properties.",
"numbered": false,
"can_add_new_rows": true,
"key": {
"name": "name",
"label": "Name",
@ -353,6 +453,8 @@
"help": "In this table you can set custom attenuation coefficients between audio zones",
"numbered": true,
"can_order": true,
"can_add_new_rows": true,
"columns": [
{
"name": "source",
@ -380,6 +482,8 @@
"label": "Reverb Settings",
"help": "In this table you can set reverb levels for audio zones. For a medium-sized (e.g., 100 square meter) meeting room, try a decay time of around 1.5 seconds and a wet/dry mix of 25%. For an airplane hangar or cathedral, try a decay time of 4 seconds and a wet/dry mix of 50%.",
"numbered": true,
"can_add_new_rows": true,
"columns": [
{
"name": "zone",
@ -479,6 +583,14 @@
"label": "Entity Server Settings",
"assignment-types": [6],
"settings": [
{
"name": "maxTmpLifetime",
"label": "Maximum Lifetime of Temporary Entities",
"help": "The maximum number of seconds for the lifetime of an entity which will be considered \"temporary\".",
"placeholder": "3600",
"default": "3600",
"advanced": true
},
{
"name": "persistFilePath",
"label": "Entities File Path",
@ -501,6 +613,8 @@
"label": "Backup Rules",
"help": "In this table you can define a set of rules for how frequently to backup copies of your entites content file.",
"numbered": false,
"can_add_new_rows": true,
"default": [
{"Name":"Half Hourly Rolling","backupInterval":1800,"format":".backup.halfhourly.%N","maxBackupVersions":5},
{"Name":"Daily Rolling","backupInterval":86400,"format":".backup.daily.%N","maxBackupVersions":7},

View file

@ -20,6 +20,17 @@ body {
top: 40px;
}
.table .value-row td, .table .inputs td {
vertical-align: middle;
}
.table .table-checkbox {
/* Fix IE sizing checkboxes to fill table cell */
width: auto;
margin-left: auto;
margin-right: auto;
}
.glyphicon-remove {
font-size: 24px;
}
@ -107,6 +118,58 @@ table {
word-wrap: break-word;
}
caption {
color: #333;
font-weight: 700;
padding-top: 0;
}
table > tbody > .headers > td {
vertical-align: middle;
}
table .headers + .headers td {
font-size: 13px;
color: #222;
}
table[name="security.standard_permissions"] .headers td + td, table[name="security.permissions"] .headers td + td {
text-align: center;
}
.tooltip.top .tooltip-arrow {
border-top-color: #fff;
border-width: 10px 10px 0;
margin-bottom: -5px;
}
.tooltip-inner {
padding: 20px 20px 10px 20px;
font-size: 14px;
text-align: left;
color: #333;
background-color: #fff;
box-shadow: 0 3px 8px 8px #e8e8e8;
}
.tooltip.in {
opacity: 1;
}
.tooltip-inner ul {
padding-left: 0;
margin-bottom: 15px;
}
.tooltip-inner li {
list-style-type: none;
margin-bottom: 5px;
}
#security .tooltip-inner {
max-width: 520px;
}
#xs-advanced-container {
margin-bottom: 20px;
}

View file

@ -232,6 +232,17 @@ $(document).ready(function(){
badgeSidebarForDifferences($(this));
});
// Bootstrap switch in table
$('#' + Settings.FORM_ID).on('change', 'input.table-checkbox', function () {
// Bootstrap switches in table: set the changed data attribute for all rows in table.
var row = $(this).closest('tr');
if (row.hasClass("value-row")) { // Don't set attribute on input row switches prior to it being added to table.
row.find('td.' + Settings.DATA_COL_CLASS + ' input').attr('data-changed', true);
updateDataChangedForSiblingRows(row, true);
badgeSidebarForDifferences($(this));
}
});
$('.advanced-toggle').click(function(){
Settings.showAdvanced = !Settings.showAdvanced
var advancedSelector = $('.' + Settings.ADVANCED_CLASS)
@ -841,6 +852,8 @@ function reloadSettings(callback) {
// setup any bootstrap switches
$('.toggle-checkbox').bootstrapSwitch();
$('[data-toggle="tooltip"]').tooltip();
// add tooltip to locked settings
$('label.locked').tooltip({
placement: 'right',
@ -875,6 +888,7 @@ function saveSettings() {
}
}
console.log("----- SAVING ------");
console.log(formJSON);
// re-enable all inputs
@ -908,10 +922,33 @@ function makeTable(setting, keypath, setting_value, isLocked) {
html += "<span class='help-block'>" + setting.help + "</span>"
}
var nonDeletableRowKey = setting["non-deletable-row-key"];
var nonDeletableRowValues = setting["non-deletable-row-values"];
html += "<table class='table table-bordered " + (isLocked ? "locked-table" : "") + "' data-short-name='" + setting.name
+ "' name='" + keypath + "' id='" + (typeof setting.html_id !== 'undefined' ? setting.html_id : keypath)
+ "' data-setting-type='" + (isArray ? 'array' : 'hash') + "'>";
if (setting.caption) {
html += "<caption>" + setting.caption + "</caption>"
}
// Column groups
if (setting.groups) {
html += "<tr class='headers'>"
_.each(setting.groups, function (group) {
html += "<td colspan='" + group.span + "'><strong>" + group.label + "</strong></td>"
})
if (!isLocked && !setting.read_only) {
if (setting.can_order) {
html += "<td class='" + Settings.REORDER_BUTTONS_CLASSES +
"'><a href='javascript:void(0);' class='glyphicon glyphicon-sort'></a></td>";
}
html += "<td class='" + Settings.ADD_DEL_BUTTONS_CLASSES + "'></td></tr>"
}
html += "</tr>"
}
// Column names
html += "<tr class='headers'>"
@ -950,6 +987,8 @@ function makeTable(setting, keypath, setting_value, isLocked) {
html += "<td class='key'>" + rowIndexOrName + "</td>"
}
var isNonDeletableRow = false;
_.each(setting.columns, function(col) {
if (isArray) {
@ -961,16 +1000,19 @@ function makeTable(setting, keypath, setting_value, isLocked) {
colName = keypath + "." + rowIndexOrName + "." + col.name;
}
// setup the td for this column
html += "<td class='" + Settings.DATA_COL_CLASS + "' name='" + colName + "'>";
isNonDeletableRow = isNonDeletableRow
|| (nonDeletableRowKey === col.name && nonDeletableRowValues.indexOf(colValue) !== -1);
// add the actual value to the td so it is displayed
html += colValue;
if (isArray && col.type === "checkbox" && col.editable) {
html += "<td class='" + Settings.DATA_COL_CLASS + "'name='" + col.name + "'>"
+ "<input type='checkbox' class='form-control table-checkbox' "
+ "name='" + colName + "'" + (colValue ? " checked" : "") + " /></td>";
} else {
// Use a hidden input so that the values are posted.
html += "<td class='" + Settings.DATA_COL_CLASS + "' name='" + colName + "'>"
+ colValue + "<input type='hidden' name='" + colName + "' value='" + colValue + "'/></td>";
}
// for values to be posted properly we add a hidden input to this td
html += "<input type='hidden' name='" + colName + "' value='" + colValue + "'/>";
html += "</td>";
})
if (!isLocked && !setting.read_only) {
@ -979,8 +1021,12 @@ function makeTable(setting, keypath, setting_value, isLocked) {
"'><a href='javascript:void(0);' class='" + Settings.MOVE_UP_SPAN_CLASSES + "'></a>"
+ "<a href='javascript:void(0);' class='" + Settings.MOVE_DOWN_SPAN_CLASSES + "'></a></td>"
}
html += "<td class='" + Settings.ADD_DEL_BUTTONS_CLASSES +
"'><a href='javascript:void(0);' class='" + Settings.DEL_ROW_SPAN_CLASSES + "'></a></td>"
if (isNonDeletableRow) {
html += "<td></td>";
} else {
html += "<td class='" + Settings.ADD_DEL_BUTTONS_CLASSES
+ "'><a href='javascript:void(0);' class='" + Settings.DEL_ROW_SPAN_CLASSES + "'></a></td>";
}
}
html += "</tr>"
@ -990,7 +1036,7 @@ function makeTable(setting, keypath, setting_value, isLocked) {
}
// populate inputs in the table for new values
if (!isLocked && !setting.read_only) {
if (!isLocked && !setting.read_only && setting.can_add_new_rows) {
html += makeTableInputs(setting)
}
html += "</table>"
@ -1012,17 +1058,23 @@ function makeTableInputs(setting) {
}
_.each(setting.columns, function(col) {
html += "<td class='" + Settings.DATA_COL_CLASS + "'name='" + col.name + "'>\
<input type='text' class='form-control' placeholder='" + (col.placeholder ? col.placeholder : "") + "'\
value='" + (col.default ? col.default : "") + "' data-default='" + (col.default ? col.default : "") + "'>\
</td>"
if (col.type === "checkbox") {
html += "<td class='" + Settings.DATA_COL_CLASS + "'name='" + col.name + "'>"
+ "<input type='checkbox' class='form-control table-checkbox' "
+ "name='" + col.name + "'" + (col.default ? " checked" : "") + "/></td>";
} else {
html += "<td class='" + Settings.DATA_COL_CLASS + "'name='" + col.name + "'>\
<input type='text' class='form-control' placeholder='" + (col.placeholder ? col.placeholder : "") + "'\
value='" + (col.default ? col.default : "") + "' data-default='" + (col.default ? col.default : "") + "'>\
</td>"
}
})
if (setting.can_order) {
html += "<td class='" + Settings.REORDER_BUTTONS_CLASSES + "'></td>"
}
html += "<td class='" + Settings.ADD_DEL_BUTTONS_CLASSES +
"'><a href='javascript:void(0);' class='glyphicon glyphicon-plus " + Settings.ADD_ROW_BUTTON_CLASS + "'></a></td>"
html += "<td class='" + Settings.ADD_DEL_BUTTONS_CLASSES +
"'><a href='javascript:void(0);' class='glyphicon glyphicon-plus " + Settings.ADD_ROW_BUTTON_CLASS + "'></a></td>"
html += "</tr>"
return html
@ -1127,11 +1179,11 @@ function addTableRow(add_glyphicon) {
} else {
$(element).html(1)
}
} else if ($(element).hasClass(Settings.REORDER_BUTTONS_CLASS)) {
$(element).html("<td class='" + Settings.REORDER_BUTTONS_CLASSES + "'><a href='javascript:void(0);'"
+ " class='" + Settings.MOVE_UP_SPAN_CLASSES + "'></a><a href='javascript:void(0);' class='"
+ Settings.MOVE_DOWN_SPAN_CLASSES + "'></span></td>")
} else if ($(element).hasClass(Settings.ADD_DEL_BUTTONS_CLASS)) {
} else if ($(element).hasClass(Settings.REORDER_BUTTONS_CLASS)) {
$(element).html("<td class='" + Settings.REORDER_BUTTONS_CLASSES + "'><a href='javascript:void(0);'"
+ " class='" + Settings.MOVE_UP_SPAN_CLASSES + "'></a><a href='javascript:void(0);' class='"
+ Settings.MOVE_DOWN_SPAN_CLASSES + "'></span></td>")
} else if ($(element).hasClass(Settings.ADD_DEL_BUTTONS_CLASS)) {
// Change buttons
var anchor = $(element).children("a")
anchor.removeClass(Settings.ADD_ROW_SPAN_CLASSES)
@ -1142,8 +1194,20 @@ function addTableRow(add_glyphicon) {
input.remove()
} else if ($(element).hasClass(Settings.DATA_COL_CLASS)) {
// Hide inputs
var input = $(element).children("input")
input.attr("type", "hidden")
var input = $(element).find("input")
var isCheckbox = false;
if (input.hasClass("table-checkbox")) {
input = $(input).parent();
isCheckbox = true;
}
var val = input.val();
if (isCheckbox) {
val = $(input).find("input").is(':checked');
// don't hide the checkbox
} else {
input.attr("type", "hidden")
}
if (isArray) {
var row_index = row.siblings('.' + Settings.DATA_ROW_CLASS).length
@ -1152,14 +1216,22 @@ function addTableRow(add_glyphicon) {
// are there multiple columns or just one?
// with multiple we have an array of Objects, with one we have an array of whatever the value type is
var num_columns = row.children('.' + Settings.DATA_COL_CLASS).length
input.attr("name", setting_name + "[" + row_index + "]" + (num_columns > 1 ? "." + key : ""))
if (isCheckbox) {
$(input).find("input").attr("name", setting_name + "[" + row_index + "]" + (num_columns > 1 ? "." + key : ""))
} else {
input.attr("name", setting_name + "[" + row_index + "]" + (num_columns > 1 ? "." + key : ""))
}
} else {
input.attr("name", full_name + "." + $(element).attr("name"))
}
input.attr("data-changed", "true")
$(element).append(input.val())
if (isCheckbox) {
$(input).find("input").attr("data-changed", "true");
} else {
input.attr("data-changed", "true");
$(element).append(val);
}
} else {
console.log("Unknown table element")
}

View file

@ -26,7 +26,7 @@ using SharedAssignmentPointer = QSharedPointer<Assignment>;
DomainGatekeeper::DomainGatekeeper(DomainServer* server) :
_server(server)
{
}
void DomainGatekeeper::addPendingAssignedNode(const QUuid& nodeUUID, const QUuid& assignmentUUID,
@ -38,7 +38,7 @@ void DomainGatekeeper::addPendingAssignedNode(const QUuid& nodeUUID, const QUuid
QUuid DomainGatekeeper::assignmentUUIDForPendingAssignment(const QUuid& tempUUID) {
auto it = _pendingAssignedNodes.find(tempUUID);
if (it != _pendingAssignedNodes.end()) {
return it->second.getAssignmentUUID();
} else {
@ -65,53 +65,53 @@ void DomainGatekeeper::processConnectRequestPacket(QSharedPointer<ReceivedMessag
sendProtocolMismatchConnectionDenial(message->getSenderSockAddr());
return;
}
if (nodeConnection.localSockAddr.isNull() || nodeConnection.publicSockAddr.isNull()) {
qDebug() << "Unexpected data received for node local socket or public socket. Will not allow connection.";
return;
}
static const NodeSet VALID_NODE_TYPES {
NodeType::AudioMixer, NodeType::AvatarMixer, NodeType::AssetServer, NodeType::EntityServer, NodeType::Agent, NodeType::MessagesMixer
};
if (!VALID_NODE_TYPES.contains(nodeConnection.nodeType)) {
qDebug() << "Received an invalid node type with connect request. Will not allow connection from"
<< nodeConnection.senderSockAddr << ": " << nodeConnection.nodeType;
return;
}
// check if this connect request matches an assignment in the queue
auto pendingAssignment = _pendingAssignedNodes.find(nodeConnection.connectUUID);
SharedNodePointer node;
if (pendingAssignment != _pendingAssignedNodes.end()) {
node = processAssignmentConnectRequest(nodeConnection, pendingAssignment->second);
} else if (!STATICALLY_ASSIGNED_NODES.contains(nodeConnection.nodeType)) {
QString username;
QByteArray usernameSignature;
if (message->getBytesLeftToRead() > 0) {
// read username from packet
packetStream >> username;
if (message->getBytesLeftToRead() > 0) {
// read user signature from packet
packetStream >> usernameSignature;
}
}
node = processAgentConnectRequest(nodeConnection, username, usernameSignature);
}
if (node) {
// set the sending sock addr and node interest set on this node
DomainServerNodeData* nodeData = reinterpret_cast<DomainServerNodeData*>(node->getLinkedData());
nodeData->setSendingSockAddr(message->getSenderSockAddr());
nodeData->setNodeInterestSet(nodeConnection.interestList.toSet());
nodeData->setPlaceName(nodeConnection.placeName);
// signal that we just connected a node so the DomainServer can get it a list
// and broadcast its presence right away
emit connectedNode(node);
@ -120,18 +120,74 @@ void DomainGatekeeper::processConnectRequestPacket(QSharedPointer<ReceivedMessag
}
}
void DomainGatekeeper::updateNodePermissions() {
// If the permissions were changed on the domain-server webpage (and nothing else was), a restart isn't required --
// we reprocess the permissions map and update the nodes here. The node list is frequently sent out to all
// the connected nodes, so these changes are propagated to other nodes.
QList<SharedNodePointer> nodesToKill;
auto limitedNodeList = DependencyManager::get<LimitedNodeList>();
limitedNodeList->eachNodeBreakable([this, limitedNodeList, &nodesToKill](const SharedNodePointer& node){
QString username = node->getPermissions().getUserName();
NodePermissions userPerms(username);
if (node->getPermissions().isAssignment) {
// this node is an assignment-client
userPerms.isAssignment = true;
userPerms.canAdjustLocks = true;
userPerms.canRezPermanentEntities = true;
userPerms.canRezTemporaryEntities = true;
} else {
// this node is an agent
userPerms.setAll(false);
const QHostAddress& addr = node->getLocalSocket().getAddress();
bool isLocalUser = (addr == limitedNodeList->getLocalSockAddr().getAddress() ||
addr == QHostAddress::LocalHost);
if (isLocalUser) {
userPerms |= _server->_settingsManager.getStandardPermissionsForName(NodePermissions::standardNameLocalhost);
}
if (username.isEmpty()) {
userPerms |= _server->_settingsManager.getStandardPermissionsForName(NodePermissions::standardNameAnonymous);
} else {
if (_server->_settingsManager.havePermissionsForName(username)) {
userPerms = _server->_settingsManager.getPermissionsForName(username);
} else {
userPerms |= _server->_settingsManager.getStandardPermissionsForName(NodePermissions::standardNameLoggedIn);
}
}
}
node->setPermissions(userPerms);
if (!userPerms.canConnectToDomain) {
qDebug() << "node" << node->getUUID() << "no longer has permission to connect.";
// hang up on this node
nodesToKill << node;
}
return true;
});
foreach (auto node, nodesToKill) {
emit killNode(node);
}
}
SharedNodePointer DomainGatekeeper::processAssignmentConnectRequest(const NodeConnectionData& nodeConnection,
const PendingAssignedNodeData& pendingAssignment) {
// make sure this matches an assignment the DS told us we sent out
auto it = _pendingAssignedNodes.find(nodeConnection.connectUUID);
SharedAssignmentPointer matchingQueuedAssignment = SharedAssignmentPointer();
if (it != _pendingAssignedNodes.end()) {
// find the matching queued static assignment in DS queue
matchingQueuedAssignment = _server->dequeueMatchingAssignment(it->second.getAssignmentUUID(), nodeConnection.nodeType);
if (matchingQueuedAssignment) {
qDebug() << "Assignment deployed with" << uuidStringWithoutCurlyBraces(nodeConnection.connectUUID)
<< "matches unfulfilled assignment"
@ -146,124 +202,99 @@ SharedNodePointer DomainGatekeeper::processAssignmentConnectRequest(const NodeCo
qDebug() << "No assignment was deployed with UUID" << uuidStringWithoutCurlyBraces(nodeConnection.connectUUID);
return SharedNodePointer();
}
// add the new node
SharedNodePointer newNode = addVerifiedNodeFromConnectRequest(nodeConnection);
DomainServerNodeData* nodeData = reinterpret_cast<DomainServerNodeData*>(newNode->getLinkedData());
// set assignment related data on the linked data for this node
nodeData->setAssignmentUUID(matchingQueuedAssignment->getUUID());
nodeData->setWalletUUID(it->second.getWalletUUID());
nodeData->setNodeVersion(it->second.getNodeVersion());
nodeData->setWasAssigned(true);
// cleanup the PendingAssignedNodeData for this assignment now that it's connecting
_pendingAssignedNodes.erase(it);
// always allow assignment clients to create and destroy entities
newNode->setIsAllowedEditor(true);
newNode->setCanRez(true);
NodePermissions userPerms;
userPerms.isAssignment = true;
userPerms.canAdjustLocks = true;
userPerms.canRezPermanentEntities = true;
userPerms.canRezTemporaryEntities = true;
newNode->setPermissions(userPerms);
return newNode;
}
const QString MAXIMUM_USER_CAPACITY = "security.maximum_user_capacity";
const QString ALLOWED_EDITORS_SETTINGS_KEYPATH = "security.allowed_editors";
const QString EDITORS_ARE_REZZERS_KEYPATH = "security.editors_are_rezzers";
SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnectionData& nodeConnection,
const QString& username,
const QByteArray& usernameSignature) {
auto limitedNodeList = DependencyManager::get<LimitedNodeList>();
bool isRestrictingAccess =
_server->_settingsManager.valueOrDefaultValueForKeyPath(RESTRICTED_ACCESS_SETTINGS_KEYPATH).toBool();
// check if this user is on our local machine - if this is true they are always allowed to connect
// start with empty permissions
NodePermissions userPerms(username);
userPerms.setAll(false);
// check if this user is on our local machine - if this is true set permissions to those for a "localhost" connection
QHostAddress senderHostAddress = nodeConnection.senderSockAddr.getAddress();
bool isLocalUser =
(senderHostAddress == limitedNodeList->getLocalSockAddr().getAddress() || senderHostAddress == QHostAddress::LocalHost);
// if we're using restricted access and this user is not local make sure we got a user signature
if (isRestrictingAccess && !isLocalUser) {
if (!username.isEmpty()) {
if (usernameSignature.isEmpty()) {
// if user didn't include usernameSignature in connect request, send a connectionToken packet
sendConnectionTokenPacket(username, nodeConnection.senderSockAddr);
// ask for their public key right now to make sure we have it
requestUserPublicKey(username);
return SharedNodePointer();
}
}
if (isLocalUser) {
userPerms |= _server->_settingsManager.getStandardPermissionsForName(NodePermissions::standardNameLocalhost);
qDebug() << "user-permissions: is local user, so:" << userPerms;
}
bool verifiedUsername = false;
// if we do not have a local user we need to subject them to our verification and capacity checks
if (!isLocalUser) {
// check if we need to look at the username signature
if (isRestrictingAccess) {
if (isVerifiedAllowedUser(username, usernameSignature, nodeConnection.senderSockAddr)) {
// we verified the user via their username and signature - set the verifiedUsername
// so we don't re-decrypt their sig if we're trying to exempt them from max capacity check (due to
// being in the allowed editors list)
verifiedUsername = true;
} else {
// failed to verify user - return a null shared ptr
return SharedNodePointer();
}
}
if (!isWithinMaxCapacity(username, usernameSignature, verifiedUsername, nodeConnection.senderSockAddr)) {
// we can't allow this user to connect because we are at max capacity (and they either aren't an allowed editor
// or couldn't be verified as one)
if (!username.isEmpty() && usernameSignature.isEmpty()) {
// user is attempting to prove their identity to us, but we don't have enough information
sendConnectionTokenPacket(username, nodeConnection.senderSockAddr);
// ask for their public key right now to make sure we have it
requestUserPublicKey(username);
if (!userPerms.canConnectToDomain) {
return SharedNodePointer();
}
}
// if this user is in the editors list (or if the editors list is empty) set the user's node's isAllowedEditor to true
const QVariant* allowedEditorsVariant =
valueForKeyPath(_server->_settingsManager.getSettingsMap(), ALLOWED_EDITORS_SETTINGS_KEYPATH);
QStringList allowedEditors = allowedEditorsVariant ? allowedEditorsVariant->toStringList() : QStringList();
// if the allowed editors list is empty then everyone can adjust locks
bool isAllowedEditor = allowedEditors.empty();
if (allowedEditors.contains(username, Qt::CaseInsensitive)) {
// we have a non-empty allowed editors list - check if this user is verified to be in it
if (!verifiedUsername) {
if (!verifyUserSignature(username, usernameSignature, HifiSockAddr())) {
// failed to verify a user that is in the allowed editors list
// TODO: fix public key refresh in interface/metaverse and force this check
qDebug() << "Could not verify user" << username << "as allowed editor. In the interim this user"
<< "will be given edit rights to avoid a thrasing of public key requests and connect requests.";
}
isAllowedEditor = true;
if (username.isEmpty()) {
// they didn't tell us who they are
userPerms |= _server->_settingsManager.getStandardPermissionsForName(NodePermissions::standardNameAnonymous);
qDebug() << "user-permissions: no username, so:" << userPerms;
} else if (verifyUserSignature(username, usernameSignature, nodeConnection.senderSockAddr)) {
// they are sent us a username and the signature verifies it
userPerms.setUserName(username);
if (_server->_settingsManager.havePermissionsForName(username)) {
// we have specific permissions for this user.
userPerms = _server->_settingsManager.getPermissionsForName(username);
qDebug() << "user-permissions: specific user matches, so:" << userPerms;
} else {
// already verified this user and they are in the allowed editors list
isAllowedEditor = true;
// they are logged into metaverse, but we don't have specific permissions for them.
userPerms |= _server->_settingsManager.getStandardPermissionsForName(NodePermissions::standardNameLoggedIn);
qDebug() << "user-permissions: user is logged in, so:" << userPerms;
}
} else {
// they sent us a username, but it didn't check out
requestUserPublicKey(username);
if (!userPerms.canConnectToDomain) {
return SharedNodePointer();
}
}
// check if only editors should be able to rez entities
const QVariant* editorsAreRezzersVariant =
valueForKeyPath(_server->_settingsManager.getSettingsMap(), EDITORS_ARE_REZZERS_KEYPATH);
bool onlyEditorsAreRezzers = false;
if (editorsAreRezzersVariant) {
onlyEditorsAreRezzers = editorsAreRezzersVariant->toBool();
qDebug() << "user-permissions: final:" << userPerms;
if (!userPerms.canConnectToDomain) {
sendConnectionDeniedPacket("You lack the required permissions to connect to this domain.",
nodeConnection.senderSockAddr, DomainHandler::ConnectionRefusedReason::TooManyUsers);
return SharedNodePointer();
}
bool canRez = true;
if (onlyEditorsAreRezzers) {
canRez = isAllowedEditor;
if (!userPerms.canConnectPastMaxCapacity && !isWithinMaxCapacity()) {
// we can't allow this user to connect because we are at max capacity
sendConnectionDeniedPacket("Too many connected users.", nodeConnection.senderSockAddr,
DomainHandler::ConnectionRefusedReason::TooManyUsers);
return SharedNodePointer();
}
QUuid hintNodeID;
@ -282,24 +313,23 @@ SharedNodePointer DomainGatekeeper::processAgentConnectRequest(const NodeConnect
return true;
});
// add the connecting node (or re-use the matched one from eachNodeBreakable above)
SharedNodePointer newNode = addVerifiedNodeFromConnectRequest(nodeConnection, hintNodeID);
// set the edit rights for this user
newNode->setIsAllowedEditor(isAllowedEditor);
newNode->setCanRez(canRez);
newNode->setPermissions(userPerms);
// grab the linked data for our new node so we can set the username
DomainServerNodeData* nodeData = reinterpret_cast<DomainServerNodeData*>(newNode->getLinkedData());
// if we have a username from the connect request, set it on the DomainServerNodeData
nodeData->setUsername(username);
// also add an interpolation to DomainServerNodeData so that servers can get username in stats
nodeData->addOverrideForKey(USERNAME_UUID_REPLACEMENT_STATS_KEY,
uuidStringWithoutCurlyBraces(newNode->getUUID()), username);
return newNode;
}
@ -307,11 +337,11 @@ SharedNodePointer DomainGatekeeper::addVerifiedNodeFromConnectRequest(const Node
QUuid nodeID) {
HifiSockAddr discoveredSocket = nodeConnection.senderSockAddr;
SharedNetworkPeer connectedPeer = _icePeers.value(nodeConnection.connectUUID);
if (connectedPeer) {
// this user negotiated a connection with us via ICE, so re-use their ICE client ID
nodeID = nodeConnection.connectUUID;
if (connectedPeer->getActiveSocket()) {
// set their discovered socket to whatever the activated socket on the network peer object was
discoveredSocket = *connectedPeer->getActiveSocket();
@ -322,15 +352,15 @@ SharedNodePointer DomainGatekeeper::addVerifiedNodeFromConnectRequest(const Node
nodeID = QUuid::createUuid();
}
}
auto limitedNodeList = DependencyManager::get<LimitedNodeList>();
SharedNodePointer newNode = limitedNodeList->addOrUpdateNode(nodeID, nodeConnection.nodeType,
nodeConnection.publicSockAddr, nodeConnection.localSockAddr);
// So that we can send messages to this node at will - we need to activate the correct socket on this node now
newNode->activateMatchingOrNewSymmetricSocket(discoveredSocket);
return newNode;
}
@ -340,21 +370,21 @@ bool DomainGatekeeper::verifyUserSignature(const QString& username,
// it's possible this user can be allowed to connect, but we need to check their username signature
QByteArray publicKeyArray = _userPublicKeys.value(username);
const QUuid& connectionToken = _connectionTokenHash.value(username.toLower());
if (!publicKeyArray.isEmpty() && !connectionToken.isNull()) {
// if we do have a public key for the user, check for a signature match
const unsigned char* publicKeyData = reinterpret_cast<const unsigned char*>(publicKeyArray.constData());
// first load up the public key into an RSA struct
RSA* rsaPublicKey = d2i_RSA_PUBKEY(NULL, &publicKeyData, publicKeyArray.size());
QByteArray lowercaseUsername = username.toLower().toUtf8();
QByteArray usernameWithToken = QCryptographicHash::hash(lowercaseUsername.append(connectionToken.toRfc4122()),
QCryptographicHash::Sha256);
if (rsaPublicKey) {
int decryptResult = RSA_verify(NID_sha256,
reinterpret_cast<const unsigned char*>(usernameWithToken.constData()),
@ -362,29 +392,29 @@ bool DomainGatekeeper::verifyUserSignature(const QString& username,
reinterpret_cast<const unsigned char*>(usernameSignature.constData()),
usernameSignature.size(),
rsaPublicKey);
if (decryptResult == 1) {
qDebug() << "Username signature matches for" << username << "- allowing connection.";
qDebug() << "Username signature matches for" << username;
// free up the public key and remove connection token before we return
RSA_free(rsaPublicKey);
_connectionTokenHash.remove(username);
return true;
} else {
if (!senderSockAddr.isNull()) {
qDebug() << "Error decrypting username signature for " << username << "- denying connection.";
sendConnectionDeniedPacket("Error decrypting username signature.", senderSockAddr,
DomainHandler::ConnectionRefusedReason::LoginError);
}
// free up the public key, we don't need it anymore
RSA_free(rsaPublicKey);
}
} else {
// we can't let this user in since we couldn't convert their public key to an RSA key we could use
if (!senderSockAddr.isNull()) {
qDebug() << "Couldn't convert data to RSA key for" << username << "- denying connection.";
@ -399,86 +429,35 @@ bool DomainGatekeeper::verifyUserSignature(const QString& username,
DomainHandler::ConnectionRefusedReason::LoginError);
}
}
requestUserPublicKey(username); // no joy. maybe next time?
return false;
}
bool DomainGatekeeper::isVerifiedAllowedUser(const QString& username, const QByteArray& usernameSignature,
const HifiSockAddr& senderSockAddr) {
if (username.isEmpty()) {
qDebug() << "Connect request denied - no username provided.";
sendConnectionDeniedPacket("No username provided", senderSockAddr,
DomainHandler::ConnectionRefusedReason::LoginError);
return false;
}
QStringList allowedUsers =
_server->_settingsManager.valueOrDefaultValueForKeyPath(ALLOWED_USERS_SETTINGS_KEYPATH).toStringList();
if (allowedUsers.contains(username, Qt::CaseInsensitive)) {
if (!verifyUserSignature(username, usernameSignature, senderSockAddr)) {
return false;
}
} else {
qDebug() << "Connect request denied for user" << username << "- not in allowed users list.";
sendConnectionDeniedPacket("User not on whitelist.", senderSockAddr,
DomainHandler::ConnectionRefusedReason::NotAuthorized);
return false;
}
return true;
}
bool DomainGatekeeper::isWithinMaxCapacity(const QString& username, const QByteArray& usernameSignature,
bool& verifiedUsername,
const HifiSockAddr& senderSockAddr) {
bool DomainGatekeeper::isWithinMaxCapacity() {
// find out what our maximum capacity is
const QVariant* maximumUserCapacityVariant = valueForKeyPath(_server->_settingsManager.getSettingsMap(), MAXIMUM_USER_CAPACITY);
const QVariant* maximumUserCapacityVariant =
valueForKeyPath(_server->_settingsManager.getSettingsMap(), MAXIMUM_USER_CAPACITY);
unsigned int maximumUserCapacity = maximumUserCapacityVariant ? maximumUserCapacityVariant->toUInt() : 0;
if (maximumUserCapacity > 0) {
unsigned int connectedUsers = _server->countConnectedUsers();
if (connectedUsers >= maximumUserCapacity) {
// too many users, deny the new connection unless this user is an allowed editor
const QVariant* allowedEditorsVariant =
valueForKeyPath(_server->_settingsManager.getSettingsMap(), ALLOWED_EDITORS_SETTINGS_KEYPATH);
QStringList allowedEditors = allowedEditorsVariant ? allowedEditorsVariant->toStringList() : QStringList();
if (allowedEditors.contains(username)) {
if (verifiedUsername || verifyUserSignature(username, usernameSignature, senderSockAddr)) {
verifiedUsername = true;
qDebug() << "Above maximum capacity -" << connectedUsers << "/" << maximumUserCapacity <<
"but user" << username << "is in allowed editors list so will be allowed to connect.";
return true;
}
}
// deny connection from this user
qDebug() << connectedUsers << "/" << maximumUserCapacity << "users connected, denying new connection.";
sendConnectionDeniedPacket("Too many connected users.", senderSockAddr,
DomainHandler::ConnectionRefusedReason::TooManyUsers);
return false;
}
qDebug() << connectedUsers << "/" << maximumUserCapacity << "users connected, allowing new connection.";
}
return true;
}
void DomainGatekeeper::preloadAllowedUserPublicKeys() {
const QVariant* allowedUsersVariant = valueForKeyPath(_server->_settingsManager.getSettingsMap(), ALLOWED_USERS_SETTINGS_KEYPATH);
QStringList allowedUsers = allowedUsersVariant ? allowedUsersVariant->toStringList() : QStringList();
QStringList allowedUsers = _server->_settingsManager.getAllNames();
if (allowedUsers.size() > 0) {
// in the future we may need to limit how many requests here - for now assume that lists of allowed users are not
// going to create > 100 requests
@ -489,15 +468,20 @@ void DomainGatekeeper::preloadAllowedUserPublicKeys() {
}
void DomainGatekeeper::requestUserPublicKey(const QString& username) {
// don't request public keys for the standard psuedo-account-names
if (NodePermissions::standardNames.contains(username, Qt::CaseInsensitive)) {
return;
}
// even if we have a public key for them right now, request a new one in case it has just changed
JSONCallbackParameters callbackParams;
callbackParams.jsonCallbackReceiver = this;
callbackParams.jsonCallbackMethod = "publicKeyJSONCallback";
const QString USER_PUBLIC_KEY_PATH = "api/v1/users/%1/public_key";
qDebug() << "Requesting public key for user" << username;
DependencyManager::get<AccountManager>()->sendRequest(USER_PUBLIC_KEY_PATH.arg(username),
AccountManagerAuth::None,
QNetworkAccessManager::GetOperation, callbackParams);
@ -505,22 +489,22 @@ void DomainGatekeeper::requestUserPublicKey(const QString& username) {
void DomainGatekeeper::publicKeyJSONCallback(QNetworkReply& requestReply) {
QJsonObject jsonObject = QJsonDocument::fromJson(requestReply.readAll()).object();
if (jsonObject["status"].toString() == "success") {
// figure out which user this is for
const QString PUBLIC_KEY_URL_REGEX_STRING = "api\\/v1\\/users\\/([A-Za-z0-9_\\.]+)\\/public_key";
QRegExp usernameRegex(PUBLIC_KEY_URL_REGEX_STRING);
if (usernameRegex.indexIn(requestReply.url().toString()) != -1) {
QString username = usernameRegex.cap(1);
qDebug() << "Storing a public key for user" << username;
// pull the public key as a QByteArray from this response
const QString JSON_DATA_KEY = "data";
const QString JSON_PUBLIC_KEY_KEY = "public_key";
_userPublicKeys[username] =
QByteArray::fromBase64(jsonObject[JSON_DATA_KEY].toObject()[JSON_PUBLIC_KEY_KEY].toString().toUtf8());
}
@ -536,16 +520,16 @@ void DomainGatekeeper::sendProtocolMismatchConnectionDenial(const HifiSockAddr&
DomainHandler::ConnectionRefusedReason::ProtocolMismatch);
}
void DomainGatekeeper::sendConnectionDeniedPacket(const QString& reason, const HifiSockAddr& senderSockAddr,
void DomainGatekeeper::sendConnectionDeniedPacket(const QString& reason, const HifiSockAddr& senderSockAddr,
DomainHandler::ConnectionRefusedReason reasonCode) {
// this is an agent and we've decided we won't let them connect - send them a packet to deny connection
QByteArray utfString = reason.toUtf8();
quint16 payloadSize = utfString.size();
// setup the DomainConnectionDenied packet
auto connectionDeniedPacket = NLPacket::create(PacketType::DomainConnectionDenied,
auto connectionDeniedPacket = NLPacket::create(PacketType::DomainConnectionDenied,
payloadSize + sizeof(payloadSize) + sizeof(uint8_t));
// pack in the reason the connection was denied (the client displays this)
if (payloadSize > 0) {
uint8_t reasonCodeWire = (uint8_t)reasonCode;
@ -553,7 +537,7 @@ void DomainGatekeeper::sendConnectionDeniedPacket(const QString& reason, const H
connectionDeniedPacket->writePrimitive(payloadSize);
connectionDeniedPacket->write(utfString);
}
// send the packet off
DependencyManager::get<LimitedNodeList>()->sendPacket(std::move(connectionDeniedPacket), senderSockAddr);
}
@ -561,20 +545,20 @@ void DomainGatekeeper::sendConnectionDeniedPacket(const QString& reason, const H
void DomainGatekeeper::sendConnectionTokenPacket(const QString& username, const HifiSockAddr& senderSockAddr) {
// get the existing connection token or create a new one
QUuid& connectionToken = _connectionTokenHash[username.toLower()];
if (connectionToken.isNull()) {
connectionToken = QUuid::createUuid();
}
// setup a static connection token packet
static auto connectionTokenPacket = NLPacket::create(PacketType::DomainServerConnectionToken, NUM_BYTES_RFC4122_UUID);
// reset the packet before each time we send
connectionTokenPacket->reset();
// write the connection token
connectionTokenPacket->write(connectionToken.toRfc4122());
// send off the packet unreliably
DependencyManager::get<LimitedNodeList>()->sendUnreliablePacket(*connectionTokenPacket, senderSockAddr);
}
@ -582,33 +566,33 @@ void DomainGatekeeper::sendConnectionTokenPacket(const QString& username, const
const int NUM_PEER_PINGS_BEFORE_DELETE = 2000 / UDP_PUNCH_PING_INTERVAL_MS;
void DomainGatekeeper::pingPunchForConnectingPeer(const SharedNetworkPeer& peer) {
if (peer->getConnectionAttempts() >= NUM_PEER_PINGS_BEFORE_DELETE) {
// we've reached the maximum number of ping attempts
qDebug() << "Maximum number of ping attempts reached for peer with ID" << peer->getUUID();
qDebug() << "Removing from list of connecting peers.";
_icePeers.remove(peer->getUUID());
} else {
auto limitedNodeList = DependencyManager::get<LimitedNodeList>();
// send the ping packet to the local and public sockets for this node
auto localPingPacket = limitedNodeList->constructICEPingPacket(PingType::Local, limitedNodeList->getSessionUUID());
limitedNodeList->sendPacket(std::move(localPingPacket), peer->getLocalSocket());
auto publicPingPacket = limitedNodeList->constructICEPingPacket(PingType::Public, limitedNodeList->getSessionUUID());
limitedNodeList->sendPacket(std::move(publicPingPacket), peer->getPublicSocket());
peer->incrementConnectionAttempts();
}
}
void DomainGatekeeper::handlePeerPingTimeout() {
NetworkPeer* senderPeer = qobject_cast<NetworkPeer*>(sender());
if (senderPeer) {
SharedNetworkPeer sharedPeer = _icePeers.value(senderPeer->getUUID());
if (sharedPeer && !sharedPeer->getActiveSocket()) {
pingPunchForConnectingPeer(sharedPeer);
}
@ -619,24 +603,24 @@ void DomainGatekeeper::processICEPeerInformationPacket(QSharedPointer<ReceivedMe
// loop through the packet and pull out network peers
// any peer we don't have we add to the hash, otherwise we update
QDataStream iceResponseStream(message->getMessage());
NetworkPeer* receivedPeer = new NetworkPeer;
iceResponseStream >> *receivedPeer;
if (!_icePeers.contains(receivedPeer->getUUID())) {
qDebug() << "New peer requesting ICE connection being added to hash -" << *receivedPeer;
SharedNetworkPeer newPeer = SharedNetworkPeer(receivedPeer);
_icePeers[receivedPeer->getUUID()] = newPeer;
// make sure we know when we should ping this peer
connect(newPeer.data(), &NetworkPeer::pingTimerTimeout, this, &DomainGatekeeper::handlePeerPingTimeout);
// immediately ping the new peer, and start a timer to continue pinging it until we connect to it
newPeer->startPingTimer();
qDebug() << "Sending ping packets to establish connectivity with ICE peer with ID"
<< newPeer->getUUID();
pingPunchForConnectingPeer(newPeer);
} else {
delete receivedPeer;
@ -646,18 +630,18 @@ void DomainGatekeeper::processICEPeerInformationPacket(QSharedPointer<ReceivedMe
void DomainGatekeeper::processICEPingPacket(QSharedPointer<ReceivedMessage> message) {
auto limitedNodeList = DependencyManager::get<LimitedNodeList>();
auto pingReplyPacket = limitedNodeList->constructICEPingReplyPacket(*message, limitedNodeList->getSessionUUID());
limitedNodeList->sendPacket(std::move(pingReplyPacket), message->getSenderSockAddr());
}
void DomainGatekeeper::processICEPingReplyPacket(QSharedPointer<ReceivedMessage> message) {
QDataStream packetStream(message->getMessage());
QUuid nodeUUID;
packetStream >> nodeUUID;
SharedNetworkPeer sendingPeer = _icePeers.value(nodeUUID);
if (sendingPeer) {
// we had this NetworkPeer in our connecting list - add the right sock addr to our connected list
sendingPeer->activateMatchingOrNewSymmetricSocket(message->getSenderSockAddr());

View file

@ -53,8 +53,12 @@ public slots:
void publicKeyJSONCallback(QNetworkReply& requestReply);
signals:
void killNode(SharedNodePointer node);
void connectedNode(SharedNodePointer node);
public slots:
void updateNodePermissions();
private slots:
void handlePeerPingTimeout();
private:
@ -68,11 +72,7 @@ private:
bool verifyUserSignature(const QString& username, const QByteArray& usernameSignature,
const HifiSockAddr& senderSockAddr);
bool isVerifiedAllowedUser(const QString& username, const QByteArray& usernameSignature,
const HifiSockAddr& senderSockAddr);
bool isWithinMaxCapacity(const QString& username, const QByteArray& usernameSignature,
bool& verifiedUsername,
const HifiSockAddr& senderSockAddr);
bool isWithinMaxCapacity();
bool shouldAllowConnectionFromNode(const QString& username, const QByteArray& usernameSignature,
const HifiSockAddr& senderSockAddr);

View file

@ -101,6 +101,13 @@ DomainServer::DomainServer(int argc, char* argv[]) :
// make sure we hear about newly connected nodes from our gatekeeper
connect(&_gatekeeper, &DomainGatekeeper::connectedNode, this, &DomainServer::handleConnectedNode);
// if a connected node loses connection privileges, hang up on it
connect(&_gatekeeper, &DomainGatekeeper::killNode, this, &DomainServer::handleKillNode);
// if permissions are updated, relay the changes to the Node datastructures
connect(&_settingsManager, &DomainServerSettingsManager::updateNodePermissions,
&_gatekeeper, &DomainGatekeeper::updateNodePermissions);
if (optionallyReadX509KeyAndCertificate() && optionallySetupOAuth()) {
// we either read a certificate and private key or were not passed one
// and completed login or did not need to
@ -795,8 +802,7 @@ void DomainServer::sendDomainListToNode(const SharedNodePointer& node, const Hif
extendedHeaderStream << limitedNodeList->getSessionUUID();
extendedHeaderStream << node->getUUID();
extendedHeaderStream << (quint8) node->isAllowedEditor();
extendedHeaderStream << (quint8) node->getCanRez();
extendedHeaderStream << node->getPermissions();
auto domainListPackets = NLPacketList::create(PacketType::DomainList, extendedHeader);
@ -1088,11 +1094,12 @@ void DomainServer::sendHeartbeatToMetaverse(const QString& networkAddress) {
static const QString AUTOMATIC_NETWORKING_KEY = "automatic_networking";
domainObject[AUTOMATIC_NETWORKING_KEY] = _automaticNetworkingSetting;
// Add a flag to indicate if this domain uses restricted access -
// for now that will exclude it from listings
static const QString RESTRICTED_ACCESS_FLAG = "restricted";
domainObject[RESTRICTED_ACCESS_FLAG] =
_settingsManager.valueOrDefaultValueForKeyPath(RESTRICTED_ACCESS_SETTINGS_KEYPATH).toBool();
// add a flag to indicate if this domain uses restricted access - for now that will exclude it from listings
const QString RESTRICTED_ACCESS_FLAG = "restricted";
// consider the domain to have restricted access if "anonymous" connections can't connect to the domain.
NodePermissions anonymousPermissions = _settingsManager.getPermissionsForName(NodePermissions::standardNameAnonymous);
domainObject[RESTRICTED_ACCESS_FLAG] = !anonymousPermissions.canConnectToDomain;
// Add the metadata to the heartbeat
static const QString DOMAIN_HEARTBEAT_KEY = "heartbeat";
@ -2102,35 +2109,42 @@ void DomainServer::processPathQueryPacket(QSharedPointer<ReceivedMessage> messag
void DomainServer::processNodeDisconnectRequestPacket(QSharedPointer<ReceivedMessage> message) {
// This packet has been matched to a source node and they're asking not to be in the domain anymore
auto limitedNodeList = DependencyManager::get<LimitedNodeList>();
const QUuid& nodeUUID = message->getSourceID();
qDebug() << "Received a disconnect request from node with UUID" << nodeUUID;
// we want to check what type this node was before going to kill it so that we can avoid sending the RemovedNode
// packet to nodes that don't care about this type
auto nodeToKill = limitedNodeList->nodeWithUUID(nodeUUID);
if (nodeToKill) {
auto nodeType = nodeToKill->getType();
limitedNodeList->killNodeWithUUID(nodeUUID);
static auto removedNodePacket = NLPacket::create(PacketType::DomainServerRemovedNode, NUM_BYTES_RFC4122_UUID);
removedNodePacket->reset();
removedNodePacket->write(nodeUUID.toRfc4122());
// broadcast out the DomainServerRemovedNode message
limitedNodeList->eachMatchingNode([&nodeType](const SharedNodePointer& otherNode) -> bool {
// only send the removed node packet to nodes that care about the type of node this was
auto nodeLinkedData = dynamic_cast<DomainServerNodeData*>(otherNode->getLinkedData());
return (nodeLinkedData != nullptr) && nodeLinkedData->getNodeInterestSet().contains(nodeType);
}, [&limitedNodeList](const SharedNodePointer& otherNode){
limitedNodeList->sendUnreliablePacket(*removedNodePacket, *otherNode);
});
handleKillNode(nodeToKill);
}
}
void DomainServer::handleKillNode(SharedNodePointer nodeToKill) {
auto nodeType = nodeToKill->getType();
auto limitedNodeList = DependencyManager::get<LimitedNodeList>();
const QUuid& nodeUUID = nodeToKill->getUUID();
limitedNodeList->killNodeWithUUID(nodeUUID);
static auto removedNodePacket = NLPacket::create(PacketType::DomainServerRemovedNode, NUM_BYTES_RFC4122_UUID);
removedNodePacket->reset();
removedNodePacket->write(nodeUUID.toRfc4122());
// broadcast out the DomainServerRemovedNode message
limitedNodeList->eachMatchingNode([&nodeType](const SharedNodePointer& otherNode) -> bool {
// only send the removed node packet to nodes that care about the type of node this was
auto nodeLinkedData = dynamic_cast<DomainServerNodeData*>(otherNode->getLinkedData());
return (nodeLinkedData != nullptr) && nodeLinkedData->getNodeInterestSet().contains(nodeType);
}, [&limitedNodeList](const SharedNodePointer& otherNode){
limitedNodeList->sendUnreliablePacket(*removedNodePacket, *otherNode);
});
}
void DomainServer::processICEServerHeartbeatDenialPacket(QSharedPointer<ReceivedMessage> message) {
static const int NUM_HEARTBEAT_DENIALS_FOR_KEYPAIR_REGEN = 3;

View file

@ -114,6 +114,8 @@ private:
unsigned int countConnectedUsers();
void handleKillNode(SharedNodePointer nodeToKill);
void sendDomainListToNode(const SharedNodePointer& node, const HifiSockAddr& senderSockAddr);
QUuid connectionSecretForNodes(const SharedNodePointer& nodeA, const SharedNodePointer& nodeB);

View file

@ -9,6 +9,8 @@
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#include <algorithm>
#include <QtCore/QCoreApplication>
#include <QtCore/QDir>
#include <QtCore/QFile>
@ -26,6 +28,9 @@
#include "DomainServerSettingsManager.h"
#define WANT_DEBUG 1
const QString SETTINGS_DESCRIPTION_RELATIVE_PATH = "/resources/describe-settings.json";
const QString DESCRIPTION_SETTINGS_KEY = "settings";
@ -44,7 +49,8 @@ DomainServerSettingsManager::DomainServerSettingsManager() :
QFile descriptionFile(QCoreApplication::applicationDirPath() + SETTINGS_DESCRIPTION_RELATIVE_PATH);
descriptionFile.open(QIODevice::ReadOnly);
QJsonDocument descriptionDocument = QJsonDocument::fromJson(descriptionFile.readAll());
QJsonParseError parseError;
QJsonDocument descriptionDocument = QJsonDocument::fromJson(descriptionFile.readAll(), &parseError);
if (descriptionDocument.isObject()) {
QJsonObject descriptionObject = descriptionDocument.object();
@ -63,8 +69,8 @@ DomainServerSettingsManager::DomainServerSettingsManager() :
}
static const QString MISSING_SETTINGS_DESC_MSG =
QString("Did not find settings decription in JSON at %1 - Unable to continue. domain-server will quit.")
.arg(SETTINGS_DESCRIPTION_RELATIVE_PATH);
QString("Did not find settings decription in JSON at %1 - Unable to continue. domain-server will quit.\n%2 at %3")
.arg(SETTINGS_DESCRIPTION_RELATIVE_PATH).arg(parseError.errorString()).arg(parseError.offset);
static const int MISSING_SETTINGS_DESC_ERROR_CODE = 6;
QMetaObject::invokeMethod(QCoreApplication::instance(), "queuedQuit", Qt::QueuedConnection,
@ -88,7 +94,8 @@ void DomainServerSettingsManager::processSettingsRequestPacket(QSharedPointer<Re
}
void DomainServerSettingsManager::setupConfigMap(const QStringList& argumentList) {
_configMap.loadMasterAndUserConfig(argumentList);
_argumentList = argumentList;
_configMap.loadMasterAndUserConfig(_argumentList);
// What settings version were we before and what are we using now?
// Do we need to do any re-mapping?
@ -97,6 +104,11 @@ void DomainServerSettingsManager::setupConfigMap(const QStringList& argumentList
double oldVersion = appSettings.value(JSON_SETTINGS_VERSION_KEY, 0.0).toDouble();
if (oldVersion != _descriptionVersion) {
const QString ALLOWED_USERS_SETTINGS_KEYPATH = "security.allowed_users";
const QString RESTRICTED_ACCESS_SETTINGS_KEYPATH = "security.restricted_access";
const QString ALLOWED_EDITORS_SETTINGS_KEYPATH = "security.allowed_editors";
const QString EDITORS_ARE_REZZERS_KEYPATH = "security.editors_are_rezzers";
qDebug() << "Previous domain-server settings version was"
<< QString::number(oldVersion, 'g', 8) << "and the new version is"
<< QString::number(_descriptionVersion, 'g', 8) << "- checking if any re-mapping is required";
@ -127,7 +139,7 @@ void DomainServerSettingsManager::setupConfigMap(const QStringList& argumentList
persistToFile();
// reload the master and user config so that the merged config is right
_configMap.loadMasterAndUserConfig(argumentList);
_configMap.loadMasterAndUserConfig(_argumentList);
}
}
@ -163,7 +175,7 @@ void DomainServerSettingsManager::setupConfigMap(const QStringList& argumentList
persistToFile();
// reload the master and user config so that the merged config is right
_configMap.loadMasterAndUserConfig(argumentList);
_configMap.loadMasterAndUserConfig(_argumentList);
}
}
@ -186,15 +198,216 @@ void DomainServerSettingsManager::setupConfigMap(const QStringList& argumentList
persistToFile();
// reload the master and user config so the merged config is correct
_configMap.loadMasterAndUserConfig(argumentList);
_configMap.loadMasterAndUserConfig(_argumentList);
}
}
if (oldVersion < 1.4) {
// This was prior to the permissions-grid in the domain-server settings page
bool isRestrictedAccess = valueOrDefaultValueForKeyPath(RESTRICTED_ACCESS_SETTINGS_KEYPATH).toBool();
QStringList allowedUsers = valueOrDefaultValueForKeyPath(ALLOWED_USERS_SETTINGS_KEYPATH).toStringList();
QStringList allowedEditors = valueOrDefaultValueForKeyPath(ALLOWED_EDITORS_SETTINGS_KEYPATH).toStringList();
bool onlyEditorsAreRezzers = valueOrDefaultValueForKeyPath(EDITORS_ARE_REZZERS_KEYPATH).toBool();
_standardAgentPermissions[NodePermissions::standardNameLocalhost].reset(
new NodePermissions(NodePermissions::standardNameLocalhost));
_standardAgentPermissions[NodePermissions::standardNameLocalhost]->setAll(true);
_standardAgentPermissions[NodePermissions::standardNameAnonymous].reset(
new NodePermissions(NodePermissions::standardNameAnonymous));
_standardAgentPermissions[NodePermissions::standardNameLoggedIn].reset(
new NodePermissions(NodePermissions::standardNameLoggedIn));
if (isRestrictedAccess) {
// only users in allow-users list can connect
_standardAgentPermissions[NodePermissions::standardNameAnonymous]->canConnectToDomain = false;
_standardAgentPermissions[NodePermissions::standardNameLoggedIn]->canConnectToDomain = false;
} // else anonymous and logged-in retain default of canConnectToDomain = true
foreach (QString allowedUser, allowedUsers) {
// even if isRestrictedAccess is false, we have to add explicit rows for these users.
// defaults to canConnectToDomain = true
_agentPermissions[allowedUser].reset(new NodePermissions(allowedUser));
}
foreach (QString allowedEditor, allowedEditors) {
if (!_agentPermissions.contains(allowedEditor)) {
_agentPermissions[allowedEditor].reset(new NodePermissions(allowedEditor));
if (isRestrictedAccess) {
// they can change locks, but can't connect.
_agentPermissions[allowedEditor]->canConnectToDomain = false;
}
}
_agentPermissions[allowedEditor]->canAdjustLocks = true;
}
QList<QHash<QString, NodePermissionsPointer>> permissionsSets;
permissionsSets << _standardAgentPermissions << _agentPermissions;
foreach (auto permissionsSet, permissionsSets) {
foreach (QString userName, permissionsSet.keys()) {
if (onlyEditorsAreRezzers) {
permissionsSet[userName]->canRezPermanentEntities = permissionsSet[userName]->canAdjustLocks;
permissionsSet[userName]->canRezTemporaryEntities = permissionsSet[userName]->canAdjustLocks;
} else {
permissionsSet[userName]->canRezPermanentEntities = true;
permissionsSet[userName]->canRezTemporaryEntities = true;
}
}
}
packPermissions();
_standardAgentPermissions.clear();
_agentPermissions.clear();
}
}
unpackPermissions();
// write the current description version to our settings
appSettings.setValue(JSON_SETTINGS_VERSION_KEY, _descriptionVersion);
}
void DomainServerSettingsManager::packPermissionsForMap(QString mapName,
QHash<QString, NodePermissionsPointer> agentPermissions,
QString keyPath) {
QVariant* security = valueForKeyPath(_configMap.getUserConfig(), "security");
if (!security || !security->canConvert(QMetaType::QVariantMap)) {
security = valueForKeyPath(_configMap.getUserConfig(), "security", true);
(*security) = QVariantMap();
}
// save settings for anonymous / logged-in / localhost
QVariant* permissions = valueForKeyPath(_configMap.getUserConfig(), keyPath);
if (!permissions || !permissions->canConvert(QMetaType::QVariantList)) {
permissions = valueForKeyPath(_configMap.getUserConfig(), keyPath, true);
(*permissions) = QVariantList();
}
QVariantList* permissionsList = reinterpret_cast<QVariantList*>(permissions);
(*permissionsList).clear();
foreach (QString userName, agentPermissions.keys()) {
*permissionsList += agentPermissions[userName]->toVariant();
}
}
void DomainServerSettingsManager::packPermissions() {
// transfer details from _agentPermissions to _configMap
packPermissionsForMap("standard_permissions", _standardAgentPermissions, AGENT_STANDARD_PERMISSIONS_KEYPATH);
// save settings for specific users
packPermissionsForMap("permissions", _agentPermissions, AGENT_PERMISSIONS_KEYPATH);
persistToFile();
_configMap.loadMasterAndUserConfig(_argumentList);
}
void DomainServerSettingsManager::unpackPermissions() {
// transfer details from _configMap to _agentPermissions;
_standardAgentPermissions.clear();
_agentPermissions.clear();
bool foundLocalhost = false;
bool foundAnonymous = false;
bool foundLoggedIn = false;
bool needPack = false;
QVariant* standardPermissions = valueForKeyPath(_configMap.getUserConfig(), AGENT_STANDARD_PERMISSIONS_KEYPATH);
if (!standardPermissions || !standardPermissions->canConvert(QMetaType::QVariantList)) {
qDebug() << "failed to extract standard permissions from settings.";
standardPermissions = valueForKeyPath(_configMap.getUserConfig(), AGENT_STANDARD_PERMISSIONS_KEYPATH, true);
(*standardPermissions) = QVariantList();
}
QVariant* permissions = valueForKeyPath(_configMap.getUserConfig(), AGENT_PERMISSIONS_KEYPATH);
if (!permissions || !permissions->canConvert(QMetaType::QVariantList)) {
qDebug() << "failed to extract permissions from settings.";
permissions = valueForKeyPath(_configMap.getUserConfig(), AGENT_PERMISSIONS_KEYPATH, true);
(*permissions) = QVariantList();
}
QList<QVariant> standardPermissionsList = standardPermissions->toList();
foreach (QVariant permsHash, standardPermissionsList) {
NodePermissionsPointer perms { new NodePermissions(permsHash.toMap()) };
QString id = perms->getID();
foundLocalhost |= (id == NodePermissions::standardNameLocalhost);
foundAnonymous |= (id == NodePermissions::standardNameAnonymous);
foundLoggedIn |= (id == NodePermissions::standardNameLoggedIn);
if (_standardAgentPermissions.contains(id)) {
qDebug() << "duplicate name in standard permissions table: " << id;
_standardAgentPermissions[id] |= perms;
needPack = true;
} else {
_standardAgentPermissions[id] = perms;
}
}
QList<QVariant> permissionsList = permissions->toList();
foreach (QVariant permsHash, permissionsList) {
NodePermissionsPointer perms { new NodePermissions(permsHash.toMap()) };
QString id = perms->getID();
if (_agentPermissions.contains(id)) {
qDebug() << "duplicate name in permissions table: " << id;
_agentPermissions[id] |= perms;
needPack = true;
} else {
_agentPermissions[id] = perms;
}
}
// if any of the standard names are missing, add them
if (!foundLocalhost) {
NodePermissionsPointer perms { new NodePermissions(NodePermissions::standardNameLocalhost) };
perms->setAll(true);
_standardAgentPermissions[perms->getID()] = perms;
needPack = true;
}
if (!foundAnonymous) {
NodePermissionsPointer perms { new NodePermissions(NodePermissions::standardNameAnonymous) };
_standardAgentPermissions[perms->getID()] = perms;
needPack = true;
}
if (!foundLoggedIn) {
NodePermissionsPointer perms { new NodePermissions(NodePermissions::standardNameLoggedIn) };
_standardAgentPermissions[perms->getID()] = perms;
needPack = true;
}
if (needPack) {
packPermissions();
}
#ifdef WANT_DEBUG
qDebug() << "--------------- permissions ---------------------";
QList<QHash<QString, NodePermissionsPointer>> permissionsSets;
permissionsSets << _standardAgentPermissions << _agentPermissions;
foreach (auto permissionSet, permissionsSets) {
QHashIterator<QString, NodePermissionsPointer> i(permissionSet);
while (i.hasNext()) {
i.next();
NodePermissionsPointer perms = i.value();
qDebug() << i.key() << perms;
}
}
#endif
}
NodePermissions DomainServerSettingsManager::getStandardPermissionsForName(const QString& name) const {
if (_standardAgentPermissions.contains(name)) {
return *(_standardAgentPermissions[name].get());
}
NodePermissions nullPermissions;
nullPermissions.setAll(false);
return nullPermissions;
}
NodePermissions DomainServerSettingsManager::getPermissionsForName(const QString& name) const {
if (_agentPermissions.contains(name)) {
return *(_agentPermissions[name].get());
}
NodePermissions nullPermissions;
nullPermissions.setAll(false);
return nullPermissions;
}
QVariant DomainServerSettingsManager::valueOrDefaultValueForKeyPath(const QString& keyPath) {
const QVariant* foundValue = valueForKeyPath(_configMap.getMergedConfig(), keyPath);
@ -257,7 +470,7 @@ bool DomainServerSettingsManager::handleAuthenticatedHTTPRequest(HTTPConnection
qDebug() << "DomainServerSettingsManager postedObject -" << postedObject;
// we recurse one level deep below each group for the appropriate setting
recurseJSONObjectAndOverwriteSettings(postedObject);
bool restartRequired = recurseJSONObjectAndOverwriteSettings(postedObject);
// store whatever the current _settingsMap is to file
persistToFile();
@ -267,8 +480,13 @@ bool DomainServerSettingsManager::handleAuthenticatedHTTPRequest(HTTPConnection
connection->respond(HTTPConnection::StatusCode200, jsonSuccess.toUtf8(), "application/json");
// defer a restart to the domain-server, this gives our HTTPConnection enough time to respond
const int DOMAIN_SERVER_RESTART_TIMER_MSECS = 1000;
QTimer::singleShot(DOMAIN_SERVER_RESTART_TIMER_MSECS, qApp, SLOT(restart()));
if (restartRequired) {
const int DOMAIN_SERVER_RESTART_TIMER_MSECS = 1000;
QTimer::singleShot(DOMAIN_SERVER_RESTART_TIMER_MSECS, qApp, SLOT(restart()));
} else {
unpackPermissions();
emit updateNodePermissions();
}
return true;
} else if (connection->requestOperation() == QNetworkAccessManager::GetOperation && url.path() == SETTINGS_PATH_JSON) {
@ -282,7 +500,6 @@ bool DomainServerSettingsManager::handleAuthenticatedHTTPRequest(HTTPConnection
rootObject[SETTINGS_RESPONSE_VALUE_KEY] = responseObjectForType("", true);
rootObject[SETTINGS_RESPONSE_LOCKED_VALUES_KEY] = QJsonDocument::fromVariant(_configMap.getMasterConfig()).object();
connection->respond(HTTPConnection::StatusCode200, QJsonDocument(rootObject).toJson(), "application/json");
}
@ -458,6 +675,8 @@ void DomainServerSettingsManager::updateSetting(const QString& key, const QJsonV
// TODO: we still need to recurse here with the description in case values in the array have special types
settingMap[key] = newValue.toArray().toVariantList();
}
sortPermissions();
}
QJsonObject DomainServerSettingsManager::settingDescriptionFromGroup(const QJsonObject& groupObject, const QString& settingName) {
@ -471,9 +690,10 @@ QJsonObject DomainServerSettingsManager::settingDescriptionFromGroup(const QJson
return QJsonObject();
}
void DomainServerSettingsManager::recurseJSONObjectAndOverwriteSettings(const QJsonObject& postedObject) {
bool DomainServerSettingsManager::recurseJSONObjectAndOverwriteSettings(const QJsonObject& postedObject) {
auto& settingsVariant = _configMap.getUserConfig();
bool needRestart = false;
// Iterate on the setting groups
foreach(const QString& rootKey, postedObject.keys()) {
QJsonValue rootValue = postedObject[rootKey];
@ -521,6 +741,9 @@ void DomainServerSettingsManager::recurseJSONObjectAndOverwriteSettings(const QJ
if (!matchingDescriptionObject.isEmpty()) {
updateSetting(rootKey, rootValue, *thisMap, matchingDescriptionObject);
if (rootKey != "security") {
needRestart = true;
}
} else {
qDebug() << "Setting for root key" << rootKey << "does not exist - cannot update setting.";
}
@ -534,6 +757,9 @@ void DomainServerSettingsManager::recurseJSONObjectAndOverwriteSettings(const QJ
if (!matchingDescriptionObject.isEmpty()) {
QJsonValue settingValue = rootValue.toObject()[settingKey];
updateSetting(settingKey, settingValue, *thisMap, matchingDescriptionObject);
if (rootKey != "security") {
needRestart = true;
}
} else {
qDebug() << "Could not find description for setting" << settingKey << "in group" << rootKey <<
"- cannot update setting.";
@ -549,9 +775,42 @@ void DomainServerSettingsManager::recurseJSONObjectAndOverwriteSettings(const QJ
// re-merge the user and master configs after a settings change
_configMap.mergeMasterAndUserConfigs();
return needRestart;
}
// Compare two members of a permissions list
bool permissionVariantLessThan(const QVariant &v1, const QVariant &v2) {
if (!v1.canConvert(QMetaType::QVariantMap) ||
!v2.canConvert(QMetaType::QVariantMap)) {
return v1.toString() < v2.toString();
}
QVariantMap m1 = v1.toMap();
QVariantMap m2 = v2.toMap();
if (!m1.contains("permissions_id") ||
!m2.contains("permissions_id")) {
return v1.toString() < v2.toString();
}
return m1["permissions_id"].toString() < m2["permissions_id"].toString();
}
void DomainServerSettingsManager::sortPermissions() {
// sort the permission-names
QVariant* standardPermissions = valueForKeyPath(_configMap.getUserConfig(), AGENT_STANDARD_PERMISSIONS_KEYPATH);
if (standardPermissions && standardPermissions->canConvert(QMetaType::QVariantList)) {
QList<QVariant>* standardPermissionsList = reinterpret_cast<QVariantList*>(standardPermissions);
std::sort((*standardPermissionsList).begin(), (*standardPermissionsList).end(), permissionVariantLessThan);
}
QVariant* permissions = valueForKeyPath(_configMap.getUserConfig(), AGENT_PERMISSIONS_KEYPATH);
if (permissions && permissions->canConvert(QMetaType::QVariantList)) {
QList<QVariant>* permissionsList = reinterpret_cast<QVariantList*>(permissions);
std::sort((*permissionsList).begin(), (*permissionsList).end(), permissionVariantLessThan);
}
}
void DomainServerSettingsManager::persistToFile() {
sortPermissions();
// make sure we have the dir the settings file is supposed to live in
QFileInfo settingsFileInfo(_configMap.getUserConfigFilename());

View file

@ -19,14 +19,14 @@
#include <HTTPManager.h>
#include <ReceivedMessage.h>
#include "NodePermissions.h"
const QString SETTINGS_PATHS_KEY = "paths";
const QString SETTINGS_PATH = "/settings";
const QString SETTINGS_PATH_JSON = SETTINGS_PATH + ".json";
const QString ALLOWED_USERS_SETTINGS_KEYPATH = "security.allowed_users";
const QString RESTRICTED_ACCESS_SETTINGS_KEYPATH = "security.restricted_access";
const QString AGENT_STANDARD_PERMISSIONS_KEYPATH = "security.standard_permissions";
const QString AGENT_PERMISSIONS_KEYPATH = "security.permissions";
class DomainServerSettingsManager : public QObject {
Q_OBJECT
@ -41,16 +41,29 @@ public:
QVariantMap& getUserSettingsMap() { return _configMap.getUserConfig(); }
QVariantMap& getSettingsMap() { return _configMap.getMergedConfig(); }
bool haveStandardPermissionsForName(const QString& name) const { return _standardAgentPermissions.contains(name); }
bool havePermissionsForName(const QString& name) const { return _agentPermissions.contains(name); }
NodePermissions getStandardPermissionsForName(const QString& name) const;
NodePermissions getPermissionsForName(const QString& name) const;
QStringList getAllNames() { return _agentPermissions.keys(); }
signals:
void updateNodePermissions();
private slots:
void processSettingsRequestPacket(QSharedPointer<ReceivedMessage> message);
private:
QStringList _argumentList;
QJsonObject responseObjectForType(const QString& typeValue, bool isAuthenticated = false);
void recurseJSONObjectAndOverwriteSettings(const QJsonObject& postedObject);
bool recurseJSONObjectAndOverwriteSettings(const QJsonObject& postedObject);
void updateSetting(const QString& key, const QJsonValue& newValue, QVariantMap& settingMap,
const QJsonObject& settingDescription);
QJsonObject settingDescriptionFromGroup(const QJsonObject& groupObject, const QString& settingName);
void sortPermissions();
void persistToFile();
double _descriptionVersion;
@ -58,6 +71,12 @@ private:
HifiConfigVariantMap _configMap;
friend class DomainServer;
void packPermissionsForMap(QString mapName, QHash<QString, NodePermissionsPointer> agentPermissions, QString keyPath);
void packPermissions();
void unpackPermissions();
QHash<QString, NodePermissionsPointer> _standardAgentPermissions; // anonymous, logged-in, localhost
QHash<QString, NodePermissionsPointer> _agentPermissions; // specific account-names
};
#endif // hifi_DomainServerSettingsManager_h

View file

@ -4292,7 +4292,7 @@ void Application::nodeActivated(SharedNodePointer node) {
if (assetDialog) {
auto nodeList = DependencyManager::get<NodeList>();
if (nodeList->getThisNodeCanRez()) {
if (nodeList->getThisNodeCanWriteAssets()) {
// call reload on the shown asset browser dialog to get the mappings (if permissions allow)
QMetaObject::invokeMethod(assetDialog, "reload");
} else {
@ -4788,7 +4788,7 @@ void Application::toggleRunningScriptsWidget() const {
}
void Application::toggleAssetServerWidget(QString filePath) {
if (!DependencyManager::get<NodeList>()->getThisNodeCanRez()) {
if (!DependencyManager::get<NodeList>()->getThisNodeCanWriteAssets()) {
return;
}

View file

@ -136,8 +136,8 @@ Menu::Menu() {
Qt::CTRL | Qt::SHIFT | Qt::Key_A,
qApp, SLOT(toggleAssetServerWidget()));
auto nodeList = DependencyManager::get<NodeList>();
QObject::connect(nodeList.data(), &NodeList::canRezChanged, assetServerAction, &QAction::setEnabled);
assetServerAction->setEnabled(nodeList->getThisNodeCanRez());
QObject::connect(nodeList.data(), &NodeList::canWriteAssetsChanged, assetServerAction, &QAction::setEnabled);
assetServerAction->setEnabled(nodeList->getThisNodeCanWriteAssets());
// Edit > Package Model... [advanced]
addActionToQMenuAndActionHash(editMenu, MenuOption::PackageModel, 0,

View file

@ -32,6 +32,7 @@ EntityScriptingInterface::EntityScriptingInterface(bool bidOnSimulationOwnership
auto nodeList = DependencyManager::get<NodeList>();
connect(nodeList.data(), &NodeList::isAllowedEditorChanged, this, &EntityScriptingInterface::canAdjustLocksChanged);
connect(nodeList.data(), &NodeList::canRezChanged, this, &EntityScriptingInterface::canRezChanged);
connect(nodeList.data(), &NodeList::canRezTmpChanged, this, &EntityScriptingInterface::canRezTmpChanged);
}
void EntityScriptingInterface::queueEntityMessage(PacketType packetType,
@ -49,6 +50,11 @@ bool EntityScriptingInterface::canRez() {
return nodeList->getThisNodeCanRez();
}
bool EntityScriptingInterface::canRezTmp() {
auto nodeList = DependencyManager::get<NodeList>();
return nodeList->getThisNodeCanRezTmp();
}
void EntityScriptingInterface::setEntityTree(EntityTreePointer elementTree) {
if (_entityTree) {
disconnect(_entityTree.get(), &EntityTree::addingEntity, this, &EntityScriptingInterface::addingEntity);

View file

@ -80,6 +80,7 @@ public slots:
// returns true if the DomainServer will allow this Node/Avatar to rez new entities
Q_INVOKABLE bool canRez();
Q_INVOKABLE bool canRezTmp();
/// adds a model with the specific properties
Q_INVOKABLE QUuid addEntity(const EntityItemProperties& properties, bool clientOnly = false);
@ -179,6 +180,7 @@ signals:
void canAdjustLocksChanged(bool canAdjustLocks);
void canRezChanged(bool canRez);
void canRezTmpChanged(bool canRez);
void mousePressOnEntity(const EntityItemID& entityItemID, const MouseEvent& event);
void mouseMoveOnEntity(const EntityItemID& entityItemID, const MouseEvent& event);

View file

@ -26,6 +26,8 @@
#include "LogHandler.h"
static const quint64 DELETED_ENTITIES_EXTRA_USECS_TO_CONSIDER = USECS_PER_MSEC * 50;
const float EntityTree::DEFAULT_MAX_TMP_ENTITY_LIFETIME = 60 * 60; // 1 hour
EntityTree::EntityTree(bool shouldReaverage) :
Octree(shouldReaverage),
@ -128,13 +130,16 @@ bool EntityTree::updateEntityWithElement(EntityItemPointer entity, const EntityI
EntityItemProperties properties = origProperties;
bool allowLockChange;
bool canRezPermanentEntities;
QUuid senderID;
if (senderNode.isNull()) {
auto nodeList = DependencyManager::get<NodeList>();
allowLockChange = nodeList->isAllowedEditor();
canRezPermanentEntities = nodeList->getThisNodeCanRez();
senderID = nodeList->getSessionUUID();
} else {
allowLockChange = senderNode->isAllowedEditor();
canRezPermanentEntities = senderNode->getCanRez();
senderID = senderNode->getUUID();
}
@ -143,6 +148,12 @@ bool EntityTree::updateEntityWithElement(EntityItemPointer entity, const EntityI
return false;
}
if (!canRezPermanentEntities && (entity->getLifetime() != properties.getLifetime())) {
// we don't allow a Node that can't create permanent entities to adjust lifetimes on existing ones
qCDebug(entities) << "Refusing disallowed entity lifetime adjustment.";
return false;
}
// enforce support for locked entities. If an entity is currently locked, then the only
// property we allow you to change is the locked property.
if (entity->getLocked()) {
@ -308,17 +319,39 @@ bool EntityTree::updateEntityWithElement(EntityItemPointer entity, const EntityI
return true;
}
bool EntityTree::permissionsAllowRez(const EntityItemProperties& properties, bool canRez, bool canRezTmp) {
float lifeTime = properties.getLifetime();
if (lifeTime == 0.0f || lifeTime > _maxTmpEntityLifetime) {
// this is an attempt to rez a permanent entity.
if (!canRez) {
return false;
}
} else {
// this is an attempt to rez a temporary entity.
if (!canRezTmp) {
return false;
}
}
return true;
}
EntityItemPointer EntityTree::addEntity(const EntityItemID& entityID, const EntityItemProperties& properties) {
EntityItemPointer result = NULL;
auto nodeList = DependencyManager::get<NodeList>();
if (!nodeList) {
qDebug() << "EntityTree::addEntity -- can't get NodeList";
return nullptr;
}
bool clientOnly = properties.getClientOnly();
if (!clientOnly && getIsClient()) {
if (!clientOnly && getIsClient() &&
!permissionsAllowRez(properties, nodeList->getThisNodeCanRez(), nodeList->getThisNodeCanRezTmp())) {
// if our Node isn't allowed to create entities in this domain, don't try.
auto nodeList = DependencyManager::get<NodeList>();
if (nodeList && !nodeList->getThisNodeCanRez()) {
return NULL;
}
return nullptr;
}
bool recordCreationTime = false;
@ -920,7 +953,7 @@ int EntityTree::processEditPacketData(ReceivedMessage& message, const unsigned c
endUpdate = usecTimestampNow();
_totalUpdates++;
} else if (message.getType() == PacketType::EntityAdd) {
if (senderNode->getCanRez()) {
if (permissionsAllowRez(properties, senderNode->getCanRez(), senderNode->getCanRezTmp())) {
// this is a new entity... assign a new entityID
properties.setCreated(properties.getLastEdited());
startCreate = usecTimestampNow();

View file

@ -62,6 +62,10 @@ public:
void createRootElement();
void setEntityMaxTmpLifetime(float maxTmpEntityLifetime) { _maxTmpEntityLifetime = maxTmpEntityLifetime; }
bool permissionsAllowRez(const EntityItemProperties& properties, bool canRez, bool canRezTmp);
/// Implements our type specific root element factory
virtual OctreeElementPointer createNewElement(unsigned char* octalCode = NULL) override;
@ -252,6 +256,8 @@ public:
void notifyNewCollisionSoundURL(const QString& newCollisionSoundURL, const EntityItemID& entityID);
static const float DEFAULT_MAX_TMP_ENTITY_LIFETIME;
public slots:
void callLoader(EntityItemID entityID);
@ -331,6 +337,8 @@ protected:
// we maintain a list of avatarIDs to notice when an entity is a child of one.
QSet<QUuid> _avatarIDs; // IDs of avatars connected to entity server
QHash<QUuid, QSet<EntityItemID>> _childrenOfAvatars; // which entities are children of which avatars
float _maxTmpEntityLifetime { DEFAULT_MAX_TMP_ENTITY_LIFETIME };
};
#endif // hifi_EntityTree_h

View file

@ -52,7 +52,7 @@ LimitedNodeList::LimitedNodeList(unsigned short socketListenPort, unsigned short
_numCollectedPackets(0),
_numCollectedBytes(0),
_packetStatTimer(),
_thisNodeCanRez(true)
_permissions(NodePermissions())
{
static bool firstCall = true;
if (firstCall) {
@ -130,17 +130,22 @@ void LimitedNodeList::setSessionUUID(const QUuid& sessionUUID) {
}
}
void LimitedNodeList::setIsAllowedEditor(bool isAllowedEditor) {
if (_isAllowedEditor != isAllowedEditor) {
_isAllowedEditor = isAllowedEditor;
emit isAllowedEditorChanged(isAllowedEditor);
}
}
void LimitedNodeList::setPermissions(const NodePermissions& newPermissions) {
NodePermissions originalPermissions = _permissions;
void LimitedNodeList::setThisNodeCanRez(bool canRez) {
if (_thisNodeCanRez != canRez) {
_thisNodeCanRez = canRez;
emit canRezChanged(canRez);
_permissions = newPermissions;
if (originalPermissions.canAdjustLocks != newPermissions.canAdjustLocks) {
emit isAllowedEditorChanged(_permissions.canAdjustLocks);
}
if (originalPermissions.canRezPermanentEntities != newPermissions.canRezPermanentEntities) {
emit canRezChanged(_permissions.canRezPermanentEntities);
}
if (originalPermissions.canRezTemporaryEntities != newPermissions.canRezTemporaryEntities) {
emit canRezTmpChanged(_permissions.canRezTemporaryEntities);
}
if (originalPermissions.canWriteToAssetServer != newPermissions.canWriteToAssetServer) {
emit canWriteAssetsChanged(_permissions.canWriteToAssetServer);
}
}
@ -515,7 +520,7 @@ void LimitedNodeList::handleNodeKill(const SharedNodePointer& node) {
SharedNodePointer LimitedNodeList::addOrUpdateNode(const QUuid& uuid, NodeType_t nodeType,
const HifiSockAddr& publicSocket, const HifiSockAddr& localSocket,
bool isAllowedEditor, bool canRez,
const NodePermissions& permissions,
const QUuid& connectionSecret) {
NodeHash::const_iterator it = _nodeHash.find(uuid);
@ -524,14 +529,13 @@ SharedNodePointer LimitedNodeList::addOrUpdateNode(const QUuid& uuid, NodeType_t
matchingNode->setPublicSocket(publicSocket);
matchingNode->setLocalSocket(localSocket);
matchingNode->setIsAllowedEditor(isAllowedEditor);
matchingNode->setCanRez(canRez);
matchingNode->setPermissions(permissions);
matchingNode->setConnectionSecret(connectionSecret);
return matchingNode;
} else {
// we didn't have this node, so add them
Node* newNode = new Node(uuid, nodeType, publicSocket, localSocket, isAllowedEditor, canRez, connectionSecret, this);
Node* newNode = new Node(uuid, nodeType, publicSocket, localSocket, permissions, connectionSecret, this);
if (nodeType == NodeType::AudioMixer) {
LimitedNodeList::flagTimeForConnectionStep(LimitedNodeList::AddedAudioMixer);

View file

@ -104,12 +104,12 @@ public:
const QUuid& getSessionUUID() const { return _sessionUUID; }
void setSessionUUID(const QUuid& sessionUUID);
bool isAllowedEditor() const { return _isAllowedEditor; }
void setIsAllowedEditor(bool isAllowedEditor);
void setPermissions(const NodePermissions& newPermissions);
bool isAllowedEditor() const { return _permissions.canAdjustLocks; }
bool getThisNodeCanRez() const { return _permissions.canRezPermanentEntities; }
bool getThisNodeCanRezTmp() const { return _permissions.canRezTemporaryEntities; }
bool getThisNodeCanWriteAssets() const { return _permissions.canWriteToAssetServer; }
bool getThisNodeCanRez() const { return _thisNodeCanRez; }
void setThisNodeCanRez(bool canRez);
quint16 getSocketLocalPort() const { return _nodeSocket.localPort(); }
QUdpSocket& getDTLSSocket();
@ -137,7 +137,7 @@ public:
SharedNodePointer addOrUpdateNode(const QUuid& uuid, NodeType_t nodeType,
const HifiSockAddr& publicSocket, const HifiSockAddr& localSocket,
bool isAllowedEditor = false, bool canRez = false,
const NodePermissions& permissions = DEFAULT_AGENT_PERMISSIONS,
const QUuid& connectionSecret = QUuid());
bool hasCompletedInitialSTUN() const { return _hasCompletedInitialSTUN; }
@ -254,6 +254,8 @@ signals:
void isAllowedEditorChanged(bool isAllowedEditor);
void canRezChanged(bool canRez);
void canRezTmpChanged(bool canRezTmp);
void canWriteAssetsChanged(bool canWriteAssets);
protected slots:
void connectedForLocalSocketTest();
@ -300,8 +302,7 @@ protected:
int _numCollectedBytes;
QElapsedTimer _packetStatTimer;
bool _isAllowedEditor { false };
bool _thisNodeCanRez;
NodePermissions _permissions;
QPointer<QTimer> _initialSTUNTimer;

View file

@ -16,6 +16,7 @@
#include "Node.h"
#include "SharedUtil.h"
#include "NodePermissions.h"
#include <QtCore/QDataStream>
#include <QtCore/QDebug>
@ -47,7 +48,7 @@ const QString& NodeType::getNodeTypeName(NodeType_t nodeType) {
}
Node::Node(const QUuid& uuid, NodeType_t type, const HifiSockAddr& publicSocket,
const HifiSockAddr& localSocket, bool isAllowedEditor, bool canRez, const QUuid& connectionSecret,
const HifiSockAddr& localSocket, const NodePermissions& permissions, const QUuid& connectionSecret,
QObject* parent) :
NetworkPeer(uuid, publicSocket, localSocket, parent),
_type(type),
@ -57,8 +58,7 @@ Node::Node(const QUuid& uuid, NodeType_t type, const HifiSockAddr& publicSocket,
_clockSkewUsec(0),
_mutex(),
_clockSkewMovingPercentile(30, 0.8f), // moving 80th percentile of 30 samples
_isAllowedEditor(isAllowedEditor),
_canRez(canRez)
_permissions(permissions)
{
// Update socket's object name
setType(_type);
@ -78,15 +78,12 @@ void Node::updateClockSkewUsec(qint64 clockSkewSample) {
_clockSkewUsec = (quint64)_clockSkewMovingPercentile.getValueAtPercentile();
}
QDataStream& operator<<(QDataStream& out, const Node& node) {
out << node._type;
out << node._uuid;
out << node._publicSocket;
out << node._localSocket;
out << node._isAllowedEditor;
out << node._canRez;
out << node._permissions;
return out;
}
@ -95,9 +92,7 @@ QDataStream& operator>>(QDataStream& in, Node& node) {
in >> node._uuid;
in >> node._publicSocket;
in >> node._localSocket;
in >> node._isAllowedEditor;
in >> node._canRez;
in >> node._permissions;
return in;
}

View file

@ -27,13 +27,14 @@
#include "NodeType.h"
#include "SimpleMovingAverage.h"
#include "MovingPercentile.h"
#include "NodePermissions.h"
class Node : public NetworkPeer {
Q_OBJECT
public:
Node(const QUuid& uuid, NodeType_t type,
const HifiSockAddr& publicSocket, const HifiSockAddr& localSocket,
bool isAllowedEditor, bool canRez, const QUuid& connectionSecret = QUuid(),
const NodePermissions& permissions, const QUuid& connectionSecret = QUuid(),
QObject* parent = 0);
bool operator==(const Node& otherNode) const { return _uuid == otherNode._uuid; }
@ -58,11 +59,12 @@ public:
void updateClockSkewUsec(qint64 clockSkewSample);
QMutex& getMutex() { return _mutex; }
void setIsAllowedEditor(bool isAllowedEditor) { _isAllowedEditor = isAllowedEditor; }
bool isAllowedEditor() { return _isAllowedEditor; }
void setCanRez(bool canRez) { _canRez = canRez; }
bool getCanRez() { return _canRez; }
void setPermissions(const NodePermissions& newPermissions) { _permissions = newPermissions; }
NodePermissions getPermissions() const { return _permissions; }
bool isAllowedEditor() const { return _permissions.canAdjustLocks; }
bool getCanRez() const { return _permissions.canRezPermanentEntities; }
bool getCanRezTmp() const { return _permissions.canRezTemporaryEntities; }
bool getCanWriteToAssetServer() const { return _permissions.canWriteToAssetServer; }
friend QDataStream& operator<<(QDataStream& out, const Node& node);
friend QDataStream& operator>>(QDataStream& in, Node& node);
@ -81,8 +83,7 @@ private:
qint64 _clockSkewUsec;
QMutex _mutex;
MovingPercentile _clockSkewMovingPercentile;
bool _isAllowedEditor;
bool _canRez;
NodePermissions _permissions;
};
Q_DECLARE_METATYPE(Node*)

View file

@ -526,7 +526,7 @@ void NodeList::processDomainServerList(QSharedPointer<ReceivedMessage> message)
DependencyManager::get<NodeList>()->flagTimeForConnectionStep(LimitedNodeList::ConnectionStep::ReceiveDSList);
QDataStream packetStream(message->getMessage());
// grab the domain's ID from the beginning of the packet
QUuid domainUUID;
packetStream >> domainUUID;
@ -542,14 +542,9 @@ void NodeList::processDomainServerList(QSharedPointer<ReceivedMessage> message)
packetStream >> newUUID;
setSessionUUID(newUUID);
quint8 isAllowedEditor;
packetStream >> isAllowedEditor;
setIsAllowedEditor((bool) isAllowedEditor);
// pull the permissions/right/privileges for this node out of the stream
packetStream >> _permissions;
quint8 thisNodeCanRez;
packetStream >> thisNodeCanRez;
setThisNodeCanRez((bool) thisNodeCanRez);
// pull each node in the packet
while (packetStream.device()->pos() < message->getSize()) {
parseNodeFromPacketStream(packetStream);
@ -576,10 +571,9 @@ void NodeList::parseNodeFromPacketStream(QDataStream& packetStream) {
qint8 nodeType;
QUuid nodeUUID, connectionUUID;
HifiSockAddr nodePublicSocket, nodeLocalSocket;
bool isAllowedEditor;
bool canRez;
NodePermissions permissions;
packetStream >> nodeType >> nodeUUID >> nodePublicSocket >> nodeLocalSocket >> isAllowedEditor >> canRez;
packetStream >> nodeType >> nodeUUID >> nodePublicSocket >> nodeLocalSocket >> permissions;
// if the public socket address is 0 then it's reachable at the same IP
// as the domain server
@ -590,8 +584,7 @@ void NodeList::parseNodeFromPacketStream(QDataStream& packetStream) {
packetStream >> connectionUUID;
SharedNodePointer node = addOrUpdateNode(nodeUUID, nodeType, nodePublicSocket,
nodeLocalSocket, isAllowedEditor, canRez,
connectionUUID);
nodeLocalSocket, permissions, connectionUUID);
}
void NodeList::sendAssignment(Assignment& assignment) {

View file

@ -0,0 +1,97 @@
//
// NodePermissions.cpp
// libraries/networking/src/
//
// Created by Seth Alves on 2016-6-1.
// Copyright 2016 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#include <QDataStream>
#include <QtCore/QDebug>
#include "NodePermissions.h"
QString NodePermissions::standardNameLocalhost = QString("localhost");
QString NodePermissions::standardNameLoggedIn = QString("logged-in");
QString NodePermissions::standardNameAnonymous = QString("anonymous");
QStringList NodePermissions::standardNames = QList<QString>()
<< NodePermissions::standardNameLocalhost
<< NodePermissions::standardNameLoggedIn
<< NodePermissions::standardNameAnonymous;
NodePermissions& NodePermissions::operator|=(const NodePermissions& rhs) {
this->canConnectToDomain |= rhs.canConnectToDomain;
this->canAdjustLocks |= rhs.canAdjustLocks;
this->canRezPermanentEntities |= rhs.canRezPermanentEntities;
this->canRezTemporaryEntities |= rhs.canRezTemporaryEntities;
this->canWriteToAssetServer |= rhs.canWriteToAssetServer;
this->canConnectPastMaxCapacity |= rhs.canConnectPastMaxCapacity;
return *this;
}
NodePermissions& NodePermissions::operator|=(const NodePermissionsPointer& rhs) {
if (rhs) {
*this |= *rhs.get();
}
return *this;
}
NodePermissionsPointer& operator|=(NodePermissionsPointer& lhs, const NodePermissionsPointer& rhs) {
if (lhs && rhs) {
*lhs.get() |= rhs;
}
return lhs;
}
QDataStream& operator<<(QDataStream& out, const NodePermissions& perms) {
out << perms.canConnectToDomain;
out << perms.canAdjustLocks;
out << perms.canRezPermanentEntities;
out << perms.canRezTemporaryEntities;
out << perms.canWriteToAssetServer;
out << perms.canConnectPastMaxCapacity;
return out;
}
QDataStream& operator>>(QDataStream& in, NodePermissions& perms) {
in >> perms.canConnectToDomain;
in >> perms.canAdjustLocks;
in >> perms.canRezPermanentEntities;
in >> perms.canRezTemporaryEntities;
in >> perms.canWriteToAssetServer;
in >> perms.canConnectPastMaxCapacity;
return in;
}
QDebug operator<<(QDebug debug, const NodePermissions& perms) {
debug.nospace() << "[permissions: " << perms.getID() << " --";
if (perms.canConnectToDomain) {
debug << " connect";
}
if (perms.canAdjustLocks) {
debug << " locks";
}
if (perms.canRezPermanentEntities) {
debug << " rez";
}
if (perms.canRezTemporaryEntities) {
debug << " rez-tmp";
}
if (perms.canWriteToAssetServer) {
debug << " asset-server";
}
if (perms.canConnectPastMaxCapacity) {
debug << " ignore-max-cap";
}
debug.nospace() << "]";
return debug.nospace();
}
QDebug operator<<(QDebug debug, const NodePermissionsPointer& perms) {
if (perms) {
return operator<<(debug, *perms.get());
}
debug.nospace() << "[permissions: null]";
return debug.nospace();
}

View file

@ -0,0 +1,97 @@
//
// NodePermissions.h
// libraries/networking/src/
//
// Created by Seth Alves on 2016-6-1.
// Copyright 2016 High Fidelity, Inc.
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
#ifndef hifi_NodePermissions_h
#define hifi_NodePermissions_h
#include <memory>
#include <QString>
#include <QMap>
#include <QVariant>
#include <QUuid>
class NodePermissions;
using NodePermissionsPointer = std::shared_ptr<NodePermissions>;
class NodePermissions {
public:
NodePermissions() { _id = QUuid::createUuid().toString(); }
NodePermissions(const QString& name) { _id = name; }
NodePermissions(QMap<QString, QVariant> perms) {
_id = perms["permissions_id"].toString();
canConnectToDomain = perms["id_can_connect"].toBool();
canAdjustLocks = perms["id_can_adjust_locks"].toBool();
canRezPermanentEntities = perms["id_can_rez"].toBool();
canRezTemporaryEntities = perms["id_can_rez_tmp"].toBool();
canWriteToAssetServer = perms["id_can_write_to_asset_server"].toBool();
canConnectPastMaxCapacity = perms["id_can_connect_past_max_capacity"].toBool();
}
QString getID() const { return _id; }
// the _id member isn't authenticated and _username is.
void setUserName(QString userName) { _userName = userName; }
QString getUserName() { return _userName; }
bool isAssignment { false };
// these 3 names have special meaning.
static QString standardNameLocalhost;
static QString standardNameLoggedIn;
static QString standardNameAnonymous;
static QStringList standardNames;
// the initializations here should match the defaults in describe-settings.json
bool canConnectToDomain { true };
bool canAdjustLocks { false };
bool canRezPermanentEntities { false };
bool canRezTemporaryEntities { false };
bool canWriteToAssetServer { false };
bool canConnectPastMaxCapacity { false };
void setAll(bool value) {
canConnectToDomain = value;
canAdjustLocks = value;
canRezPermanentEntities = value;
canRezTemporaryEntities = value;
canWriteToAssetServer = value;
canConnectPastMaxCapacity = value;
}
QVariant toVariant() {
QMap<QString, QVariant> values;
values["permissions_id"] = _id;
values["id_can_connect"] = canConnectToDomain;
values["id_can_adjust_locks"] = canAdjustLocks;
values["id_can_rez"] = canRezPermanentEntities;
values["id_can_rez_tmp"] = canRezTemporaryEntities;
values["id_can_write_to_asset_server"] = canWriteToAssetServer;
values["id_can_connect_past_max_capacity"] = canConnectPastMaxCapacity;
return QVariant(values);
}
NodePermissions& operator|=(const NodePermissions& rhs);
NodePermissions& operator|=(const NodePermissionsPointer& rhs);
friend QDataStream& operator<<(QDataStream& out, const NodePermissions& perms);
friend QDataStream& operator>>(QDataStream& in, NodePermissions& perms);
protected:
QString _id;
QString _userName;
};
const NodePermissions DEFAULT_AGENT_PERMISSIONS;
QDebug operator<<(QDebug debug, const NodePermissions& perms);
QDebug operator<<(QDebug debug, const NodePermissionsPointer& perms);
NodePermissionsPointer& operator|=(NodePermissionsPointer& lhs, const NodePermissionsPointer& rhs);
#endif // hifi_NodePermissions_h

View file

@ -45,7 +45,7 @@ const QSet<PacketType> RELIABLE_PACKETS = QSet<PacketType>();
PacketVersion versionForPacketType(PacketType packetType) {
switch (packetType) {
case PacketType::DomainList:
return 18;
return static_cast<PacketVersion>(DomainListVersion::PermissionsGrid);
case PacketType::EntityAdd:
case PacketType::EntityEdit:
case PacketType::EntityData:
@ -69,6 +69,9 @@ PacketVersion versionForPacketType(PacketType packetType) {
case PacketType::DomainConnectRequest:
return static_cast<PacketVersion>(DomainConnectRequestVersion::HasProtocolVersions);
case PacketType::DomainServerAddedNode:
return static_cast<PacketVersion>(DomainServerAddedNodeVersion::PermissionsGrid);
default:
return 17;
}

View file

@ -199,4 +199,14 @@ enum class DomainConnectionDeniedVersion : PacketVersion {
IncludesReasonCode
};
enum class DomainServerAddedNodeVersion : PacketVersion {
PrePermissionsGrid = 17,
PermissionsGrid
};
enum class DomainListVersion : PacketVersion {
PrePermissionsGrid = 18,
PermissionsGrid
};
#endif // hifi_PacketHeaders_h

View file

@ -213,10 +213,12 @@ QVariant* valueForKeyPath(QVariantMap& variantMap, const QString& keyPath, bool
if (shouldCreateIfMissing || variantMap.contains(firstKey)) {
if (dotIndex == -1) {
return &variantMap[firstKey];
} else if (variantMap[firstKey].canConvert(QMetaType::QVariantMap)) {
return valueForKeyPath(*static_cast<QVariantMap*>(variantMap[firstKey].data()), keyPath.mid(dotIndex + 1),
shouldCreateIfMissing);
}
if (!variantMap[firstKey].canConvert(QMetaType::QVariantMap)) {
variantMap[firstKey] = QVariantMap();
}
return valueForKeyPath(*static_cast<QVariantMap*>(variantMap[firstKey].data()), keyPath.mid(dotIndex + 1),
shouldCreateIfMissing);
}
return NULL;