Add allowed thing configuration

This commit is contained in:
Simon Stürz 2025-10-27 15:51:02 +01:00
parent e14137296e
commit 0c574dff84
6 changed files with 194 additions and 37 deletions

View File

@ -92,6 +92,39 @@ void UserInfo::setScopes(PermissionScopes scopes)
}
}
QList<QUuid> UserInfo::allowedThingIds() const
{
return m_allowedThingIds;
}
void UserInfo::setAllowedThingIds(const QList<QUuid> &allowedThingIds)
{
if (m_allowedThingIds != allowedThingIds) {
m_allowedThingIds = allowedThingIds;
emit allowedThingIdsChanged();
}
}
bool UserInfo::thingAllowed(const QUuid &thingId) const
{
return m_allowedThingIds.contains(thingId);
}
void UserInfo::allowThingId(const QUuid &thingId, bool allowed)
{
if (allowed) {
if (!m_allowedThingIds.contains(thingId)) {
m_allowedThingIds.append(thingId);
emit allowedThingIdsChanged();
}
} else {
if (m_allowedThingIds.contains(thingId)) {
m_allowedThingIds.removeAll(thingId);
emit allowedThingIdsChanged();
}
}
}
QStringList UserInfo::scopesToList(PermissionScopes scopes)
{
QStringList ret;

View File

@ -25,6 +25,7 @@
#ifndef USERINFO_H
#define USERINFO_H
#include <QUuid>
#include <QObject>
class UserInfo : public QObject
@ -34,6 +35,8 @@ class UserInfo : public QObject
Q_PROPERTY(QString email READ email NOTIFY emailChanged)
Q_PROPERTY(QString displayName READ displayName NOTIFY displayNameChanged)
Q_PROPERTY(PermissionScopes scopes READ scopes NOTIFY scopesChanged)
Q_PROPERTY(QList<QUuid> allowedThingIds READ allowedThingIds NOTIFY allowedThingIdsChanged)
public:
enum PermissionScope {
PermissionScopeNone = 0x0000,
@ -62,6 +65,12 @@ public:
PermissionScopes scopes() const;
void setScopes(PermissionScopes scopes);
QList<QUuid> allowedThingIds() const;
void setAllowedThingIds(const QList<QUuid> &allowedThingIds);
Q_INVOKABLE bool thingAllowed(const QUuid &thingId) const;
Q_INVOKABLE void allowThingId(const QUuid &thingId, bool allowed);
static QStringList scopesToList(PermissionScopes scopes);
static PermissionScopes listToScopes(const QStringList &scopeList);
@ -70,12 +79,14 @@ signals:
void emailChanged();
void displayNameChanged();
void scopesChanged();
void allowedThingIdsChanged();
private:
QString m_username;
QString m_email;
QString m_displayName;
PermissionScopes m_scopes = PermissionScopeNone;
QList<QUuid> m_allowedThingIds;
};

View File

@ -67,6 +67,7 @@ void UserManager::setEngine(Engine *engine)
m_loading = true;
emit loadingChanged();
m_engine->jsonRpcClient()->sendCommand("Users.GetUsers", QVariantMap(), this, "getUsersResponse");
m_engine->jsonRpcClient()->sendCommand("Users.GetUserInfo", QVariantMap(), this, "getUserInfoResponse");
m_engine->jsonRpcClient()->sendCommand("Users.GetTokens", QVariantMap(), this, "getTokensResponse");
@ -94,8 +95,7 @@ Users *UserManager::users() const
return m_users;
}
int UserManager::createUser(const QString &username, const QString &password, const QString &displayName, const QString &email, int permissionScopes)
int UserManager::createUser(const QString &username, const QString &password, const QString &displayName, const QString &email, int permissionScopes, const QList<QUuid> &allowedThingIds)
{
QVariantMap params;
params.insert("username", username);
@ -105,7 +105,15 @@ int UserManager::createUser(const QString &username, const QString &password, co
params.insert("email", email);
params.insert("scopes", UserInfo::scopesToList((UserInfo::PermissionScopes)permissionScopes));
}
qCDebug(dcUserManager()) << "Creating user" << username << permissionScopes;
if (m_engine->jsonRpcClient()->ensureServerVersion("8.4") && !allowedThingIds.isEmpty()) {
QVariantList thingIds;
foreach (const QUuid &thingId, allowedThingIds)
thingIds.append(thingId.toString());
params.insert("allowedThingIds", thingIds);
}
qCDebug(dcUserManager()) << "Creating user" << username << permissionScopes << allowedThingIds;
return m_engine->jsonRpcClient()->sendCommand("Users.CreateUser", params, this, "createUserResponse");
}
@ -133,12 +141,19 @@ int UserManager::removeUser(const QString &username)
return m_engine->jsonRpcClient()->sendCommand("Users.RemoveUser", params, this, "removeUserResponse");
}
int UserManager::setUserScopes(const QString &username, int scopes)
int UserManager::setUserScopes(const QString &username, int scopes, const QList<QUuid> &allowedThingIds)
{
QVariantMap params;
params.insert("username", username);
params.insert("scopes", UserInfo::scopesToList((UserInfo::PermissionScopes)scopes));
qCDebug(dcUserManager()) << "Setting new permission scopes for user" << username << scopes << (int)scopes;
if (m_engine->jsonRpcClient()->ensureServerVersion("8.4") && !allowedThingIds.isEmpty()) {
QVariantList thingIds;
foreach (const QUuid &thingId, allowedThingIds)
thingIds.append(thingId);
params.insert("allowedThingIds", thingIds);
}
qCDebug(dcUserManager()) << "Setting new permission scopes for user" << username << scopes << (int)scopes << allowedThingIds;
return m_engine->jsonRpcClient()->sendCommand("Users.SetUserScopes", params, this, "setUserScopesResponse");
}
@ -162,6 +177,11 @@ void UserManager::notificationReceived(const QVariantMap &data)
info->setDisplayName(userMap.value("displayName").toString());
info->setEmail(userMap.value("email").toString());
info->setScopes(UserInfo::listToScopes(userMap.value("scopes").toStringList()));
QList<QUuid> allowedThingIds;
foreach (const QString &thingIdString, userMap.value("allowedThingIds").toStringList())
allowedThingIds.append(QUuid(thingIdString));
info->setAllowedThingIds(allowedThingIds);
m_users->insertUser(info);
} else if (notification == "Users.UserRemoved") {
m_users->removeUser(data.value("params").toMap().value("username").toString());
@ -171,11 +191,19 @@ void UserManager::notificationReceived(const QVariantMap &data)
QString displayName = userMap.value("displayName").toString();
QString email = userMap.value("email").toString();
UserInfo::PermissionScopes scopes = UserInfo::listToScopes(userMap.value("scopes").toStringList());
QList<QUuid> allowedThingIds;
foreach (const QString &thingIdString, userMap.value("allowedThingIds").toStringList())
allowedThingIds.append(QUuid(thingIdString));
// Update current user info
if (m_userInfo && m_userInfo->username() == username) {
m_userInfo->setDisplayName(displayName);
m_userInfo->setEmail(email);
m_userInfo->setScopes(scopes);
m_userInfo->setAllowedThingIds(allowedThingIds);
}
// Update user info in the list of all users.
UserInfo *info = m_users->getUserInfo(username);
@ -186,6 +214,7 @@ void UserManager::notificationReceived(const QVariantMap &data)
info->setDisplayName(displayName);
info->setEmail(email);
info->setScopes(scopes);
info->setAllowedThingIds(allowedThingIds);
}
}
@ -195,10 +224,16 @@ void UserManager::getUsersResponse(int commandId, const QVariantMap &data)
foreach (const QVariant &userVariant, data.value("users").toList()) {
QVariantMap userMap = userVariant.toMap();
QList<QUuid> allowedThingIds;
foreach (const QString &thingIdString, userMap.value("allowedThingIds").toStringList())
allowedThingIds.append(QUuid(thingIdString));
UserInfo *userInfo = new UserInfo(userMap.value("username").toString());
userInfo->setDisplayName(userMap.value("displayName").toString());
userInfo->setEmail(userMap.value("email").toString());
userInfo->setScopes(UserInfo::listToScopes(userMap.value("scopes").toStringList()));
userInfo->setAllowedThingIds(allowedThingIds);
m_users->insertUser(userInfo);
}
}
@ -207,15 +242,20 @@ void UserManager::getUserInfoResponse(int commandId, const QVariantMap &data)
{
qCDebug(dcUserManager()) << "User info reply" << commandId << data;
QVariantMap userMap = data.value("userInfo").toMap();
QList<QUuid> allowedThingIds;
foreach (const QString &thingIdString, userMap.value("allowedThingIds").toStringList())
allowedThingIds.append(QUuid(thingIdString));
m_userInfo->setUsername(userMap.value("username").toString());
m_userInfo->setEmail(userMap.value("email").toString());
m_userInfo->setDisplayName(userMap.value("displayName").toString());
m_userInfo->setScopes(UserInfo::listToScopes(userMap.value("scopes").toStringList()));
m_userInfo->setAllowedThingIds(allowedThingIds);
}
void UserManager::getTokensResponse(int /*commandId*/, const QVariantMap &data)
void UserManager::getTokensResponse(int commandId, const QVariantMap &data)
{
Q_UNUSED(commandId)
foreach (const QVariant &tokenVariant, data.value("tokenInfoList").toList()) {
// qDebug() << "Token received" << tokenVariant.toMap();
QVariantMap token = tokenVariant.toMap();
@ -226,7 +266,6 @@ void UserManager::getTokensResponse(int /*commandId*/, const QVariantMap &data)
TokenInfo *tokenInfo = new TokenInfo(id, username, deviceName, creationTime);
m_tokenInfos->addToken(tokenInfo);
}
}
void UserManager::removeTokenResponse(int commandId, const QVariantMap &params)
@ -309,6 +348,13 @@ QVariant Users::data(const QModelIndex &index, int role) const
return m_users.at(index.row())->email();
case RoleScopes:
return static_cast<int>(m_users.at(index.row())->scopes());
case RoleAllowedThingIds: {
QVariantList thingIds;
foreach (const QUuid &thingId, m_users.at(index.row())->allowedThingIds())
thingIds.append(thingId);
return thingIds;
}
}
return QVariant();
}
@ -320,6 +366,7 @@ QHash<int, QByteArray> Users::roleNames() const
roles.insert(RoleDisplayName, "displayName");
roles.insert(RoleEmail, "email");
roles.insert(RoleScopes, "scopes");
roles.insert(RoleAllowedThingIds, "allowedThingIds");
return roles;
}
@ -344,6 +391,12 @@ void Users::insertUser(UserInfo *userInfo)
emit dataChanged(index(idx), index(idx), {RoleScopes});
}
});
connect(userInfo, &UserInfo::allowedThingIdsChanged, this, [=](){
int idx = m_users.indexOf(userInfo);
if (idx >= 0) {
emit dataChanged(index(idx), index(idx), {RoleAllowedThingIds});
}
});
beginInsertRows(QModelIndex(), static_cast<int>(m_users.count()), static_cast<int>(m_users.count()));
m_users.append(userInfo);

