From e14137296e18cdf5ad05237429e2b329f3060781 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=BCrz?= Date: Wed, 22 Oct 2025 14:01:14 +0200 Subject: [PATCH] Improve permission handling and add new Thing based permission basics --- libnymea-app/types/userinfo.h | 1 + libnymea-app/usermanager.h | 3 +- nymea-app/ui/system/UsersSettingsPage.qml | 78 ++++++++++++---- nymea-app/ui/utils/NymeaUtils.qml | 109 ++++++++++++++++++++-- 4 files changed, 162 insertions(+), 29 deletions(-) diff --git a/libnymea-app/types/userinfo.h b/libnymea-app/types/userinfo.h index 27b80a11..47fd6ca9 100644 --- a/libnymea-app/types/userinfo.h +++ b/libnymea-app/types/userinfo.h @@ -39,6 +39,7 @@ public: PermissionScopeNone = 0x0000, PermissionScopeControlThings = 0x0001, PermissionScopeConfigureThings = 0x0003, + PermissionScopeAccessAllThings = 0x0004, PermissionScopeExecuteRules = 0x0010, PermissionScopeConfigureRules = 0x0030, PermissionScopeAdmin = 0xFFFF, diff --git a/libnymea-app/usermanager.h b/libnymea-app/usermanager.h index 71a9cbda..f3c07d9e 100644 --- a/libnymea-app/usermanager.h +++ b/libnymea-app/usermanager.h @@ -53,7 +53,8 @@ public: UserErrorDuplicateUserId, UserErrorBadPassword, UserErrorTokenNotFound, - UserErrorPermissionDenied + UserErrorPermissionDenied, + UserErrorInconsistantScopes }; Q_ENUM(UserError) diff --git a/nymea-app/ui/system/UsersSettingsPage.qml b/nymea-app/ui/system/UsersSettingsPage.qml index 75988440..7bf8ecd0 100644 --- a/nymea-app/ui/system/UsersSettingsPage.qml +++ b/nymea-app/ui/system/UsersSettingsPage.qml @@ -323,6 +323,7 @@ SettingsPageBase { title: qsTr("Manage %1").arg(userInfo.username) property UserInfo userInfo: null + property bool restrictedThingAccess: userDetailsPage.userInfo.scopes & UserInfo.PermissionScopeAccessAllThings === 0 Component { id: confirmUserDeletionComponent @@ -382,30 +383,66 @@ SettingsPageBase { Repeater { model: NymeaUtils.scopesModel - delegate: CheckDelegate { - Layout.fillWidth: true - text: model.text - checked: (userDetailsPage.userInfo.scopes & model.scope) === model.scope + delegate: NymeaSwipeDelegate { - enabled: model.scope === UserInfo.PermissionScopeAdmin && userDetailsPage.userInfo.username == userManager.userInfo.username ? - false : model.scope === UserInfo.PermissionScopeAdmin || - ((userDetailsPage.userInfo.scopes & UserInfo.PermissionScopeAdmin) !== UserInfo.PermissionScopeAdmin) - onClicked: { - print("scopes:", userDetailsPage.userInfo.scopes) - var scopes = userDetailsPage.userInfo.scopes - if (checked) { - scopes |= model.scope - } else { - scopes &= ~model.scope - scopes |= model.resetOnUnset + Layout.fillWidth: true + + text: model.text + subText: model.description + progressive: false + + CheckBox { + anchors.right: parent.right + anchors.rightMargin: app.margins + anchors.verticalCenter: parent.verticalCenter + + checked: (userDetailsPage.userInfo.scopes & model.scope) === model.scope + enabled: { + // Prevent an admin to lock himself out as admin + if (model.scope === UserInfo.PermissionScopeAdmin && userDetailsPage.userInfo.username == userManager.userInfo.username) { + return false + } else { + return model.scope === UserInfo.PermissionScopeAdmin || ((userDetailsPage.userInfo.scopes & UserInfo.PermissionScopeAdmin) !== UserInfo.PermissionScopeAdmin) + } + } + + onClicked: { + var scopes = userDetailsPage.userInfo.scopes + if (checked) { + scopes |= model.scope + } else { + scopes &= ~model.scope + } + + // make sure the new permissions are consistant before sending them to the core + scopes = NymeaUtils.getPermissionScopeAdjustments(model.scope, checked, scopes) + userManager.setUserScopes(userDetailsPage.userInfo.username, scopes) } - print("username:", userDetailsPage.userInfo.username) - print("new scopes:", scopes, UserInfo.PermissionScopeAdmin) - userManager.setUserScopes(userDetailsPage.userInfo.username, scopes) } } } + + SettingsPageSectionHeader { + text: qsTr("Acessable things") + visible: userDetailsPage.restrictedThingAccess + Layout.fillWidth: true + } + + + NymeaSwipeDelegate { + id: allowedThingsEntry + Layout.fillWidth: true + text: qsTr("Allowed things for this user") + visible: userDetailsPage.restrictedThingAccess + progressive: true + onClicked: { + + } + + } + + SettingsPageSectionHeader { text: qsTr("Remove") } @@ -496,6 +533,7 @@ SettingsPageBase { text: qsTr("Permissions") } + Repeater { id: scopesRepeater model: NymeaUtils.scopesModel @@ -510,8 +548,10 @@ SettingsPageBase { scopes |= model.scope } else { scopes &= ~model.scope - scopes |= model.resetOnUnset } + + // make sure the new permissions are consistant before sending them to the core + scopes = NymeaUtils.getPermissionScopeAdjustments(model.scope, checked, scopes) createUserPage.permissionScopes = scopes } } diff --git a/nymea-app/ui/utils/NymeaUtils.qml b/nymea-app/ui/utils/NymeaUtils.qml index daec4ae7..b4c56ea1 100644 --- a/nymea-app/ui/utils/NymeaUtils.qml +++ b/nymea-app/ui/utils/NymeaUtils.qml @@ -32,7 +32,7 @@ Item { id: root function pad(num, size, base) { - if (base == undefined) { + if (base === undefined) { base = 10 } @@ -167,11 +167,102 @@ Item { } property ListModel scopesModel: ListModel { - ListElement { text: qsTr("Admin"); scope: UserInfo.PermissionScopeAdmin; resetOnUnset: UserInfo.PermissionScopeNone } - ListElement { text: qsTr("Control things"); scope: UserInfo.PermissionScopeControlThings; resetOnUnset: UserInfo.PermissionScopeNone } - ListElement { text: qsTr("Configure things"); scope: UserInfo.PermissionScopeConfigureThings; resetOnUnset: UserInfo.PermissionScopeControlThings } - ListElement { text: qsTr("Execute magic"); scope: UserInfo.PermissionScopeExecuteRules; resetOnUnset: UserInfo.PermissionScopeNone } - ListElement { text: qsTr("Configure magic"); scope: UserInfo.PermissionScopeConfigureRules; resetOnUnset: UserInfo.PermissionScopeExecuteRules } + ListElement { + text: qsTr("Admin") + description: qsTr("Full access to the system.") + scope: UserInfo.PermissionScopeAdmin + } + ListElement { + text: qsTr("Control things") + description: qsTr("Execute actions and use things and services.") + scope: UserInfo.PermissionScopeControlThings + } + ListElement { + text: qsTr("Configure things") + description: qsTr("Add new things and change settings.") + scope: UserInfo.PermissionScopeConfigureThings + } + ListElement { + text: qsTr("Access all things") + description: qsTr("Allow to see and use all things of the system.") + scope: UserInfo.PermissionScopeAccessAllThings + } + ListElement { + text: qsTr("Execute magic") + description: qsTr("Execute rules, scenes and scripts.") + scope: UserInfo.PermissionScopeExecuteRules + } + ListElement { + text: qsTr("Configure magic") + description: qsTr("Create new rules and scripts in the system.") + scope: UserInfo.PermissionScopeConfigureRules + } + } + + function getPermissionScopeAdjustments(scope, enabled, currentScopes) { + + var adjustedScopes = currentScopes; + + console.warn("Adjust permissions", scope, "->", enabled, currentScopes) + + if (enabled) { + + // Scope has been enabled + switch (scope) { + case UserInfo.PermissionScopeAdmin: + adjustedScopes = UserInfo.PermissionScopeAdmin + break; + case UserInfo.PermissionScopeControlThings: + break; + case UserInfo.PermissionScopeConfigureThings: + adjustedScopes |= UserInfo.PermissionScopeControlThings + adjustedScopes |= UserInfo.PermissionScopeAccessAllThings + break; + case UserInfo.PermissionScopeAccessAllThings: + adjustedScopes |= UserInfo.PermissionScopeControlThings + break; + case UserInfo.PermissionScopeExecuteRules: + adjustedScopes |= UserInfo.PermissionScopeAccessAllThings + break; + case UserInfo.PermissionScopeConfigureRules: + adjustedScopes |= UserInfo.PermissionScopeExecuteRules + adjustedScopes |= UserInfo.PermissionScopeAccessAllThings + break; + } + + } else { + + // Scope has been disabled + switch (scope) { + case UserInfo.PermissionScopeAdmin: + // Set the default permission for non admin + adjustedScopes = UserInfo.PermissionScopeAccessAllThings | UserInfo.PermissionScopeControlThings | UserInfo.PermissionScopeExecuteRules + break; + case UserInfo.PermissionScopeControlThings: + adjustedScopes &= ~UserInfo.PermissionScopeConfigureThings + break; + case UserInfo.PermissionScopeConfigureThings: + // Note: PermissionScopeConfigureThings is 3 and unsets therefore also the abbility to control things. + adjustedScopes |= UserInfo.PermissionScopeControlThings + break; + case UserInfo.PermissionScopeAccessAllThings: + adjustedScopes &= ~UserInfo.PermissionScopeConfigureThings + adjustedScopes &= ~UserInfo.PermissionScopeExecuteRules + adjustedScopes &= ~UserInfo.PermissionScopeConfigureRules + // Make sure we still can controll those things we added + adjustedScopes |= UserInfo.PermissionScopeControlThings + break; + case UserInfo.PermissionScopeExecuteRules: + adjustedScopes &= ~UserInfo.PermissionScopeConfigureRules + break; + case UserInfo.PermissionScopeConfigureRules: + // Note: PermissionScopeConfigureRules constand unsets therefore also the abbility to execute rules (screnes). + adjustedScopes |= UserInfo.PermissionScopeExecuteRules + break; + } + } + + return adjustedScopes } function hasPermissionScope(permissions, requestedScope) { @@ -201,9 +292,9 @@ Item { } function rgb2hsv(r,g,b) { - var v=Math.max(r,g,b), c=v-Math.min(r,g,b); - var h= c && ((v==r) ? (g-b)/c : ((v==g) ? 2+(b-r)/c : 4+(r-g)/c)); - return [60*(h<0?h+6:h), v&&c/v, v]; + var v=Math.max(r,g,b), c=v-Math.min(r,g,b); + var h= c && ((v===r) ? (g-b)/c : ((v===g) ? 2+(b-r)/c : 4+(r-g)/c)); + return [60*(h<0?h+6:h), v&&c/v, v]; } readonly property var sensorInterfaceStateMap: {