diff --git a/android/apps/interface/src/main/java/io/highfidelity/hifiinterface/PermissionChecker.java b/android/apps/interface/src/main/java/io/highfidelity/hifiinterface/PermissionChecker.java index ef9876c71a..0703fabf02 100644 --- a/android/apps/interface/src/main/java/io/highfidelity/hifiinterface/PermissionChecker.java +++ b/android/apps/interface/src/main/java/io/highfidelity/hifiinterface/PermissionChecker.java @@ -109,7 +109,7 @@ public class PermissionChecker extends Activity { JSONObject obj = new JSONObject(); try { obj.put("firstRun",false); - obj.put("Avatar/fullAvatarURL", avatarPaths[which]); + obj.put(SETTINGS_FULL_PRIVATE_GROUP_NAME + "/Avatar/fullAvatarURL", avatarPaths[which]); File directory = new File(pathForJson); if(!directory.exists()) directory.mkdirs(); diff --git a/interface/resources/qml/hifi/dialogs/security/ScriptSecurity.qml b/interface/resources/qml/hifi/dialogs/security/ScriptSecurity.qml new file mode 100644 index 0000000000..de7304b6fb --- /dev/null +++ b/interface/resources/qml/hifi/dialogs/security/ScriptSecurity.qml @@ -0,0 +1,152 @@ +// +// ScriptPermissions.cpp +// libraries/script-engine/src/ScriptPermissions.cpp +// +// Created by dr Karol Suprynowicz on 2024/03/24. +// Copyright 2024 Overte e.V. +// +// Based on EntityScriptQMLWhitelist.qml +// Created by Kalila L. on 2019.12.05 | realities.dev | somnilibertas@gmail.com +// Copyright 2019 Kalila L. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// +// Security settings for the script engines + +import Hifi 1.0 as Hifi +import QtQuick 2.8 +import QtQuick.Controls 2.3 +import QtQuick.Layouts 1.12 +import stylesUit 1.0 as HifiStylesUit +import controlsUit 1.0 as HiFiControls +import PerformanceEnums 1.0 +import "../../../windows" + + +Rectangle { + id: parentBody; + + function getWhitelistAsText() { + var whitelist = Settings.getValue("private/scriptPermissionGetAvatarURLSafeURLs"); + var arrayWhitelist = whitelist.replace(",", "\n"); + return arrayWhitelist; + } + + function setWhitelistAsText(whitelistText) { + Settings.setValue("private/scriptPermissionGetAvatarURLSafeURLs", whitelistText.text); + notificationText.text = "Whitelist saved."; + } + + function setAvatarProtection(enabled) { + Settings.setValue("private/scriptPermissionGetAvatarURLEnable", enabled); + console.info("Setting Protect Avatar URLs to:", enabled); + } + + anchors.fill: parent + width: parent.width; + height: 120; + color: "#80010203"; + + HifiStylesUit.RalewayRegular { + id: titleText; + text: "Protect Avatar URLs" + // Text size + size: 24; + // Style + color: "white"; + elide: Text.ElideRight; + // Anchors + anchors.top: parent.top; + anchors.left: parent.left; + anchors.leftMargin: 20; + anchors.right: parent.right; + anchors.rightMargin: 20; + height: 60; + + CheckBox { + id: whitelistEnabled; + + checked: Settings.getValue("private/scriptPermissionGetAvatarURLEnable", true); + + anchors.right: parent.right; + anchors.top: parent.top; + anchors.topMargin: 10; + onToggled: { + setAvatarProtection(whitelistEnabled.checked) + } + + Label { + text: "Enabled" + color: "white" + font.pixelSize: 18; + anchors.right: parent.left; + anchors.top: parent.top; + anchors.topMargin: 10; + } + } + } + + Rectangle { + id: textAreaRectangle; + color: "black"; + width: parent.width; + height: 250; + anchors.top: titleText.bottom; + + ScrollView { + id: textAreaScrollView + anchors.fill: parent; + width: parent.width + height: parent.height + contentWidth: parent.width + contentHeight: parent.height + clip: false; + + TextArea { + id: whitelistTextArea + text: getWhitelistAsText(); + onTextChanged: notificationText.text = ""; + width: parent.width; + height: parent.height; + font.family: "Ubuntu"; + font.pointSize: 12; + color: "white"; + } + } + + Button { + id: saveChanges + anchors.topMargin: 5; + anchors.leftMargin: 20; + anchors.rightMargin: 20; + x: textAreaRectangle.x + textAreaRectangle.width - width - 15; + y: textAreaRectangle.y + textAreaRectangle.height - height; + contentItem: Text { + text: saveChanges.text + font.family: "Ubuntu"; + font.pointSize: 12; + opacity: enabled ? 1.0 : 0.3 + color: "black" + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + elide: Text.ElideRight + } + text: "Save Changes" + onClicked: setWhitelistAsText(whitelistTextArea) + + HifiStylesUit.RalewayRegular { + id: notificationText; + text: "" + // Text size + size: 16; + // Style + color: "white"; + elide: Text.ElideLeft; + // Anchors + anchors.right: parent.left; + anchors.rightMargin: 10; + } + } + } +} diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 137457c722..431bd2cb96 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -3305,7 +3305,7 @@ void Application::initializeUi() { // END PULL SAFEURLS FROM INTERFACE.JSON Settings - if (AUTHORIZED_EXTERNAL_QML_SOURCE.isParentOf(url)) { + if (QUrl(NetworkingConstants::OVERTE_COMMUNITY_APPLICATIONS).isParentOf(url)) { return true; } else { for (const auto& str : safeURLS) { diff --git a/interface/src/AvatarBookmarks.cpp b/interface/src/AvatarBookmarks.cpp index 256ce2f6fc..461c55e64e 100644 --- a/interface/src/AvatarBookmarks.cpp +++ b/interface/src/AvatarBookmarks.cpp @@ -41,6 +41,15 @@ #include #include #include "WarningsSuppression.h" +#include "ScriptPermissions.h" + +QVariantMap AvatarBookmarks::getBookmarks() { + if (ScriptPermissions::isCurrentScriptAllowed(ScriptPermissions::Permission::SCRIPT_PERMISSION_GET_AVATAR_URL)) { + return _bookmarks; + } else { + return {}; + } +} void addAvatarEntities(const QVariantList& avatarEntities) { auto nodeList = DependencyManager::get(); @@ -123,6 +132,12 @@ AvatarBookmarks::AvatarBookmarks() { } void AvatarBookmarks::addBookmark(const QString& bookmarkName) { + if (ScriptPermissions::isCurrentScriptAllowed(ScriptPermissions::Permission::SCRIPT_PERMISSION_GET_AVATAR_URL)) { + addBookmarkInternal(bookmarkName); + } +} + +void AvatarBookmarks::addBookmarkInternal(const QString& bookmarkName) { if (QThread::currentThread() != thread()) { BLOCKING_INVOKE_METHOD(this, "addBookmark", Q_ARG(QString, bookmarkName)); return; @@ -134,6 +149,12 @@ void AvatarBookmarks::addBookmark(const QString& bookmarkName) { } void AvatarBookmarks::saveBookmark(const QString& bookmarkName) { + if (ScriptPermissions::isCurrentScriptAllowed(ScriptPermissions::Permission::SCRIPT_PERMISSION_GET_AVATAR_URL)) { + saveBookmarkInternal(bookmarkName); + } +} + +void AvatarBookmarks::saveBookmarkInternal(const QString& bookmarkName) { if (QThread::currentThread() != thread()) { BLOCKING_INVOKE_METHOD(this, "saveBookmark", Q_ARG(QString, bookmarkName)); return; @@ -145,6 +166,12 @@ void AvatarBookmarks::saveBookmark(const QString& bookmarkName) { } void AvatarBookmarks::removeBookmark(const QString& bookmarkName) { + if (ScriptPermissions::isCurrentScriptAllowed(ScriptPermissions::Permission::SCRIPT_PERMISSION_GET_AVATAR_URL)) { + removeBookmarkInternal(bookmarkName); + } +} + +void AvatarBookmarks::removeBookmarkInternal(const QString& bookmarkName) { if (QThread::currentThread() != thread()) { BLOCKING_INVOKE_METHOD(this, "removeBookmark", Q_ARG(QString, bookmarkName)); return; @@ -200,6 +227,12 @@ void AvatarBookmarks::updateAvatarEntities(const QVariantList &avatarEntities) { */ void AvatarBookmarks::loadBookmark(const QString& bookmarkName) { + if (ScriptPermissions::isCurrentScriptAllowed(ScriptPermissions::Permission::SCRIPT_PERMISSION_GET_AVATAR_URL)) { + loadBookmarkInternal(bookmarkName); + } +} + +void AvatarBookmarks::loadBookmarkInternal(const QString& bookmarkName) { if (QThread::currentThread() != thread()) { BLOCKING_INVOKE_METHOD(this, "loadBookmark", Q_ARG(QString, bookmarkName)); return; @@ -268,6 +301,15 @@ void AvatarBookmarks::readFromFile() { } QVariantMap AvatarBookmarks::getBookmark(const QString &bookmarkName) +{ + if (ScriptPermissions::isCurrentScriptAllowed(ScriptPermissions::Permission::SCRIPT_PERMISSION_GET_AVATAR_URL)) { + return getBookmarkInternal(bookmarkName); + } else { + return {}; + } +} + +QVariantMap AvatarBookmarks::getBookmarkInternal(const QString &bookmarkName) { if (QThread::currentThread() != thread()) { QVariantMap result; diff --git a/interface/src/AvatarBookmarks.h b/interface/src/AvatarBookmarks.h index c2c7eb5a0a..bf06743b3f 100644 --- a/interface/src/AvatarBookmarks.h +++ b/interface/src/AvatarBookmarks.h @@ -100,7 +100,7 @@ public slots: * print("- " + key + " " + bookmarks[key].avatarUrl); * }; */ - QVariantMap getBookmarks() { return _bookmarks; } + QVariantMap getBookmarks(); signals: /*@jsdoc @@ -147,6 +147,11 @@ protected slots: void deleteBookmark() override; private: + QVariantMap getBookmarkInternal(const QString &bookmarkName); + void addBookmarkInternal(const QString& bookmarkName); + void saveBookmarkInternal(const QString& bookmarkName); + void loadBookmarkInternal(const QString& bookmarkName); + void removeBookmarkInternal(const QString& bookmarkName); const QString AVATARBOOKMARKS_FILENAME = "avatarbookmarks.json"; const QString ENTRY_AVATAR_URL = "avatarUrl"; const QString ENTRY_AVATAR_ICON = "avatarIcon"; diff --git a/interface/src/CrashRecoveryHandler.cpp b/interface/src/CrashRecoveryHandler.cpp index 1f6cbef9ba..c03e8bc70f 100644 --- a/interface/src/CrashRecoveryHandler.cpp +++ b/interface/src/CrashRecoveryHandler.cpp @@ -258,10 +258,11 @@ void CrashRecoveryHandler::handleCrash(CrashRecoveryHandler::Action action) { // Display name and avatar settings.beginGroup(AVATAR_GROUP); displayName = settings.value(DISPLAY_NAME_KEY).toString(); - fullAvatarURL = settings.value(FULL_AVATAR_URL_KEY).toUrl(); fullAvatarModelName = settings.value(FULL_AVATAR_MODEL_NAME_KEY).toString(); settings.endGroup(); + fullAvatarURL = settings.value(SETTINGS_FULL_PRIVATE_GROUP_NAME + "/" + AVATAR_GROUP + "/" + FULL_AVATAR_URL_KEY).toUrl(); + // Tutorial complete tutorialComplete = settings.value(TUTORIAL_COMPLETE_FLAG_KEY).toBool(); } @@ -280,12 +281,12 @@ void CrashRecoveryHandler::handleCrash(CrashRecoveryHandler::Action action) { // Display name and avatar settings.beginGroup(AVATAR_GROUP); settings.setValue(DISPLAY_NAME_KEY, displayName); - settings.setValue(FULL_AVATAR_URL_KEY, fullAvatarURL); settings.setValue(FULL_AVATAR_MODEL_NAME_KEY, fullAvatarModelName); settings.endGroup(); + settings.setValue(SETTINGS_FULL_PRIVATE_GROUP_NAME + "/" + AVATAR_GROUP + "/" + FULL_AVATAR_URL_KEY, fullAvatarURL); + // Tutorial complete settings.setValue(TUTORIAL_COMPLETE_FLAG_KEY, tutorialComplete); } } - diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index aae6839282..7017f2a083 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -323,6 +323,19 @@ Menu::Menu() { } }); + // Settings > Script Security + action = addActionToQMenuAndActionHash(settingsMenu, MenuOption::ScriptSecurity); + connect(action, &QAction::triggered, [] { + auto tablet = DependencyManager::get()->getTablet("com.highfidelity.interface.tablet.system"); + auto hmd = DependencyManager::get(); + + tablet->pushOntoStack("hifi/dialogs/security/ScriptSecurity.qml"); + + if (!hmd->getShouldShowTablet()) { + hmd->toggleShouldShowTablet(); + } + }); + // Settings > Developer Menu addCheckableActionToQMenuAndActionHash(settingsMenu, "Developer Menu", 0, false, this, SLOT(toggleDeveloperMenus())); diff --git a/interface/src/Menu.h b/interface/src/Menu.h index 931c00cfd9..e0cdfdf4fd 100644 --- a/interface/src/Menu.h +++ b/interface/src/Menu.h @@ -190,6 +190,7 @@ namespace MenuOption { const QString RunTimingTests = "Run Timing Tests"; const QString ScriptedMotorControl = "Enable Scripted Motor Control"; const QString EntityScriptQMLWhitelist = "Entity Script / QML Whitelist"; + const QString ScriptSecurity = "Script Security"; const QString ShowTrackedObjects = "Show Tracked Objects"; const QString SelfieCamera = "Selfie"; const QString SendWrongDSConnectVersion = "Send wrong DS connect version"; diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 62100f9cae..21ac211e78 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -73,6 +73,7 @@ #include "MovingEntitiesOperator.h" #include "SceneScriptingInterface.h" #include "WarningsSuppression.h" +#include "ScriptPermissions.h" using namespace std; @@ -226,7 +227,7 @@ MyAvatar::MyAvatar(QThread* thread) : _yawSpeedSetting(QStringList() << AVATAR_SETTINGS_GROUP_NAME << "yawSpeed", _yawSpeed), _hmdYawSpeedSetting(QStringList() << AVATAR_SETTINGS_GROUP_NAME << "hmdYawSpeed", _hmdYawSpeed), _pitchSpeedSetting(QStringList() << AVATAR_SETTINGS_GROUP_NAME << "pitchSpeed", _pitchSpeed), - _fullAvatarURLSetting(QStringList() << AVATAR_SETTINGS_GROUP_NAME << "fullAvatarURL", + _fullAvatarURLSetting(QStringList() << SETTINGS_FULL_PRIVATE_GROUP_NAME << AVATAR_SETTINGS_GROUP_NAME << "fullAvatarURL", AvatarData::defaultFullAvatarModelUrl()), _fullAvatarModelNameSetting(QStringList() << AVATAR_SETTINGS_GROUP_NAME << "fullAvatarModelName", _fullAvatarModelName), _animGraphURLSetting(QStringList() << AVATAR_SETTINGS_GROUP_NAME << "animGraphURL", QUrl("")), @@ -2236,6 +2237,9 @@ AttachmentData MyAvatar::loadAttachmentData(const QUrl& modelURL, const QString& return attachment; } +bool MyAvatar::isMyAvatarURLProtected() const { + return !ScriptPermissions::isCurrentScriptAllowed(ScriptPermissions::Permission::SCRIPT_PERMISSION_GET_AVATAR_URL); +} int MyAvatar::parseDataFromBuffer(const QByteArray& buffer) { qCDebug(interfaceapp) << "Error: ignoring update packet for MyAvatar" diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 5e0627360c..4198deba84 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -2683,6 +2683,7 @@ private: void setEnableDrawAverageFacing(bool drawAverage) { _drawAverageFacingEnabled = drawAverage; } bool getEnableDrawAverageFacing() const { return _drawAverageFacingEnabled; } virtual bool isMyAvatar() const override { return true; } + virtual bool isMyAvatarURLProtected() const override; virtual int parseDataFromBuffer(const QByteArray& buffer) override; virtual glm::vec3 getSkeletonPosition() const override; int _skeletonModelChangeCount { 0 }; diff --git a/interface/src/scripting/SettingsScriptingInterface.cpp b/interface/src/scripting/SettingsScriptingInterface.cpp index b7ef172f19..00cdf009eb 100644 --- a/interface/src/scripting/SettingsScriptingInterface.cpp +++ b/interface/src/scripting/SettingsScriptingInterface.cpp @@ -21,6 +21,9 @@ SettingsScriptingInterface* SettingsScriptingInterface::getInstance() { } QVariant SettingsScriptingInterface::getValue(const QString& setting) { + if (_restrictPrivateValues && setting.startsWith(SETTINGS_FULL_PRIVATE_GROUP_NAME + "/")) { + return {""}; + } QVariant value = Setting::Handle(setting).get(); if (!value.isValid()) { value = ""; @@ -29,6 +32,9 @@ QVariant SettingsScriptingInterface::getValue(const QString& setting) { } QVariant SettingsScriptingInterface::getValue(const QString& setting, const QVariant& defaultValue) { + if (_restrictPrivateValues && setting.startsWith(SETTINGS_FULL_PRIVATE_GROUP_NAME + "/")) { + return {""}; + } QVariant value = Setting::Handle(setting, defaultValue).get(); if (!value.isValid()) { value = ""; @@ -40,7 +46,7 @@ void SettingsScriptingInterface::setValue(const QString& setting, const QVariant if (getValue(setting) == value) { return; } - if (setting.startsWith("private/")) { + if (setting.startsWith("private/") || setting.startsWith(SETTINGS_FULL_PRIVATE_GROUP_NAME + "/")) { if (_restrictPrivateValues) { qWarning() << "SettingsScriptingInterface::setValue -- restricted write: " << setting << value; return; diff --git a/libraries/avatars/src/AvatarData.cpp b/libraries/avatars/src/AvatarData.cpp index c71da50b1a..21cfec65d4 100644 --- a/libraries/avatars/src/AvatarData.cpp +++ b/libraries/avatars/src/AvatarData.cpp @@ -2106,6 +2106,14 @@ const QUrl& AvatarData::getSkeletonModelURL() const { } } +QString AvatarData::getSkeletonModelURLFromScript() const { + if (isMyAvatar() && !isMyAvatarURLProtected()) { + return _skeletonModelURL.toString(); + } + + return QString(); +}; + QByteArray AvatarData::packSkeletonData() const { // Send an avatar trait packet with the skeleton data before the mesh is loaded int avatarDataSize = 0; diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index 0b2a925de0..d3bf8a3282 100644 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -610,6 +610,8 @@ public: AvatarData(); virtual ~AvatarData(); + virtual bool isMyAvatarURLProtected() const { return false; } // This needs to be here because both MyAvatar and AvatarData inherit from MyAvatar + static const QUrl& defaultFullAvatarModelUrl(); const QUuid getSessionUUID() const { return getID(); } @@ -1355,7 +1357,7 @@ public: */ Q_INVOKABLE virtual void detachAll(const QString& modelURL, const QString& jointName = QString()); - QString getSkeletonModelURLFromScript() const { return _skeletonModelURL.toString(); } + QString getSkeletonModelURLFromScript() const; void setSkeletonModelURLFromScript(const QString& skeletonModelString) { setSkeletonModelURL(QUrl(skeletonModelString)); } void setOwningAvatarMixer(const QWeakPointer& owningAvatarMixer) { _owningAvatarMixer = owningAvatarMixer; } diff --git a/libraries/avatars/src/ScriptAvatarData.cpp b/libraries/avatars/src/ScriptAvatarData.cpp index a07c402555..3b5397fd55 100644 --- a/libraries/avatars/src/ScriptAvatarData.cpp +++ b/libraries/avatars/src/ScriptAvatarData.cpp @@ -204,7 +204,11 @@ bool ScriptAvatarData::getLookAtSnappingEnabled() const { // QString ScriptAvatarData::getSkeletonModelURLFromScript() const { if (AvatarSharedPointer sharedAvatarData = _avatarData.lock()) { - return sharedAvatarData->getSkeletonModelURLFromScript(); + if (sharedAvatarData->isMyAvatar() && !sharedAvatarData->isMyAvatarURLProtected()) { + return sharedAvatarData->getSkeletonModelURLFromScript(); + } + + return QString(); } else { return QString(); } diff --git a/libraries/networking/src/NetworkingConstants.h b/libraries/networking/src/NetworkingConstants.h index 4287da92d4..b10e09497a 100644 --- a/libraries/networking/src/NetworkingConstants.h +++ b/libraries/networking/src/NetworkingConstants.h @@ -58,6 +58,8 @@ namespace NetworkingConstants { const QString HF_PUBLIC_CDN_URL = ""; const QString HF_MARKETPLACE_CDN_HOSTNAME = ""; const QString OVERTE_CONTENT_CDN_URL = "https://content.overte.org/"; + const QString OVERTE_COMMUNITY_APPLICATIONS = { "https://more.overte.org/applications" }; + const QString OVERTE_TUTORIAL_SCRIPTS = { "https://more.overte.org/tutorial" }; #if USE_STABLE_GLOBAL_SERVICES const QString ICE_SERVER_DEFAULT_HOSTNAME = "ice.overte.org"; diff --git a/libraries/script-engine/src/ScriptManager.cpp b/libraries/script-engine/src/ScriptManager.cpp index 37ba7d0442..2caab052ec 100644 --- a/libraries/script-engine/src/ScriptManager.cpp +++ b/libraries/script-engine/src/ScriptManager.cpp @@ -542,6 +542,10 @@ QString ScriptManager::getFilename() const { return lastPart; } +QString ScriptManager::getAbsoluteFilename() const { + return _fileNameString; +} + bool ScriptManager::hasValidScriptSuffix(const QString& scriptFileName) { QFileInfo fileInfo(scriptFileName); QString scriptSuffixToLower = fileInfo.completeSuffix().toLower(); diff --git a/libraries/script-engine/src/ScriptManager.h b/libraries/script-engine/src/ScriptManager.h index 623b51a43f..8197f26285 100644 --- a/libraries/script-engine/src/ScriptManager.h +++ b/libraries/script-engine/src/ScriptManager.h @@ -430,6 +430,13 @@ public: */ QString getFilename() const; + /** + * @brief Get the filename of the running script, with absolute path. + * + * @return QString Filename + */ + QString getAbsoluteFilename() const; + /** * @brief Underlying scripting engine * diff --git a/libraries/script-engine/src/ScriptPermissions.cpp b/libraries/script-engine/src/ScriptPermissions.cpp new file mode 100644 index 0000000000..a817f00056 --- /dev/null +++ b/libraries/script-engine/src/ScriptPermissions.cpp @@ -0,0 +1,124 @@ +// +// ScriptPermissions.cpp +// libraries/script-engine/src/ScriptPermissions.cpp +// +// Created by dr Karol Suprynowicz on 2024/03/24. +// Copyright 2024 Overte e.V. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "ScriptPermissions.h" + +#include +#include + +#include "ScriptEngine.h" +#include "ScriptManager.h" +#include "Scriptable.h" + +static const bool PERMISSIONS_DEBUG_ENABLED = false; + +extern const std::array(ScriptPermissions::Permission::SCRIPT_PERMISSIONS_SIZE)> scriptPermissionNames { + "Permission to get user's avatar URL" //SCRIPT_PERMISSION_GET_AVATAR_URL +}; + +extern const std::array(ScriptPermissions::Permission::SCRIPT_PERMISSIONS_SIZE)> scriptPermissionSettingKeyNames { + "private/scriptPermissionGetAvatarURLSafeURLs" //SCRIPT_PERMISSION_GET_AVATAR_URL +}; + +extern const std::array(ScriptPermissions::Permission::SCRIPT_PERMISSIONS_SIZE)> scriptPermissionSettingEnableKeyNames { + "private/scriptPermissionGetAvatarURLEnable" //SCRIPT_PERMISSION_GET_AVATAR_URL +}; + +extern const std::array(ScriptPermissions::Permission::SCRIPT_PERMISSIONS_SIZE)> scriptPermissionSettingEnableDefaultValues { + true //SCRIPT_PERMISSION_GET_AVATAR_URL +}; + +bool ScriptPermissions::isCurrentScriptAllowed(ScriptPermissions::Permission permission) { + if (permission >= ScriptPermissions::Permission::SCRIPT_PERMISSIONS_SIZE) { + return false; + } + int permissionIndex = static_cast(permission); + // Check if the permission checking is active + Setting::Handle isCheckingEnabled(scriptPermissionSettingEnableKeyNames[permissionIndex], scriptPermissionSettingEnableDefaultValues[permissionIndex]); + if (!isCheckingEnabled.get()) { + return true; + } + // Get the script manager: + auto engine = Scriptable::engine(); + if (!engine) { + // When this happens it means that function was called from QML or C++ and should always be allowed + if (PERMISSIONS_DEBUG_ENABLED) { + qDebug() << "ScriptPermissions::isCurrentScriptAllowed called outside script engine for permission: " + << scriptPermissionNames[permissionIndex]; + } + return true; + } + auto manager = engine->manager(); + if (!manager) { + qDebug() << "ScriptPermissions::isCurrentScriptAllowed called from script engine with no script manager for permission: " << scriptPermissionNames[permissionIndex]; + return false; + } + std::vector urlsToCheck; + QString scriptURL = manager->getAbsoluteFilename(); + + // If this is an entity script manager, we need to find the file name of the current script instead + if (!scriptURL.startsWith("about:Entities")) { + urlsToCheck.push_back(scriptURL); + } + + auto currentURL = Scriptable::context()->currentFileName(); + if (!currentURL.isEmpty() && currentURL != scriptURL) { + urlsToCheck.push_back(currentURL); + } + + if (PERMISSIONS_DEBUG_ENABLED) { + qDebug() << "ScriptPermissions::isCurrentScriptAllowed: filename: " << scriptURL; + } + auto parentContext = Scriptable::context()->parentContext(); + while (parentContext) { + QString parentFilename = parentContext->currentFileName(); + if (!parentFilename.isEmpty()) { + urlsToCheck.push_back(parentContext->currentFileName()); + if (PERMISSIONS_DEBUG_ENABLED) { + qDebug() << "ScriptPermissions::isCurrentScriptAllowed: parent filename: " << parentContext->currentFileName(); + } + } + parentContext = parentContext->parentContext(); + } + + // Check if the script is allowed: + QList safeURLPrefixes = { "file:///", "qrc:/", NetworkingConstants::OVERTE_COMMUNITY_APPLICATIONS, + NetworkingConstants::OVERTE_TUTORIAL_SCRIPTS, "about:console"}; + Setting::Handle allowedURLsSetting(scriptPermissionSettingKeyNames[permissionIndex]); + QList allowedURLs = allowedURLsSetting.get().split("\n"); + + for (auto entry : allowedURLs) { + safeURLPrefixes.push_back(entry); + } + + for (auto urlToCheck : urlsToCheck) { + bool urlIsAllowed = false; + for (const auto& str : safeURLPrefixes) { + if (!str.isEmpty() && urlToCheck.startsWith(str)) { + urlIsAllowed = true; + if (PERMISSIONS_DEBUG_ENABLED) { + qDebug() << "ScriptPermissions::isCurrentScriptAllowed: " << scriptPermissionNames[permissionIndex] + << " for script " << urlToCheck << " accepted with rule: " << str; + } + } + } + + if (!urlIsAllowed) { + if (PERMISSIONS_DEBUG_ENABLED) { + qDebug() << "ScriptPermissions::isCurrentScriptAllowed: " << scriptPermissionNames[permissionIndex] + << " for script " << urlToCheck << " rejected."; + } + return false; + } + } + + return true; +} \ No newline at end of file diff --git a/libraries/script-engine/src/ScriptPermissions.h b/libraries/script-engine/src/ScriptPermissions.h new file mode 100644 index 0000000000..f4b06253c5 --- /dev/null +++ b/libraries/script-engine/src/ScriptPermissions.h @@ -0,0 +1,31 @@ +// +// ScriptPermissions.h +// libraries/script-engine/src/ScriptPermissions.h +// +// Created by dr Karol Suprynowicz on 2024/03/24. +// Copyright 2024 Overte e.V. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#pragma once + +#include + +#include "SettingHandle.h" +#include "DependencyManager.h" + +class ScriptPermissions { +public: + enum class Permission { + SCRIPT_PERMISSION_GET_AVATAR_URL, + SCRIPT_PERMISSIONS_SIZE + }; + + static bool isCurrentScriptAllowed(Permission permission); + //TODO: add a function to request permission through a popup +}; + +// TODO: add ScriptPermissionsScriptingInterface, where script can check if they have permissions +// and request permissions through a tablet popup. diff --git a/libraries/shared/src/SettingHandle.cpp b/libraries/shared/src/SettingHandle.cpp index 88785e5700..2353f30933 100644 --- a/libraries/shared/src/SettingHandle.cpp +++ b/libraries/shared/src/SettingHandle.cpp @@ -18,6 +18,8 @@ Q_LOGGING_CATEGORY(settings_handle, "settings.handle") +const QString SETTINGS_FULL_PRIVATE_GROUP_NAME = "fullPrivate"; + const QString Settings::firstRun { "firstRun" }; diff --git a/libraries/shared/src/SettingHandle.h b/libraries/shared/src/SettingHandle.h index 2390063555..f8ba5f66ed 100644 --- a/libraries/shared/src/SettingHandle.h +++ b/libraries/shared/src/SettingHandle.h @@ -31,6 +31,15 @@ Q_DECLARE_LOGGING_CATEGORY(settings_handle) +/** + * @brief Name of the fully private settings group + * + * Settings in this group will be protected from reading and writing from script engines. + * + */ + +extern const QString SETTINGS_FULL_PRIVATE_GROUP_NAME; + /** * @brief QSettings analog * diff --git a/tools/nitpick/AppDataHighFidelity/Interface.json b/tools/nitpick/AppDataHighFidelity/Interface.json index a9d27d8309..bf3519fcb6 100644 --- a/tools/nitpick/AppDataHighFidelity/Interface.json +++ b/tools/nitpick/AppDataHighFidelity/Interface.json @@ -43,7 +43,7 @@ "Avatar/dominantHand": "right", "Avatar/flyingHMD": false, "Avatar/fullAvatarModelName": "Default", - "Avatar/fullAvatarURL": "", + "fullPrivate/Avatar/fullAvatarURL": "", "Avatar/headPitch": 0, "Avatar/pitchSpeed": 75, "Avatar/scale": 1,