View File

@ -71,12 +71,12 @@ public:
Users *users() const;
// NOTE: Q_FLAG from another QObject (UserInfo::PermissionScopes) doesn't seem to work in certain Qt versions. Using int instead
Q_INVOKABLE int createUser(const QString &username, const QString &password, const QString &displayName, const QString &email, int permissionScopes = UserInfo::PermissionScopeAdmin);
Q_INVOKABLE int createUser(const QString &username, const QString &password, const QString &displayName, const QString &email, int permissionScopes = UserInfo::PermissionScopeAdmin, const QList<QUuid> &allowedThingIds = QList<QUuid>());
Q_INVOKABLE int changePassword(const QString &newPassword);
Q_INVOKABLE int removeToken(const QUuid &id);
Q_INVOKABLE int removeUser(const QString &username);
// NOTE: Q_FLAG from another QObject (UserInfo::PermissionScopes) doesn't seem to work in certain Qt versions. Using int instead
Q_INVOKABLE int setUserScopes(const QString &username, int permissionScopes);
Q_INVOKABLE int setUserScopes(const QString &username, int permissionScopes, const QList<QUuid> &allowedThingIds = QList<QUuid>());
Q_INVOKABLE int setUserInfo(const QString &username, const QString &displayName, const QString &email);
signals:
@ -124,7 +124,8 @@ public:
RoleUsername,
RoleDisplayName,
RoleEmail,
RoleScopes
RoleScopes,
RoleAllowedThingIds
};
Q_ENUM(Roles)
@ -137,14 +138,14 @@ public:
void insertUser(UserInfo *userInfo);
void removeUser(const QString &username);
Q_INVOKABLE UserInfo* get(int index) const;
Q_INVOKABLE UserInfo* getUserInfo(const QString &username) const;
Q_INVOKABLE UserInfo *get(int index) const;
Q_INVOKABLE UserInfo *getUserInfo(const QString &username) const;
signals:
void countChanged();
private:
QList<UserInfo*> m_users;
QList<UserInfo *> m_users;
};
#endif // USERMANAGER_H

View File

@ -31,6 +31,7 @@ import Nymea
import NymeaApp.Utils
import "../components"
import "../delegates"
SettingsPageBase {
id: root
@ -138,6 +139,7 @@ SettingsPageBase {
}
}
Component {
id: editUserInfoComponent
SettingsPageBase {
@ -190,6 +192,59 @@ SettingsPageBase {
}
}
Component {
id: configureAllowedThingsComponent
Page {
id: configureAllowedThingsPage
property UserInfo userInfo: null
header: NymeaHeader {
text: root.title
backButtonVisible: true
onBackPressed: pageStack.pop()
}
title: qsTr("Allowed things for") + " \"" + userInfo.username + "\""
ColumnLayout {
anchors.fill: parent
ListFilterInput {
id: filterInput
Layout.fillWidth: true
}
GroupedListView {
Layout.fillWidth: true
Layout.fillHeight: true
clip: true
model: ThingsProxy {
id: thingsProxy
engine: _engine
groupByInterface: true
nameFilter: filterInput.shown ? filterInput.text : ""
}
delegate: ThingDelegate {
id: thingDelegate
thing: thingsProxy.getThing(model.id)
canDelete: false
progressive: false
additionalItem: CheckBox {
checked: configureAllowedThingsPage.userInfo.thingAllowed(thingDelegate.thing.id)
onCheckedChanged: {
configureAllowedThingsPage.userInfo.allowThingId(thingDelegate.thing.id, checked)
userManager.setUserScopes(configureAllowedThingsPage.userInfo.username, configureAllowedThingsPage.userInfo.scopes, configureAllowedThingsPage.userInfo.allowedThingIds)
}
}
}
}
}
}
}
Component {
id: changePasswordComponent
SettingsPageBase {
@ -318,12 +373,12 @@ SettingsPageBase {
Component {
id: userDetailsComponent
SettingsPageBase {
id: userDetailsPage
title: qsTr("Manage %1").arg(userInfo.username)
property UserInfo userInfo: null
property bool restrictedThingAccess: userDetailsPage.userInfo.scopes & UserInfo.PermissionScopeAccessAllThings === 0
Component {
id: confirmUserDeletionComponent
@ -381,8 +436,9 @@ SettingsPageBase {
}
Repeater {
model: NymeaUtils.scopesModel
id: permissionRepeater
model: NymeaUtils.scopesModel
delegate: NymeaSwipeDelegate {
Layout.fillWidth: true
@ -416,7 +472,7 @@ SettingsPageBase {
// 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)
userManager.setUserScopes(userDetailsPage.userInfo.username, scopes, userDetailsPage.userInfo.allowedThingIds)
}
}
}
@ -425,7 +481,7 @@ SettingsPageBase {
SettingsPageSectionHeader {
text: qsTr("Acessable things")
visible: userDetailsPage.restrictedThingAccess
visible: (userDetailsPage.userInfo.scopes & UserInfo.PermissionScopeAccessAllThings) !== UserInfo.PermissionScopeAccessAllThings
Layout.fillWidth: true
}
@ -434,15 +490,14 @@ SettingsPageBase {
id: allowedThingsEntry
Layout.fillWidth: true
text: qsTr("Allowed things for this user")
visible: userDetailsPage.restrictedThingAccess
subText: userDetailsPage.userInfo.allowedThingIds.length + " " + qsTr("things accessable")
visible: (userDetailsPage.userInfo.scopes & UserInfo.PermissionScopeAccessAllThings) !== UserInfo.PermissionScopeAccessAllThings
progressive: true
onClicked: {
pageStack.push(configureAllowedThingsComponent, {userInfo: userDetailsPage.userInfo})
}
}
SettingsPageSectionHeader {
text: qsTr("Remove")
}

View File

@ -32,6 +32,7 @@ import "../delegates"
SettingsPageBase {
id: root
property Thing thing: null
busy: d.pendingCommand != -1
@ -47,12 +48,14 @@ SettingsPageBase {
ThingInfoPane {
id: infoPane
Layout.fillWidth: true
thing: root.thing
}
Menu {
id: deviceMenu
width: implicitWidth + app.margins
x: parent.width - width
@ -194,6 +197,7 @@ SettingsPageBase {
analogInputs: true
analogOutputs: true
}
Repeater {
model: ioModel
delegate: NymeaSwipeDelegate {
@ -269,6 +273,7 @@ SettingsPageBase {
}
property bool dirty: false
}
Button {
Layout.fillWidth: true
Layout.leftMargin: app.margins
@ -295,13 +300,16 @@ SettingsPageBase {
Component {
id: errorDialog
ErrorDialog { }
}
Component {
id: removeDialogComponent
NymeaDialog {
id: removeDialog
title: qsTr("Remove thing?")
text: qsTr("Are you sure you want to remove %1 and all associated settings?").arg(root.thing.name)
standardButtons: Dialog.Yes | Dialog.No
@ -314,8 +322,10 @@ SettingsPageBase {
Component {
id: renameDialog
Dialog {
id: dialog
width: parent.width * .8
x: (parent.width - width) / 2
y: app.margins
@ -340,6 +350,7 @@ SettingsPageBase {
Component {
id: ioConnectionsDialogComponent
NymeaDialog {
id: ioConnectionDialog
standardButtons: Dialog.NoButton
@ -356,18 +367,13 @@ SettingsPageBase {
text: qsTr("Connect \"%1\" to:").arg(ioConnectionDialog.ioStateType.displayName)
wrapMode: Text.WordWrap
}
// Label { text: "\n" } // Fake in some spacing
GridLayout {
columns: (ioConnectionDialog.width / 400) * 2
// Label {
// Layout.fillWidth: true
// text: qsTr("Thing")
// }
ComboBox {
id: ioThingComboBox
model: ThingsProxy {
id: connectableIODevices
engine: _engine
@ -395,13 +401,9 @@ SettingsPageBase {
}
}
// Label {
// Layout.fillWidth: true
// text: (ioConnectionDialog.ioStateType.ioType == Types.IOTypeDigitalInput || ioConnectionDialog.ioStateType.ioType == Types.IOTypeAnalogInput) ? qsTr("Output") : qsTr("Input")
// }
ComboBox {
id: ioStateComboBox
model: StateTypesProxy {
id: connectableStateTypes
stateTypes: connectableIODevices.get(ioThingComboBox.currentIndex).thingClass.stateTypes
@ -413,7 +415,7 @@ SettingsPageBase {
textRole: "displayName"
Layout.fillWidth: true
onCountChanged: {
// print("loading for:", ioConnectionDialog.inputWatcher.ioConnection.outputStateTypeId)
// print("loading for:", ioConnectionDialog.inputWatcher.ioConnection.outputStateTypeId)
for (var i = 0; i < connectableStateTypes.count; i++) {
print("checking:", connectableStateTypes.get(i).id)
if (ioConnectionDialog.ioStateType.ioType == Types.IOTypeDigitalInput || ioConnectionDialog.ioStateType.ioType == Types.IOTypeAnalogInput) {
@ -443,14 +445,16 @@ SettingsPageBase {
checked: ioConnectionDialog.isInput ? ioConnectionDialog.inputWatcher.ioConnection.inverted : ioConnectionDialog.outputWatcher.ioConnection.inverted
}
}
}
}
GridLayout {
id: buttonGrid
columns: width > (cancelButton.implicitWidth + disconnectButton.implicitWidth + connectButton.implicitWidth)
? 4 : 1
layoutDirection: columns == 1 ? Qt.RightToLeft : Qt.LeftToRight
Item {
Layout.fillWidth: true
}
@ -461,6 +465,7 @@ SettingsPageBase {
Layout.fillWidth: buttonGrid.columns === 1
onClicked: ioConnectionDialog.reject();
}
Button {
id: disconnectButton
text: qsTr("Disconnect")
@ -478,6 +483,7 @@ SettingsPageBase {
ioConnectionDialog.reject();
}
}
Button {
id: connectButton
text: qsTr("Connect")
@ -510,8 +516,6 @@ SettingsPageBase {
}
}
}
}
}
}