From 6ab4d49ee198b049393362fe5fc39e2ef99d79b3 Mon Sep 17 00:00:00 2001 From: Michael Zanetti Date: Thu, 29 Apr 2021 16:08:33 +0200 Subject: [PATCH] Add multi user support --- .../jsonrpc/configurationhandler.cpp | 8 +- libnymea-core/jsonrpc/integrationshandler.cpp | 58 ++-- .../jsonrpc/jsonrpcserverimplementation.cpp | 241 +++++++++------ .../jsonrpc/jsonrpcserverimplementation.h | 7 +- libnymea-core/jsonrpc/jsonvalidator.cpp | 4 +- libnymea-core/jsonrpc/ruleshandler.cpp | 2 +- libnymea-core/jsonrpc/usershandler.cpp | 220 ++++++++----- libnymea-core/jsonrpc/usershandler.h | 13 +- libnymea-core/libnymea-core.pro | 2 + libnymea-core/servers/tcpserver.cpp | 1 + libnymea-core/servers/websocketserver.cpp | 1 + libnymea-core/usermanager/userautorizer.cpp | 6 + libnymea-core/usermanager/userautorizer.h | 16 + libnymea-core/usermanager/userinfo.cpp | 46 +++ libnymea-core/usermanager/userinfo.h | 28 ++ libnymea-core/usermanager/usermanager.cpp | 289 ++++++++++++++---- libnymea-core/usermanager/usermanager.h | 13 +- libnymea/jsonrpc/jsonhandler.cpp | 6 +- libnymea/jsonrpc/jsonhandler.h | 3 +- libnymea/libnymea.pro | 1 + libnymea/types/typeutils.cpp | 70 +++++ libnymea/typeutils.h | 19 +- tests/auto/api.json | 255 +++++++++++++--- tests/auto/jsonrpc/testjsonrpc.cpp | 77 ++--- tests/auto/usermanager/testusermanager.cpp | 69 +++-- tests/testlib/nymeatestbase.cpp | 6 +- 26 files changed, 1054 insertions(+), 407 deletions(-) create mode 100644 libnymea-core/usermanager/userautorizer.cpp create mode 100644 libnymea-core/usermanager/userautorizer.h create mode 100644 libnymea/types/typeutils.cpp diff --git a/libnymea-core/jsonrpc/configurationhandler.cpp b/libnymea-core/jsonrpc/configurationhandler.cpp index ec861dfe..eb25923f 100644 --- a/libnymea-core/jsonrpc/configurationhandler.cpp +++ b/libnymea-core/jsonrpc/configurationhandler.cpp @@ -92,12 +92,12 @@ ConfigurationHandler::ConfigurationHandler(QObject *parent): QString description; QVariantMap params; QVariantMap returns; description = "Get the list of available timezones."; returns.insert("timeZones", QVariantList() << enumValueName(String)); - registerMethod("GetTimeZones", description, params, returns, "Use System.GetTimeZones instead."); + registerMethod("GetTimeZones", description, params, returns, Types::PermissionScopeNone, "Use System.GetTimeZones instead."); params.clear(); returns.clear(); description = "Returns a list of locale codes available for the server. i.e. en_US, de_AT"; returns.insert("languages", QVariantList() << enumValueName(String)); - registerMethod("GetAvailableLanguages", description, params, returns, "Use the locale property in the Handshake message instead."); + registerMethod("GetAvailableLanguages", description, params, returns, Types::PermissionScopeNone, "Use the locale property in the Handshake message instead."); params.clear(); returns.clear(); description = "Get all configuration parameters of the server."; @@ -135,13 +135,13 @@ ConfigurationHandler::ConfigurationHandler(QObject *parent): description = "Set the time zone of the server. See also: \"GetTimeZones\""; params.insert("timeZone", enumValueName(String)); returns.insert("configurationError", enumRef()); - registerMethod("SetTimeZone", description, params, returns, "Use System.SetTimeZone instead."); + registerMethod("SetTimeZone", description, params, returns, Types::PermissionScopeAdmin, "Use System.SetTimeZone instead."); params.clear(); returns.clear(); description = "Sets the server language to the given language. See also: \"GetAvailableLanguages\""; params.insert("language", enumValueName(String)); returns.insert("configurationError", enumRef()); - registerMethod("SetLanguage", description, params, returns, "Use the locale property in the Handshake message instead."); + registerMethod("SetLanguage", description, params, returns, Types::PermissionScopeAdmin, "Use the locale property in the Handshake message instead."); params.clear(); returns.clear(); description = "Enable or disable the debug server."; diff --git a/libnymea-core/jsonrpc/integrationshandler.cpp b/libnymea-core/jsonrpc/integrationshandler.cpp index abcbe0da..9db3fd58 100644 --- a/libnymea-core/jsonrpc/integrationshandler.cpp +++ b/libnymea-core/jsonrpc/integrationshandler.cpp @@ -102,7 +102,7 @@ IntegrationsHandler::IntegrationsHandler(ThingManager *thingManager, QObject *pa QString description; QVariantMap returns; QVariantMap params; description = "Returns a list of supported Vendors."; returns.insert("vendors", objectRef()); - registerMethod("GetVendors", description, params, returns); + registerMethod("GetVendors", description, params, returns, Types::PermissionScopeNone); params.clear(); returns.clear(); description = "Returns a list of supported thing classes, optionally filtered by vendorId or by a list of thing class ids."; @@ -110,26 +110,26 @@ IntegrationsHandler::IntegrationsHandler(ThingManager *thingManager, QObject *pa params.insert("o:thingClassIds", QVariantList() << enumValueName(Uuid)); returns.insert("thingError", enumRef()); returns.insert("o:thingClasses", objectRef()); - registerMethod("GetThingClasses", description, params, returns); + registerMethod("GetThingClasses", description, params, returns, Types::PermissionScopeNone); params.clear(); returns.clear(); description = "Returns a list of loaded plugins."; returns.insert("plugins", objectRef()); - registerMethod("GetPlugins", description, params, returns); + registerMethod("GetPlugins", description, params, returns, Types::PermissionScopeNone); params.clear(); returns.clear(); description = "Get a plugin's params."; params.insert("pluginId", enumValueName(Uuid)); returns.insert("thingError", enumRef()); returns.insert("o:configuration", objectRef()); - registerMethod("GetPluginConfiguration", description, params, returns); + registerMethod("GetPluginConfiguration", description, params, returns, Types::PermissionScopeConfigureThings); params.clear(); returns.clear(); description = "Set a plugin's params."; params.insert("pluginId", enumValueName(Uuid)); params.insert("configuration", objectRef()); returns.insert("thingError", enumRef()); - registerMethod("SetPluginConfiguration", description, params, returns); + registerMethod("SetPluginConfiguration", description, params, returns, Types::PermissionScopeConfigureThings); params.clear(); returns.clear(); description = "Add a new thing to the system. " @@ -146,7 +146,7 @@ IntegrationsHandler::IntegrationsHandler(ThingManager *thingManager, QObject *pa returns.insert("thingError", enumRef()); returns.insert("o:thingId", enumValueName(Uuid)); returns.insert("o:displayMessage", enumValueName(String)); - registerMethod("AddThing", description, params, returns); + registerMethod("AddThing", description, params, returns, Types::PermissionScopeConfigureThings); params.clear(); returns.clear(); description = "Pair a new thing. " @@ -178,7 +178,7 @@ IntegrationsHandler::IntegrationsHandler(ThingManager *thingManager, QObject *pa returns.insert("o:displayMessage", enumValueName(String)); returns.insert("o:oAuthUrl", enumValueName(String)); returns.insert("o:pin", enumValueName(String)); - registerMethod("PairThing", description, params, returns); + registerMethod("PairThing", description, params, returns, Types::PermissionScopeConfigureThings); params.clear(); returns.clear(); description = "Confirm an ongoing pairing. For SetupMethodUserAndPassword, provide the username in the \"username\" field " @@ -191,14 +191,14 @@ IntegrationsHandler::IntegrationsHandler(ThingManager *thingManager, QObject *pa returns.insert("thingError", enumRef()); returns.insert("o:displayMessage", enumValueName(String)); returns.insert("o:thingId", enumValueName(Uuid)); - registerMethod("ConfirmPairing", description, params, returns); + registerMethod("ConfirmPairing", description, params, returns, Types::PermissionScopeConfigureThings); params.clear(); returns.clear(); description = "Returns a list of configured things, optionally filtered by thingId."; params.insert("o:thingId", enumValueName(Uuid)); returns.insert("o:things", objectRef()); returns.insert("thingError", enumRef()); - registerMethod("GetThings", description, params, returns); + registerMethod("GetThings", description, params, returns, Types::PermissionScopeNone); params.clear(); returns.clear(); description = "Performs a thing discovery for things of the given thingClassId and returns the results. " @@ -211,7 +211,7 @@ IntegrationsHandler::IntegrationsHandler(ThingManager *thingManager, QObject *pa returns.insert("thingError", enumRef()); returns.insert("o:displayMessage", enumValueName(String)); returns.insert("o:thingDescriptors", objectRef()); - registerMethod("DiscoverThings", description, params, returns); + registerMethod("DiscoverThings", description, params, returns, Types::PermissionScopeConfigureThings); params.clear(); returns.clear(); description = "Reconfigure a thing. This comes down to removing and recreating a thing with new parameters " @@ -226,21 +226,21 @@ IntegrationsHandler::IntegrationsHandler(ThingManager *thingManager, QObject *pa params.insert("o:thingParams", objectRef()); returns.insert("thingError", enumRef()); returns.insert("o:displayMessage", enumValueName(String)); - registerMethod("ReconfigureThing", description, params, returns); + registerMethod("ReconfigureThing", description, params, returns, Types::PermissionScopeConfigureThings); params.clear(); returns.clear(); description = "Edit the name of a thing."; params.insert("thingId", enumValueName(Uuid)); params.insert("name", enumValueName(String)); returns.insert("thingError", enumRef()); - registerMethod("EditThing", description, params, returns); + registerMethod("EditThing", description, params, returns, Types::PermissionScopeConfigureThings); params.clear(); returns.clear(); description = "Change the settings of a thing."; params.insert("thingId", enumValueName(Uuid)); params.insert("settings", objectRef()); returns.insert("thingError", enumRef()); - registerMethod("SetThingSettings", description, params, returns); + registerMethod("SetThingSettings", description, params, returns, Types::PermissionScopeConfigureThings); params.clear(); returns.clear(); description = "Enable/disable logging for the given event type on the given thing."; @@ -248,7 +248,7 @@ IntegrationsHandler::IntegrationsHandler(ThingManager *thingManager, QObject *pa params.insert("eventTypeId", enumValueName(Uuid)); params.insert("enabled", enumValueName(Bool)); returns.insert("thingError", enumRef()); - registerMethod("SetEventLogging", description, params, returns); + registerMethod("SetEventLogging", description, params, returns, Types::PermissionScopeConfigureThings); params.clear(); returns.clear(); description = "Set the filter for the given state on the given thing."; @@ -256,7 +256,7 @@ IntegrationsHandler::IntegrationsHandler(ThingManager *thingManager, QObject *pa params.insert("stateTypeId", enumValueName(Uuid)); params.insert("filter", enumRef()); returns.insert("thingError", enumRef()); - registerMethod("SetStateFilter", description, params, returns); + registerMethod("SetStateFilter", description, params, returns, Types::PermissionScopeConfigureThings); params.clear(); returns.clear(); description = "Remove a thing from the system."; @@ -270,25 +270,25 @@ IntegrationsHandler::IntegrationsHandler(ThingManager *thingManager, QObject *pa params.insert("o:removePolicyList", removePolicyList); returns.insert("thingError", enumRef()); returns.insert("o:ruleIds", QVariantList() << enumValueName(Uuid)); - registerMethod("RemoveThing", description, params, returns); + registerMethod("RemoveThing", description, params, returns, Types::PermissionScopeConfigureThings); params.clear(); returns.clear(); description = "Get event types for a specified thingClassId."; params.insert("thingClassId", enumValueName(Uuid)); returns.insert("eventTypes", objectRef()); - registerMethod("GetEventTypes", description, params, returns); + registerMethod("GetEventTypes", description, params, returns, Types::PermissionScopeNone); params.clear(); returns.clear(); description = "Get action types for a specified thingClassId."; params.insert("thingClassId", enumValueName(Uuid)); returns.insert("actionTypes", objectRef()); - registerMethod("GetActionTypes", description, params, returns); + registerMethod("GetActionTypes", description, params, returns, Types::PermissionScopeNone); params.clear(); returns.clear(); description = "Get state types for a specified thingClassId."; params.insert("thingClassId", enumValueName(Uuid)); returns.insert("stateTypes", objectRef()); - registerMethod("GetStateTypes", description, params, returns); + registerMethod("GetStateTypes", description, params, returns, Types::PermissionScopeNone); params.clear(); returns.clear(); description = "Get the value of the given thing and the given stateType"; @@ -296,14 +296,14 @@ IntegrationsHandler::IntegrationsHandler(ThingManager *thingManager, QObject *pa params.insert("stateTypeId", enumValueName(Uuid)); returns.insert("thingError", enumRef()); returns.insert("o:value", enumValueName(Variant)); - registerMethod("GetStateValue", description, params, returns); + registerMethod("GetStateValue", description, params, returns, Types::PermissionScopeNone); params.clear(); returns.clear(); description = "Get all the state values of the given thing."; params.insert("thingId", enumValueName(Uuid)); returns.insert("thingError", enumRef()); returns.insert("o:values", objectRef()); - registerMethod("GetStateValues", description, params, returns); + registerMethod("GetStateValues", description, params, returns, Types::PermissionScopeNone); params.clear(); returns.clear(); description = "Browse a thing. " @@ -318,7 +318,7 @@ IntegrationsHandler::IntegrationsHandler(ThingManager *thingManager, QObject *pa returns.insert("thingError", enumRef()); returns.insert("o:displayMessage", enumValueName(String)); returns.insert("items", QVariantList() << objectRef("BrowserItem")); - registerMethod("BrowseThing", description, params, returns); + registerMethod("BrowseThing", description, params, returns, Types::PermissionScopeNone); params.clear(); returns.clear(); description = "Get a single item from the browser. " @@ -332,7 +332,7 @@ IntegrationsHandler::IntegrationsHandler(ThingManager *thingManager, QObject *pa returns.insert("thingError", enumRef()); returns.insert("o:displayMessage", enumValueName(String)); returns.insert("o:item", objectRef("BrowserItem")); - registerMethod("GetBrowserItem", description, params, returns); + registerMethod("GetBrowserItem", description, params, returns, Types::PermissionScopeNone); params.clear(); returns.clear(); description = "Execute a single action."; @@ -341,7 +341,7 @@ IntegrationsHandler::IntegrationsHandler(ThingManager *thingManager, QObject *pa params.insert("o:params", objectRef()); returns.insert("thingError", enumRef()); returns.insert("o:displayMessage", enumValueName(String)); - registerMethod("ExecuteAction", description, params, returns); + registerMethod("ExecuteAction", description, params, returns, Types::PermissionScopeControlThings); params.clear(); returns.clear(); description = "Execute the item identified by itemId on the given thing.\n" @@ -352,7 +352,7 @@ IntegrationsHandler::IntegrationsHandler(ThingManager *thingManager, QObject *pa params.insert("itemId", enumValueName(String)); returns.insert("thingError", enumRef()); returns.insert("o:displayMessage", enumValueName(String)); - registerMethod("ExecuteBrowserItem", description, params, returns); + registerMethod("ExecuteBrowserItem", description, params, returns, Types::PermissionScopeControlThings); params.clear(); returns.clear(); description = "Execute the action for the browser item identified by actionTypeId and the itemId on the given thing.\n" @@ -365,13 +365,13 @@ IntegrationsHandler::IntegrationsHandler(ThingManager *thingManager, QObject *pa params.insert("o:params", objectRef()); returns.insert("thingError", enumRef()); returns.insert("o:displayMessage", enumValueName(String)); - registerMethod("ExecuteBrowserItemAction", description, params, returns); + registerMethod("ExecuteBrowserItemAction", description, params, returns, Types::PermissionScopeControlThings); params.clear(); returns.clear(); description = "Fetch IO connections. Optionally filtered by thingId and stateTypeId."; params.insert("o:thingId", enumValueName(Uuid)); returns.insert("ioConnections", objectRef()); - registerMethod("GetIOConnections", description, params, returns); + registerMethod("GetIOConnections", description, params, returns, Types::PermissionScopeNone); params.clear(); returns.clear(); description = "Connect two generic IO states. Input and output need to be compatible, that is, either a digital input " @@ -383,13 +383,13 @@ IntegrationsHandler::IntegrationsHandler(ThingManager *thingManager, QObject *pa params.insert("o:inverted", enumValueName(Bool)); returns.insert("thingError", enumRef()); returns.insert("o:ioConnectionId", enumValueName(Uuid)); - registerMethod("ConnectIO", description, params, returns); + registerMethod("ConnectIO", description, params, returns, Types::PermissionScopeConfigureThings); params.clear(); returns.clear(); description = "Disconnect an existing IO connection."; params.insert("ioConnectionId", enumValueName(Uuid)); returns.insert("thingError", enumRef()); - registerMethod("DisconnectIO", description, params, returns); + registerMethod("DisconnectIO", description, params, returns, Types::PermissionScopeConfigureThings); // Notifications diff --git a/libnymea-core/jsonrpc/jsonrpcserverimplementation.cpp b/libnymea-core/jsonrpc/jsonrpcserverimplementation.cpp index 8639bf63..6eb8baeb 100644 --- a/libnymea-core/jsonrpc/jsonrpcserverimplementation.cpp +++ b/libnymea-core/jsonrpc/jsonrpcserverimplementation.cpp @@ -91,6 +91,7 @@ JsonRPCServerImplementation::JsonRPCServerImplementation(const QSslConfiguration registerEnum(); registerEnum(); registerEnum(); + registerFlag(); // Objects registerObject(); @@ -118,7 +119,9 @@ JsonRPCServerImplementation::JsonRPCServerImplementation(const QSslConfiguration "like initialSetupRequired might change if the setup has been performed in the meantime.\n " "The field cacheHashes may contain a map of methods and MD5 hashes. As long as the hash for " "a method does not change, a client may use a previously cached copy of the call instead of " - "fetching the content again."; + "fetching the content again. While the Hello call doesn't necessarily require a token, this " + "can be called with a token. If a token is provided, it will be verified and the reply contains " + "information about the tokens validity and the user and permissions for the given token."; params.insert("o:locale", enumValueName(String)); returns.insert("server", enumValueName(String)); returns.insert("name", enumValueName(String)); @@ -132,14 +135,17 @@ JsonRPCServerImplementation::JsonRPCServerImplementation(const QSslConfiguration returns.insert("pushButtonAuthAvailable", enumValueName(Bool)); returns.insert("o:experiences", QVariantList() << objectRef("Experience")); returns.insert("o:cacheHashes", QVariantList() << objectRef("CacheHash")); - registerMethod("Hello", description, params, returns); + returns.insert("o:authenticated", enumValueName(Bool)); + returns.insert("o:permissionScopes", flagRef()); + returns.insert("o:username", enumValueName(String)); + registerMethod("Hello", description, params, returns, Types::PermissionScopeNone); params.clear(); returns.clear(); description = "Introspect this API."; returns.insert("methods", enumValueName(Object)); returns.insert("notifications", enumValueName(Object)); returns.insert("types", enumValueName(Object)); - registerMethod("Introspect", description, params, returns); + registerMethod("Introspect", description, params, returns, Types::PermissionScopeNone); params.clear(); returns.clear(); description = "Version of this nymea/JSONRPC interface."; @@ -147,7 +153,7 @@ JsonRPCServerImplementation::JsonRPCServerImplementation(const QSslConfiguration returns.insert("protocol version", enumValueName(String)); returns.insert("qtVersion", enumValueName(String)); returns.insert("qtBuildVersion", enumValueName(String)); - registerMethod("Version", description, params, returns); + registerMethod("Version", description, params, returns, Types::PermissionScopeNone); params.clear(); returns.clear(); description = "Enable/Disable notifications for this connections. Either \"enabled\" or """ @@ -162,14 +168,18 @@ JsonRPCServerImplementation::JsonRPCServerImplementation(const QSslConfiguration params.insert("d:o:enabled", enumValueName(Bool)); returns.insert("namespaces", enumValueName(StringList)); returns.insert("d:enabled", enumValueName(Bool)); - registerMethod("SetNotificationStatus", description, params, returns); + registerMethod("SetNotificationStatus", description, params, returns, Types::PermissionScopeNone); params.clear(); returns.clear(); - description = "Create a new user in the API. Currently this is only allowed to be called once when a new nymea instance is set up. Call Authenticate after this to obtain a device token for this user."; + description = "Create a new user in the API. This is only allowed to be called when the initial setup is required. " + "To create additional users, use Users.CreateUser instead. Call Authenticate after this to obtain a " + "device token for the newly created user."; params.insert("username", enumValueName(String)); params.insert("password", enumValueName(String)); + params.insert("o:displayName", enumValueName(String)); + params.insert("o:email", enumValueName(String)); returns.insert("error", enumRef()); - registerMethod("CreateUser", description, params, returns, "Use Users.CreateUser instead."); + registerMethod("CreateUser", description, params, returns, Types::PermissionScopeAdmin); params.clear(); returns.clear(); description = "Authenticate a client to the api via user & password challenge. Provide " @@ -181,7 +191,9 @@ JsonRPCServerImplementation::JsonRPCServerImplementation(const QSslConfiguration params.insert("deviceName", enumValueName(String)); returns.insert("success", enumValueName(Bool)); returns.insert("o:token", enumValueName(String)); - registerMethod("Authenticate", description, params, returns, "Use Users.Authenticate instead."); + returns.insert("o:username", enumValueName(String)); + returns.insert("o:scopes", flagRef()); + registerMethod("Authenticate", description, params, returns, Types::PermissionScopeNone); params.clear(); returns.clear(); description = "Authenticate a client to the api via Push Button method. " @@ -200,18 +212,7 @@ JsonRPCServerImplementation::JsonRPCServerImplementation(const QSslConfiguration params.insert("deviceName", enumValueName(String)); returns.insert("success", enumValueName(Bool)); returns.insert("transactionId", enumValueName(Int)); - registerMethod("RequestPushButtonAuth", description, params, returns, "Use Users.RequestPushButtonAuth instead."); - - params.clear(); returns.clear(); - description = "Return a list of TokenInfo objects of all the tokens for the current user."; - returns.insert("tokenInfoList", QVariantList() << objectRef("TokenInfo")); - registerMethod("Tokens", description, params, returns, "Use Users.GetTokens instead."); - - params.clear(); returns.clear(); - description = "Revoke access for a given token."; - params.insert("tokenId", enumValueName(Uuid)); - returns.insert("error", enumRef()); - registerMethod("RemoveToken", description, params, returns, "Use Users.RemoveToken instead."); + registerMethod("RequestPushButtonAuth", description, params, returns, Types::PermissionScopeNone); params.clear(); returns.clear(); description = "Sets up the cloud connection by deploying a certificate and its configuration."; @@ -242,7 +243,7 @@ JsonRPCServerImplementation::JsonRPCServerImplementation(const QSslConfiguration params.insert("sessionId", enumValueName(String)); returns.insert("success", enumValueName(Bool)); returns.insert("sessionId", enumValueName(String)); - registerMethod("KeepAlive", description, params, returns); + registerMethod("KeepAlive", description, params, returns, Types::PermissionScopeNone); // Notifications params.clear(); returns.clear(); @@ -256,11 +257,14 @@ JsonRPCServerImplementation::JsonRPCServerImplementation(const QSslConfiguration params.insert("success", enumValueName(Bool)); params.insert("transactionId", enumValueName(Int)); params.insert("o:token", enumValueName(String)); - registerNotification("PushButtonAuthFinished", description, params, "Use Users.PushButtonAuthFinished instead."); + registerNotification("PushButtonAuthFinished", description, params); QMetaObject::invokeMethod(this, "setup", Qt::QueuedConnection); connect(NymeaCore::instance()->userManager(), &UserManager::pushButtonAuthFinished, this, &JsonRPCServerImplementation::onPushButtonAuthFinished); + + m_connectionLockdownTimer.setSingleShot(true); + m_connectionLockdownTimer.setInterval(3000); } /*! Returns the \e namespace of \l{JsonHandler}. */ @@ -286,7 +290,67 @@ JsonReply *JsonRPCServerImplementation::Hello(const QVariantMap ¶ms, const J delete m_newConnectionWaitTimers.take(clientId); } - return createReply(createWelcomeMessage(interface, clientId)); + // Compose the reply + QVariantMap handshake; + handshake.insert("server", "nymea"); + handshake.insert("name", NymeaCore::instance()->configuration()->serverName()); + handshake.insert("version", NYMEA_VERSION_STRING); + handshake.insert("uuid", NymeaCore::instance()->configuration()->serverUuid().toString()); + // "language" is deprecated + handshake.insert("language", m_clientLocales.value(clientId).name()); + handshake.insert("locale", m_clientLocales.value(clientId).name()); + handshake.insert("protocol version", JSON_PROTOCOL_VERSION); + handshake.insert("initialSetupRequired", (interface->configuration().authenticationEnabled ? NymeaCore::instance()->userManager()->initRequired() : false)); + handshake.insert("authenticationRequired", interface->configuration().authenticationEnabled); + handshake.insert("pushButtonAuthAvailable", NymeaCore::instance()->userManager()->pushButtonAuthAvailable()); + if (!m_experiences.isEmpty()) { + QVariantList experiences; + foreach (JsonHandler* handler, m_experiences.keys()) { + QVariantMap experience; + experience.insert("name", handler->name()); + experience.insert("version", m_experiences.value(handler)); + experiences.append(experience); + } + handshake.insert("experiences", experiences); + } + QVariantList cacheHashes; + foreach (const QString &handlerName, m_handlers.keys()) { + QHash hashes = m_handlers.value(handlerName)->cacheHashes(); + foreach (const QString &hashName, hashes.keys()) { + QVariantMap cacheHash; + cacheHash.insert("method", handlerName + "." + hashName); + cacheHash.insert("hash", hashes.value(hashName)); + cacheHashes.append(cacheHash); + } + } + if (!cacheHashes.isEmpty()) { + handshake.insert("cacheHashes", cacheHashes); + } + + bool badToken = false; + if (!context.token().isEmpty()) { + TokenInfo tokenInfo = NymeaCore::instance()->userManager()->tokenInfo(context.token()); + UserInfo userInfo = NymeaCore::instance()->userManager()->userInfo(tokenInfo.username()); + badToken = tokenInfo.id().isNull(); + handshake.insert("authenticated", !badToken); + handshake.insert("permissionScopes", Types::scopesToStringList(userInfo.scopes())); + handshake.insert("username", userInfo.username()); + } + + // If the connection is locked down already (because of a previous failed attempt) and authentication failed + // again, drop the client. He won't be able to reconnect until the lockdown timer runs out. + // This will give at max 2 attempts to present a valid token per lockdown period. + if (m_connectionLockdownTimer.isActive() && badToken) { + qCWarning(dcJsonRpc()) << "Dropping client because of repeated bad token authentication."; + interface->terminateClientConnection(clientId); + } + + if (badToken) { + qCWarning(dcJsonRpc()) << "Staring connection lockdown timer"; + m_connectionLockdownTimer.start(); + } + + return createReply(handshake);; } JsonReply* JsonRPCServerImplementation::Introspect(const QVariantMap ¶ms) const @@ -338,15 +402,17 @@ JsonReply *JsonRPCServerImplementation::CreateUser(const QVariantMap ¶ms) { QString username = params.value("username").toString(); QString password = params.value("password").toString(); + QString email = params.value("email").toString(); + QString displayName = params.value("displayName").toString(); - UserManager::UserError status = NymeaCore::instance()->userManager()->createUser(username, password); + UserManager::UserError status = NymeaCore::instance()->userManager()->createUser(username, password, email, displayName, Types::PermissionScopeAdmin); QVariantMap returns; returns.insert("error", enumValueName(status)); return createReply(returns); } -JsonReply *JsonRPCServerImplementation::Authenticate(const QVariantMap ¶ms) +JsonReply *JsonRPCServerImplementation::Authenticate(const QVariantMap ¶ms, const JsonContext &context) { QString username = params.value("username").toString(); QString password = params.value("password").toString(); @@ -357,7 +423,26 @@ JsonReply *JsonRPCServerImplementation::Authenticate(const QVariantMap ¶ms) ret.insert("success", !token.isEmpty()); if (!token.isEmpty()) { ret.insert("token", token); + TokenInfo tokenInfo = NymeaCore::instance()->userManager()->tokenInfo(token); + UserInfo userInfo = NymeaCore::instance()->userManager()->userInfo(tokenInfo.username()); + ret.insert("username", userInfo.username()); + ret.insert("scopes", Types::scopesToStringList(userInfo.scopes())); } + + // If the connection is locked down already (because of a previous failed attempt) and authentication failed + // again, drop the client. He won't be able to reconnect until the lockdown timer runs out. + // This will give at max 2 attempts to present a valid token per lockdown period. + if (m_connectionLockdownTimer.isActive() && token.isEmpty()) { + qCWarning(dcJsonRpc()) << "Dropping client because of repeated bad user/password authentication."; + TransportInterface *interface = reinterpret_cast(property("transportInterface").toLongLong()); + interface->terminateClientConnection(context.clientId()); + } + + if (token.isEmpty()) { + qCWarning(dcJsonRpc()) << "Staring connection lockdown timer"; + m_connectionLockdownTimer.start(); + } + return createReply(ret); } @@ -376,30 +461,6 @@ JsonReply *JsonRPCServerImplementation::RequestPushButtonAuth(const QVariantMap return createReply(data); } -JsonReply *JsonRPCServerImplementation::Tokens(const QVariantMap ¶ms, const JsonContext &context) const -{ - Q_UNUSED(params) - - TokenInfo tokenInfo = NymeaCore::instance()->userManager()->tokenInfo(context.token()); - QList tokens = NymeaCore::instance()->userManager()->tokens(tokenInfo.username()); - QVariantList retList; - foreach (const TokenInfo &tokenInfo, tokens) { - retList << pack(tokenInfo); - } - QVariantMap retMap; - retMap.insert("tokenInfoList", retList); - return createReply(retMap); -} - -JsonReply *JsonRPCServerImplementation::RemoveToken(const QVariantMap ¶ms) -{ - QUuid tokenId = params.value("tokenId").toUuid(); - UserManager::UserError error = NymeaCore::instance()->userManager()->removeToken(tokenId); - QVariantMap ret; - ret.insert("error", enumValueName(error)); - return createReply(ret); -} - JsonReply *JsonRPCServerImplementation::SetupCloudConnection(const QVariantMap ¶ms) { if (NymeaCore::instance()->cloudManager()->connectionState() != CloudManager::CloudConnectionStateUnconfigured) { @@ -539,46 +600,6 @@ void JsonRPCServerImplementation::sendUnauthorizedResponse(TransportInterface *i interface->sendData(clientId, data); } -QVariantMap JsonRPCServerImplementation::createWelcomeMessage(TransportInterface *interface, const QUuid &clientId) const -{ - QVariantMap handshake; - handshake.insert("server", "nymea"); - handshake.insert("name", NymeaCore::instance()->configuration()->serverName()); - handshake.insert("version", NYMEA_VERSION_STRING); - handshake.insert("uuid", NymeaCore::instance()->configuration()->serverUuid().toString()); - // "language" is deprecated - handshake.insert("language", m_clientLocales.value(clientId).name()); - handshake.insert("locale", m_clientLocales.value(clientId).name()); - handshake.insert("protocol version", JSON_PROTOCOL_VERSION); - handshake.insert("initialSetupRequired", (interface->configuration().authenticationEnabled ? NymeaCore::instance()->userManager()->initRequired() : false)); - handshake.insert("authenticationRequired", interface->configuration().authenticationEnabled); - handshake.insert("pushButtonAuthAvailable", NymeaCore::instance()->userManager()->pushButtonAuthAvailable()); - if (!m_experiences.isEmpty()) { - QVariantList experiences; - foreach (JsonHandler* handler, m_experiences.keys()) { - QVariantMap experience; - experience.insert("name", handler->name()); - experience.insert("version", m_experiences.value(handler)); - experiences.append(experience); - } - handshake.insert("experiences", experiences); - } - QVariantList cacheHashes; - foreach (const QString &handlerName, m_handlers.keys()) { - QHash hashes = m_handlers.value(handlerName)->cacheHashes(); - foreach (const QString &hashName, hashes.keys()) { - QVariantMap cacheHash; - cacheHash.insert("method", handlerName + "." + hashName); - cacheHash.insert("hash", hashes.value(hashName)); - cacheHashes.append(cacheHash); - } - } - if (!cacheHashes.isEmpty()) { - handshake.insert("cacheHashes", cacheHashes); - } - return handshake; -} - void JsonRPCServerImplementation::setup() { registerHandler(this); @@ -646,7 +667,8 @@ void JsonRPCServerImplementation::processJsonPacket(TransportInterface *interfac return; } - QStringList commandList = message.value("method").toString().split('.'); + QString methodString = message.value("method").toString(); + QStringList commandList = methodString.split('.'); if (commandList.count() != 2) { qCWarning(dcJsonRpc) << "Error parsing method.\nGot:" << message.value("method").toString() << "\nExpected: \"Namespace.method\""; sendErrorResponse(interface, clientId, commandId, QString("Error parsing method. Got: '%1'', Expected: 'Namespace.method'").arg(message.value("method").toString())); @@ -658,23 +680,38 @@ void JsonRPCServerImplementation::processJsonPacket(TransportInterface *interfac // check if authentication is required for this transport if (m_interfaces.value(interface)) { QByteArray token = message.value("token").toByteArray(); - QStringList authExemptMethodsNoUser = {"JSONRPC.Introspect", "JSONRPC.Hello", "JSONRPC.RequestPushButtonAuth", "JSONRPC.CreateUser", "Users.RequestPushButtonAuth", "Users.CreateUser"}; - QStringList authExemptMethodsWithUser = {"JSONRPC.Introspect", "JSONRPC.Hello", "JSONRPC.Authenticate", "JSONRPC.RequestPushButtonAuth", "Users.Authenticate", "Users.RequestPushButtonAuth"}; - // if there is no user in the system yet, let's fail unless this is special method for authentication itself + QStringList authExemptMethodsNoUser = {"JSONRPC.Introspect", "JSONRPC.Hello", "JSONRPC.RequestPushButtonAuth", "JSONRPC.CreateUser"}; + QStringList authExemptMethodsWithUser = {"JSONRPC.Introspect", "JSONRPC.Hello", "JSONRPC.Authenticate", "JSONRPC.RequestPushButtonAuth"}; + // if there is no user in the system yet, let's fail unless this is a special method for authentication itself if (NymeaCore::instance()->userManager()->initRequired()) { - if (!authExemptMethodsNoUser.contains(targetNamespace + "." + method) && (token.isEmpty() || !NymeaCore::instance()->userManager()->verifyToken(token))) { + if (!authExemptMethodsNoUser.contains(methodString) && (token.isEmpty() || !NymeaCore::instance()->userManager()->verifyToken(token))) { sendUnauthorizedResponse(interface, clientId, commandId, "Initial setup required. Call Users.CreateUser first."); qCWarning(dcJsonRpc()) << "Initial setup required but client does not call the setup. Dropping connection."; interface->terminateClientConnection(clientId); + qCWarning(dcJsonRpc()) << "Staring connection lockdown timer"; + m_connectionLockdownTimer.start(); return; } } else { // ok, we have a user. if there isn't a valid token, let's fail unless this is a Authenticate, Introspect Hello call - if (!authExemptMethodsWithUser.contains(targetNamespace + "." + method) && (token.isEmpty() || !NymeaCore::instance()->userManager()->verifyToken(token))) { - sendUnauthorizedResponse(interface, clientId, commandId, "Forbidden: Invalid token."); - qCWarning(dcJsonRpc()) << "Client did not not present a valid token. Dropping connection."; - interface->terminateClientConnection(clientId); - return; + if (!authExemptMethodsWithUser.contains(methodString)) { + if (token.isEmpty() || !NymeaCore::instance()->userManager()->verifyToken(token)) { + sendUnauthorizedResponse(interface, clientId, commandId, "Forbidden: Invalid token."); + qCWarning(dcJsonRpc()) << "Client did not not present a valid token. Dropping connection."; + interface->terminateClientConnection(clientId); + qCWarning(dcJsonRpc()) << "Staring connection lockdown timer"; + m_connectionLockdownTimer.start(); + return; + } + // Check if the user has the required permissions + TokenInfo tokenInfo = NymeaCore::instance()->userManager()->tokenInfo(token); + UserInfo userInfo = NymeaCore::instance()->userManager()->userInfo(tokenInfo.username()); + Types::PermissionScope methodScope = Types::scopeFromString(m_api.value("methods").toMap().value(methodString).toMap().value("permissionScope").toString()); + if (methodScope != Types::PermissionScopeNone && !userInfo.scopes().testFlag(Types::PermissionScopeAdmin) && !userInfo.scopes().testFlag(methodScope)) { + qCWarning(dcJsonRpc()) << "Method" << methodString << "requires" << Types::scopeToString(methodScope) << "but client token has:" << Types::scopesToStringList(userInfo.scopes()); + sendErrorResponse(interface, clientId, commandId, "Permission denied."); + return; + } } } } @@ -1017,6 +1054,12 @@ void JsonRPCServerImplementation::clientConnected(const QUuid &clientId) qCDebug(dcJsonRpc()) << "Client connected with uuid" << clientId.toString(); TransportInterface *interface = qobject_cast(sender()); + if (m_connectionLockdownTimer.isActive()) { + qCWarning(dcJsonRpc()) << "Connection is locked down. Rejecting new client connection."; + interface->terminateClientConnection(clientId); + return; + } + m_clientTransports.insert(clientId, interface); // Initialize the connection locale to the settings default diff --git a/libnymea-core/jsonrpc/jsonrpcserverimplementation.h b/libnymea-core/jsonrpc/jsonrpcserverimplementation.h index 1430526b..b699d4e0 100644 --- a/libnymea-core/jsonrpc/jsonrpcserverimplementation.h +++ b/libnymea-core/jsonrpc/jsonrpcserverimplementation.h @@ -63,10 +63,8 @@ public: Q_INVOKABLE JsonReply *SetNotificationStatus(const QVariantMap ¶ms, const JsonContext &context); Q_INVOKABLE JsonReply *CreateUser(const QVariantMap ¶ms); - Q_INVOKABLE JsonReply *Authenticate(const QVariantMap ¶ms); + Q_INVOKABLE JsonReply *Authenticate(const QVariantMap ¶ms, const JsonContext &context); Q_INVOKABLE JsonReply *RequestPushButtonAuth(const QVariantMap ¶ms, const JsonContext &context); - Q_INVOKABLE JsonReply *Tokens(const QVariantMap ¶ms, const JsonContext &context) const; - Q_INVOKABLE JsonReply *RemoveToken(const QVariantMap ¶ms); Q_INVOKABLE JsonReply *SetupCloudConnection(const QVariantMap ¶ms); Q_INVOKABLE JsonReply *SetupRemoteAccess(const QVariantMap ¶ms); Q_INVOKABLE JsonReply *IsCloudConnected(const QVariantMap ¶ms); @@ -90,7 +88,6 @@ private: void sendResponse(TransportInterface *interface, const QUuid &clientId, int commandId, const QVariantMap ¶ms = QVariantMap(), const QString &deprecationWarning = QString()); void sendErrorResponse(TransportInterface *interface, const QUuid &clientId, int commandId, const QString &error); void sendUnauthorizedResponse(TransportInterface *interface, const QUuid &clientId, int commandId, const QString &error); - QVariantMap createWelcomeMessage(TransportInterface *interface, const QUuid &clientId) const; void processJsonPacket(TransportInterface *interface, const QUuid &clientId, const QByteArray &data); @@ -129,6 +126,8 @@ private: int m_notificationId; + QTimer m_connectionLockdownTimer; + QString formatAssertion(const QString &targetNamespace, const QString &method, QMetaMethod::MethodType methodType, JsonHandler *handler, const QVariantMap &data) const; }; diff --git a/libnymea-core/jsonrpc/jsonvalidator.cpp b/libnymea-core/jsonrpc/jsonvalidator.cpp index 7a2b2409..3d134042 100644 --- a/libnymea-core/jsonrpc/jsonvalidator.cpp +++ b/libnymea-core/jsonrpc/jsonvalidator.cpp @@ -179,7 +179,7 @@ JsonValidator::Result JsonValidator::validateEntry(const QVariant &value, const QVariantList enumList = refDefinition.toList(); if (!enumList.contains(value.toString())) { - return Result(false, "Expected enum " + refName + " but got " + value.toJsonDocument().toJson()); + return Result(false, "Expected enum value for" + refName + " but got " + value.toString()); } return Result(true); } @@ -187,7 +187,7 @@ JsonValidator::Result JsonValidator::validateEntry(const QVariant &value, const QVariantMap flags = api.value("flags").toMap(); if (flags.contains(refName)) { QVariant refDefinition = flags.value(refName); - if (value.type() != QVariant::StringList) { + if (value.type() != QVariant::List && value.type() != QVariant::StringList) { return Result(false, "Expected flags " + refName + " but got " + value.toString()); } QString flagEnum = refDefinition.toList().first().toString(); diff --git a/libnymea-core/jsonrpc/ruleshandler.cpp b/libnymea-core/jsonrpc/ruleshandler.cpp index 60bceec8..bcdcdff0 100644 --- a/libnymea-core/jsonrpc/ruleshandler.cpp +++ b/libnymea-core/jsonrpc/ruleshandler.cpp @@ -107,7 +107,7 @@ RulesHandler::RulesHandler(QObject *parent) : description = "Get the descriptions of all configured rules. If you need more information about a specific rule use the " "method Rules.GetRuleDetails."; returns.insert("ruleDescriptions", QVariantList() << objectRef("RuleDescription")); - registerMethod("GetRules", description, params, returns); + registerMethod("GetRules", description, params, returns, Types::PermissionScopeConfigureRules); params.clear(); returns.clear(); description = "Get details for the rule identified by ruleId"; diff --git a/libnymea-core/jsonrpc/usershandler.cpp b/libnymea-core/jsonrpc/usershandler.cpp index 0bb8d996..563e3735 100644 --- a/libnymea-core/jsonrpc/usershandler.cpp +++ b/libnymea-core/jsonrpc/usershandler.cpp @@ -40,50 +40,23 @@ UsersHandler::UsersHandler(UserManager *userManager, QObject *parent): JsonHandler(parent), m_userManager(userManager) { - registerObject(); + registerFlag(); + registerObject(); registerObject(); QVariantMap params, returns; QString description; params.clear(); returns.clear(); - description = "Create a new user in the API. Currently this is only allowed to be called once when a new nymea instance is set up. Call Authenticate after this to obtain a device token for this user."; + description = "Create a new user in the API with the given username and password. Use scopes to define the permissions for the new user. If no scopes are given, this user will be an admin user. Call Authenticate after this to obtain a device token for this user."; params.insert("username", enumValueName(String)); params.insert("password", enumValueName(String)); + params.insert("o:email", enumValueName(String)); + params.insert("o:displayName", enumValueName(String)); + params.insert("o:scopes", flagRef()); returns.insert("error", enumRef()); registerMethod("CreateUser", description, params, returns); - params.clear(); returns.clear(); - description = "Authenticate a client to the api via user & password challenge. Provide " - "a device name which allows the user to identify the client and revoke the token in case " - "the device is lost or stolen. This will return a new token to be used to authorize a " - "client at the API."; - params.insert("username", enumValueName(String)); - params.insert("password", enumValueName(String)); - params.insert("deviceName", enumValueName(String)); - returns.insert("success", enumValueName(Bool)); - returns.insert("o:token", enumValueName(String)); - registerMethod("Authenticate", description, params, returns); - - params.clear(); returns.clear(); - description = "Authenticate a client to the api via Push Button method. " - "Provide a device name which allows the user to identify the client and revoke the " - "token in case the device is lost or stolen. If push button hardware is available, " - "this will return with success and start listening for push button presses. When the " - "push button is pressed, the PushButtonAuthFinished notification will be sent to the " - "requesting client. The procedure will be cancelled when the connection is interrupted. " - "If another client requests push button authentication while a procedure is still going " - "on, the second call will take over and the first one will be notified by the " - "PushButtonAuthFinished signal about the error. The application should make it clear " - "to the user to not press the button when the procedure fails as this can happen for 2 " - "reasons: a) a second user is trying to auth at the same time and only the currently " - "active user should press the button or b) it might indicate an attacker trying to take " - "over and snooping in for tokens."; - params.insert("deviceName", enumValueName(String)); - returns.insert("success", enumValueName(Bool)); - returns.insert("transactionId", enumValueName(Int)); - registerMethod("RequestPushButtonAuth", description, params, returns); - params.clear(); returns.clear(); description = "Change the password for the currently logged in user."; params.insert("newPassword", enumValueName(String)); @@ -94,7 +67,7 @@ UsersHandler::UsersHandler(UserManager *userManager, QObject *parent): description = "Get info about the current token (the currently logged in user)."; returns.insert("o:userInfo", objectRef()); returns.insert("error", enumRef()); - registerMethod("GetUserInfo", description, params, returns); + registerMethod("GetUserInfo", description, params, returns, Types::PermissionScopeNone); params.clear(); returns.clear(); description = "Get all the tokens for the current user."; @@ -108,7 +81,48 @@ UsersHandler::UsersHandler(UserManager *userManager, QObject *parent): returns.insert("error", enumRef()); registerMethod("RemoveToken", description, params, returns); + params.clear(); returns.clear(); + description = "Return a list of all users in the system."; + returns.insert("users", objectRef()); + registerMethod("GetUsers", description, params, returns); + + params.clear(); returns.clear(); + description = "Remove a user from the system."; + params.insert("username", enumValueName(String)); + returns.insert("error", enumRef()); + registerMethod("RemoveUser", description, params, returns); + + params.clear(); returns.clear(); + description = "Set the permissions (scopes) for a given user."; + params.insert("username", enumValueName(String)); + params.insert("scopes", flagRef()); + returns.insert("error", enumRef()); + registerMethod("SetUserScopes", description, params, returns); + + params.clear(); returns.clear(); + description = "Change user info. If username is given, info for the respective user is changed, otherwise the current user info is edited. Requires admin permissions to edit user info other than the own."; + params.insert("o:username", enumValueName(String)); + params.insert("o:displayName", enumValueName(String)); + params.insert("o:email", enumValueName(String)); + returns.insert("error", enumRef()); + registerMethod("SetUserInfo", description, params, returns); + // Notifications + params.clear(); + description = "Emitted when a user is added to the system."; + params.insert("userInfo", objectRef()); + registerNotification("UserAdded", description, params); + + params.clear(); + description = "Emitted when a user is removed from the system."; + params.insert("username", enumValueName(String)); + registerNotification("UserRemoved", description, params); + + params.clear(); + description = "Emitted whenever a user is changed."; + params.insert("userInfo", objectRef()); + registerNotification("UserChanged", description, params); + params.clear(); description = "Emitted when a push button authentication reaches final state. NOTE: This notification is " "special. It will only be emitted to connections that did actively request a push button " @@ -118,7 +132,21 @@ UsersHandler::UsersHandler(UserManager *userManager, QObject *parent): params.insert("o:token", enumValueName(String)); registerNotification("PushButtonAuthFinished", description, params); - connect(m_userManager, &UserManager::pushButtonAuthFinished, this, &UsersHandler::onPushButtonAuthFinished); + connect(m_userManager, &UserManager::userAdded, this, [this](const QString &username){ + QVariantMap params; + params.insert("userInfo", pack(m_userManager->userInfo(username))); + emit UserAdded(params); + }); + connect(m_userManager, &UserManager::userChanged, this, [this](const QString &username){ + QVariantMap params; + params.insert("userInfo", pack(m_userManager->userInfo(username))); + emit UserChanged(params); + }); + connect(m_userManager, &UserManager::userRemoved, this, [this](const QString &username){ + QVariantMap params; + params.insert("username", username); + emit UserRemoved(params); + }); } @@ -131,8 +159,12 @@ JsonReply *UsersHandler::CreateUser(const QVariantMap ¶ms) { QString username = params.value("username").toString(); QString password = params.value("password").toString(); + QString email = params.value("email").toString(); + QString displayName = params.value("displayName").toString(); + QStringList scopesList = params.value("scopes", Types::scopesToStringList(Types::PermissionScopeAdmin)).toStringList(); + Types::PermissionScopes scopes = Types::scopesFromStringList(scopesList); - UserManager::UserError status = m_userManager->createUser(username, password); + UserManager::UserError status = m_userManager->createUser(username, password, email, displayName, scopes); QVariantMap returns; returns.insert("error", enumValueName(status)); @@ -157,42 +189,14 @@ JsonReply *UsersHandler::ChangePassword(const QVariantMap ¶ms, const JsonCon } QString newPassword = params.value("newPassword").toString(); - QString username = m_userManager->userInfo(currentToken).username(); - UserManager::UserError status = m_userManager->changePassword(username, newPassword); + TokenInfo tokenInfo = m_userManager->tokenInfo(currentToken); + + UserManager::UserError status = m_userManager->changePassword(tokenInfo.username(), newPassword); ret.insert("error", enumValueName(status)); return createReply(ret); } -JsonReply *UsersHandler::Authenticate(const QVariantMap ¶ms) -{ - QString username = params.value("username").toString(); - QString password = params.value("password").toString(); - QString deviceName = params.value("deviceName").toString(); - - QByteArray token = m_userManager->authenticate(username, password, deviceName); - QVariantMap ret; - ret.insert("success", !token.isEmpty()); - if (!token.isEmpty()) { - ret.insert("token", token); - } - return createReply(ret); -} - -JsonReply *UsersHandler::RequestPushButtonAuth(const QVariantMap ¶ms, const JsonContext &context) -{ - QString deviceName = params.value("deviceName").toString(); - - int transactionId = m_userManager->requestPushButtonAuth(deviceName); - m_pushButtonTransactions.insert(transactionId, context.clientId()); - - QVariantMap data; - data.insert("transactionId", transactionId); - // TODO: return false if pushbutton auth is disabled in settings - data.insert("success", true); - return createReply(data); -} - JsonReply *UsersHandler::GetUserInfo(const QVariantMap ¶ms, const JsonContext &context) { Q_UNUSED(params) @@ -200,7 +204,7 @@ JsonReply *UsersHandler::GetUserInfo(const QVariantMap ¶ms, const JsonContex QByteArray currentToken = context.token(); if (currentToken.isEmpty()) { - qCWarning(dcJsonRpc()) << "Cannot get user info form an unauthenticated connection"; + qCWarning(dcJsonRpc()) << "Cannot get user info from an unauthenticated connection"; ret.insert("error", enumValueName(UserManager::UserErrorPermissionDenied)); return createReply(ret); } @@ -211,7 +215,9 @@ JsonReply *UsersHandler::GetUserInfo(const QVariantMap ¶ms, const JsonContex return createReply(ret); } - UserInfo userInfo = m_userManager->userInfo(currentToken); + TokenInfo tokenInfo = m_userManager->tokenInfo(currentToken); + + UserInfo userInfo = m_userManager->userInfo(tokenInfo.username()); ret.insert("userInfo", pack(userInfo)); ret.insert("error", enumValueName(UserManager::UserErrorNoError)); return createReply(ret); @@ -224,7 +230,7 @@ JsonReply *UsersHandler::GetTokens(const QVariantMap ¶ms, const JsonContext QByteArray currentToken = context.token(); if (currentToken.isEmpty()) { - qCWarning(dcJsonRpc()) << "Cannot fetch tokens form an unauthenticated connection"; + qCWarning(dcJsonRpc()) << "Cannot fetch tokens for an unauthenticated connection"; ret.insert("error", enumValueName(UserManager::UserErrorPermissionDenied)); return createReply(ret); } @@ -236,7 +242,7 @@ JsonReply *UsersHandler::GetTokens(const QVariantMap ¶ms, const JsonContext } TokenInfo tokenInfo = m_userManager->tokenInfo(currentToken); - qCDebug(dcJsonRpc()) << "Fetching tokens for user" << tokenInfo.username(); + qCDebug(dcJsonRpc()) << "Fetching tokens for user" << currentToken << tokenInfo.username(); QList tokens = m_userManager->tokens(tokenInfo.username()); QVariantList retList; foreach (const TokenInfo &tokenInfo, tokens) { @@ -285,24 +291,70 @@ JsonReply *UsersHandler::RemoveToken(const QVariantMap ¶ms, const JsonContex return createReply(ret); } -void UsersHandler::onPushButtonAuthFinished(int transactionId, bool success, const QByteArray &token) +JsonReply *UsersHandler::GetUsers(const QVariantMap ¶ms) { - Q_UNUSED(success) - Q_UNUSED(token) - QUuid clientId = m_pushButtonTransactions.take(transactionId); - if (clientId.isNull()) { - qCDebug(dcJsonRpc()) << "Received a PushButton reply but wasn't expecting it."; - return; + Q_UNUSED(params) + QVariantMap reply; + reply.insert("users", pack(m_userManager->users())); + return createReply(reply); +} + +JsonReply *UsersHandler::RemoveUser(const QVariantMap ¶ms, const JsonContext &context) +{ + Q_UNUSED(context) + QString username = params.value("username").toString(); + QVariantMap returns; + UserManager::UserError error = m_userManager->removeUser(username); + returns.insert("error", enumValueName(error)); + return createReply(returns); +} + +JsonReply *UsersHandler::SetUserScopes(const QVariantMap ¶ms, const JsonContext &context) +{ + Q_UNUSED(context) + QString username = params.value("username").toString(); + Types::PermissionScopes scopes = Types::scopesFromStringList(params.value("scopes").toStringList()); + UserManager::UserError error = m_userManager->setUserScopes(username, scopes); + QVariantMap returns; + returns.insert("error", enumValueName(error)); + return createReply(returns); +} + +JsonReply *UsersHandler::SetUserInfo(const QVariantMap ¶ms, const JsonContext &context) +{ + QVariantMap ret; + + TokenInfo callingTokenInfo = m_userManager->tokenInfo(context.token()); + QString username; + + if (params.contains("username")) { + username = params.value("username").toString(); + } else { + username = callingTokenInfo.username(); } - QVariantMap params; - params.insert("transactionId", transactionId); - params.insert("success", success); - if (success) { - params.insert("token", token); + if (callingTokenInfo.username() != username && !m_userManager->userInfo(callingTokenInfo.username()).scopes().testFlag(Types::PermissionScopeAdmin)) { + ret.insert("error", enumValueName(UserManager::UserErrorPermissionDenied)); + return createReply(ret); } - emit PushButtonAuthFinished(clientId, params); + UserInfo changedUserInfo = m_userManager->userInfo(username); + + QString email; + if (params.contains("email")) { + email = params.value("email").toString(); + } else { + email = changedUserInfo.email(); + } + QString displayName; + if (params.contains("displayName")) { + displayName = params.value("displayName").toString(); + } else { + displayName = changedUserInfo.displayName(); + } + UserManager::UserError status = m_userManager->setUserInfo(username, email, displayName); + ret.insert("error", enumValueName(status)); + return createReply(ret); } } diff --git a/libnymea-core/jsonrpc/usershandler.h b/libnymea-core/jsonrpc/usershandler.h index 10159720..2e048dea 100644 --- a/libnymea-core/jsonrpc/usershandler.h +++ b/libnymea-core/jsonrpc/usershandler.h @@ -49,17 +49,18 @@ public: Q_INVOKABLE JsonReply *CreateUser(const QVariantMap ¶ms); Q_INVOKABLE JsonReply *ChangePassword(const QVariantMap ¶ms, const JsonContext &context); - Q_INVOKABLE JsonReply *Authenticate(const QVariantMap ¶ms); - Q_INVOKABLE JsonReply *RequestPushButtonAuth(const QVariantMap ¶ms, const JsonContext &context); Q_INVOKABLE JsonReply *GetUserInfo(const QVariantMap ¶ms, const JsonContext &context); Q_INVOKABLE JsonReply *GetTokens(const QVariantMap ¶ms, const JsonContext &context); Q_INVOKABLE JsonReply *RemoveToken(const QVariantMap ¶ms, const JsonContext &context); + Q_INVOKABLE JsonReply *GetUsers(const QVariantMap ¶ms); + Q_INVOKABLE JsonReply *RemoveUser(const QVariantMap ¶ms, const JsonContext &context); + Q_INVOKABLE JsonReply *SetUserScopes(const QVariantMap ¶ms, const JsonContext &context); + Q_INVOKABLE JsonReply *SetUserInfo(const QVariantMap ¶ms, const JsonContext &context); signals: - void PushButtonAuthFinished(const QUuid &clientId, const QVariantMap ¶ms); - -private slots: - void onPushButtonAuthFinished(int transactionId, bool success, const QByteArray &token); + void UserAdded(const QVariantMap ¶ms); + void UserRemoved(const QVariantMap ¶ms); + void UserChanged(const QVariantMap ¶ms); private: UserManager *m_userManager = nullptr; diff --git a/libnymea-core/libnymea-core.pro b/libnymea-core/libnymea-core.pro index b34da011..e071fd8c 100644 --- a/libnymea-core/libnymea-core.pro +++ b/libnymea-core/libnymea-core.pro @@ -118,6 +118,7 @@ HEADERS += nymeacore.h \ logging/logentry.h \ logging/logvaluetool.h \ time/timemanager.h \ + usermanager/userautorizer.h \ usermanager/userinfo.h \ usermanager/usermanager.h \ usermanager/tokeninfo.h \ @@ -207,6 +208,7 @@ SOURCES += nymeacore.cpp \ logging/logentry.cpp \ logging/logvaluetool.cpp \ time/timemanager.cpp \ + usermanager/userautorizer.cpp \ usermanager/userinfo.cpp \ usermanager/usermanager.cpp \ usermanager/tokeninfo.cpp \ diff --git a/libnymea-core/servers/tcpserver.cpp b/libnymea-core/servers/tcpserver.cpp index 48b2a937..96cd478e 100644 --- a/libnymea-core/servers/tcpserver.cpp +++ b/libnymea-core/servers/tcpserver.cpp @@ -116,6 +116,7 @@ void TcpServer::terminateClientConnection(const QUuid &clientId) { QTcpSocket *client = m_clientList.value(clientId); if (client) { + client->flush(); client->close(); } } diff --git a/libnymea-core/servers/websocketserver.cpp b/libnymea-core/servers/websocketserver.cpp index 4fc196c4..4a1e0b93 100644 --- a/libnymea-core/servers/websocketserver.cpp +++ b/libnymea-core/servers/websocketserver.cpp @@ -121,6 +121,7 @@ void WebSocketServer::terminateClientConnection(const QUuid &clientId) { QWebSocket *client = m_clientList.value(clientId); if (client) { + client->flush(); client->close(); } } diff --git a/libnymea-core/usermanager/userautorizer.cpp b/libnymea-core/usermanager/userautorizer.cpp new file mode 100644 index 00000000..e3fbbab7 --- /dev/null +++ b/libnymea-core/usermanager/userautorizer.cpp @@ -0,0 +1,6 @@ +#include "userautorizer.h" + +UserAutorizer::UserAutorizer(QObject *parent) : QObject(parent) +{ + +} diff --git a/libnymea-core/usermanager/userautorizer.h b/libnymea-core/usermanager/userautorizer.h new file mode 100644 index 00000000..886ef26f --- /dev/null +++ b/libnymea-core/usermanager/userautorizer.h @@ -0,0 +1,16 @@ +#ifndef USERAUTORIZER_H +#define USERAUTORIZER_H + +#include + +class UserAutorizer : public QObject +{ + Q_OBJECT +public: + explicit UserAutorizer(QObject *parent = nullptr); + +signals: + +}; + +#endif // USERAUTORIZER_H diff --git a/libnymea-core/usermanager/userinfo.cpp b/libnymea-core/usermanager/userinfo.cpp index edd07451..6e4aec8e 100644 --- a/libnymea-core/usermanager/userinfo.cpp +++ b/libnymea-core/usermanager/userinfo.cpp @@ -1,5 +1,9 @@ #include "userinfo.h" +#include + +namespace nymeaserver { + UserInfo::UserInfo() { @@ -20,3 +24,45 @@ void UserInfo::setUsername(const QString &username) { m_username = username; } + +QString UserInfo::email() +{ + return m_email; +} + +void UserInfo::setEmail(const QString &email) +{ + m_email = email; +} + +QString UserInfo::displayName() const +{ + return m_displayName; +} + +void UserInfo::setDisplayName(const QString &displayName) +{ + m_displayName = displayName; +} + +Types::PermissionScopes UserInfo::scopes() const +{ + return m_scopes; +} + +void UserInfo::setScopes(Types::PermissionScopes scopes) +{ + m_scopes = scopes; +} + +QVariant UserInfoList::get(int index) const +{ + return QVariant::fromValue(at(index)); +} + +void UserInfoList::put(const QVariant &variant) +{ + append(variant.value()); +} + +} diff --git a/libnymea-core/usermanager/userinfo.h b/libnymea-core/usermanager/userinfo.h index 8423d470..4851a2a6 100644 --- a/libnymea-core/usermanager/userinfo.h +++ b/libnymea-core/usermanager/userinfo.h @@ -33,11 +33,18 @@ #include #include +#include +#include "typeutils.h" + +namespace nymeaserver { class UserInfo { Q_GADGET Q_PROPERTY(QString username READ username) + Q_PROPERTY(QString email READ email) + Q_PROPERTY(QString displayName READ displayName) + Q_PROPERTY(Types::PermissionScopes scopes READ scopes) public: UserInfo(); @@ -46,8 +53,29 @@ public: QString username() const; void setUsername(const QString &username); + QString email(); + void setEmail(const QString &email); + + QString displayName() const; + void setDisplayName(const QString &displayName); + + Types::PermissionScopes scopes() const; + void setScopes(Types::PermissionScopes scopes); + private: QString m_username; + QString m_email; + QString m_displayName; + Types::PermissionScopes m_scopes = Types::PermissionScopeNone; }; +class UserInfoList: public QList +{ + Q_GADGET + Q_PROPERTY(int count READ count) +public: + Q_INVOKABLE QVariant get(int index) const; + Q_INVOKABLE void put(const QVariant &variant); +}; +} #endif // USERINFO_H diff --git a/libnymea-core/usermanager/usermanager.cpp b/libnymea-core/usermanager/usermanager.cpp index 23d856d2..a2817c9e 100644 --- a/libnymea-core/usermanager/usermanager.cpp +++ b/libnymea-core/usermanager/usermanager.cpp @@ -77,6 +77,7 @@ #include #include #include +#include #include #include #include @@ -105,7 +106,7 @@ UserManager::UserManager(const QString &dbName, QObject *parent): if (QFileInfo(m_db.databaseName()).exists()) { rotate(m_db.databaseName()); if (!initDB()) { - qCWarning(dcLogEngine()) << "Error fixing user database. Giving up. Users can't be stored."; + qCWarning(dcUserManager()) << "Error fixing user database. Giving up. Users can't be stored."; } } } @@ -133,19 +134,23 @@ bool UserManager::initRequired() const } /*! Returns the list of user names for this UserManager. */ -QStringList UserManager::users() const +UserInfoList UserManager::users() const { - QString userQuery("SELECT username FROM users;"); + QString userQuery("SELECT * FROM users;"); QSqlQuery result = m_db.exec(userQuery); - QStringList ret; + UserInfoList users; while (result.next()) { - ret << result.value("username").toString(); + UserInfo info = UserInfo(result.value("username").toString()); + info.setEmail(result.value("email").toString()); + info.setDisplayName(result.value("displayName").toString()); + info.setScopes(Types::scopesFromStringList(result.value("scopes").toString().split(','))); + users.append(info); } - return ret; + return users; } /*! Creates a new user with the given \a username and \a password. Returns the \l UserError to inform about the result. */ -UserManager::UserError UserManager::createUser(const QString &username, const QString &password) +UserManager::UserError UserManager::createUser(const QString &username, const QString &password, const QString &email, const QString &displayName, Types::PermissionScopes scopes) { if (!validateUsername(username)) { qCWarning(dcUserManager) << "Error creating user. Invalid username:" << username; @@ -157,24 +162,33 @@ UserManager::UserError UserManager::createUser(const QString &username, const QS return UserErrorBadPassword; } - QString checkForDuplicateUserQuery = QString("SELECT * FROM users WHERE lower(username) = \"%1\";").arg(username.toLower()); - QSqlQuery result = m_db.exec(checkForDuplicateUserQuery); - if (result.first()) { + QSqlQuery checkForDuplicateUserQuery(m_db); + checkForDuplicateUserQuery.prepare("SELECT * FROM users WHERE lower(username) = ?;"); + checkForDuplicateUserQuery.addBindValue(username.toLower()); + checkForDuplicateUserQuery.exec(); + if (checkForDuplicateUserQuery.first()) { qCWarning(dcUserManager) << "Username already in use"; return UserErrorDuplicateUserId; } QByteArray salt = QUuid::createUuid().toString().remove(QRegExp("[{}]")).toUtf8(); QByteArray hashedPassword = QCryptographicHash::hash(QString(password + salt).toUtf8(), QCryptographicHash::Sha512).toBase64(); - QString queryString = QString("INSERT INTO users(username, password, salt) values(\"%1\", \"%2\", \"%3\");") - .arg(username.toLower()) - .arg(QString::fromUtf8(hashedPassword)) - .arg(QString::fromUtf8(salt)); - m_db.exec(queryString); - if (m_db.lastError().type() != QSqlError::NoError) { - qCWarning(dcUserManager) << "Error creating user:" << m_db.lastError().databaseText() << m_db.lastError().driverText(); + QSqlQuery query(m_db); + query.prepare("INSERT INTO users(username, email, displayName, password, salt, scopes) VALUES(?, ?, ?, ?, ?, ?);"); + query.addBindValue(username.toLower()); + query.addBindValue(email); + query.addBindValue(displayName); + query.addBindValue(QString::fromUtf8(hashedPassword)); + query.addBindValue(QString::fromUtf8(salt)); + query.addBindValue(Types::scopesToStringList(scopes).join(',')); + query.exec(); + if (query.lastError().type() != QSqlError::NoError) { + qCWarning(dcUserManager) << "Error creating user:" << query.lastError().databaseText() << query.lastError().driverText(); return UserErrorBackendError; } + + qCInfo(dcUserManager()) << "New user" << username << "added to the system with permissions:" << Types::scopesToStringList(scopes); + emit userAdded(username); return UserErrorNoError; } @@ -215,17 +229,49 @@ UserManager::UserError UserManager::changePassword(const QString &username, cons UserManager::UserError UserManager::removeUser(const QString &username) { - if (!username.isEmpty()) { - QString dropUserQuery = QString("DELETE FROM users WHERE lower(username) =\"%1\";").arg(username.toLower()); - QSqlQuery result = m_db.exec(dropUserQuery); - if (result.numRowsAffected() == 0) { - return UserErrorInvalidUserId; - } + QString dropUserQuery = QString("DELETE FROM users WHERE lower(username) =\"%1\";").arg(username.toLower()); + QSqlQuery result = m_db.exec(dropUserQuery); + if (result.numRowsAffected() == 0) { + return UserErrorInvalidUserId; } QString dropTokensQuery = QString("DELETE FROM tokens WHERE lower(username) = \"%1\";").arg(username.toLower()); m_db.exec(dropTokensQuery); + emit userRemoved(username); + return UserErrorNoError; +} + +UserManager::UserError UserManager::setUserScopes(const QString &username, Types::PermissionScopes scopes) +{ + QString scopesString = Types::scopesToStringList(scopes).join(','); + QSqlQuery setScopesQuery(m_db); + setScopesQuery.prepare("UPDATE users SET scopes = '%1' WHERE username = '%2'"); + setScopesQuery.addBindValue(scopesString); + setScopesQuery.addBindValue(username); + setScopesQuery.exec(); + if (setScopesQuery.lastError().type() != QSqlError::NoError) { + qCWarning(dcUserManager()) << "Error updating scopes for user" << username << setScopesQuery.lastError().databaseText() << setScopesQuery.lastError().driverText(); + return UserErrorBackendError; + } + + emit userChanged(username); + return UserErrorNoError; +} + +UserManager::UserError UserManager::setUserInfo(const QString &username, const QString &email, const QString &displayName) +{ + QSqlQuery query(m_db); + query.prepare("UPDATE users SET email = ?, displayName = ? WHERE username = ?;"); + query.addBindValue(email); + query.addBindValue(displayName); + query.addBindValue(username); + query.exec(); + if (query.lastError().type() != QSqlError::NoError) { + qCWarning(dcUserManager()) << "Error updating user info for user" << username << query.lastError().databaseText() << query.lastError().driverText() << query.executedQuery(); + return UserErrorBackendError; + } + emit userChanged(username); return UserErrorNoError; } @@ -241,18 +287,20 @@ bool UserManager::pushButtonAuthAvailable() const QByteArray UserManager::authenticate(const QString &username, const QString &password, const QString &deviceName) { if (!validateUsername(username)) { - qCWarning(dcUserManager) << "Username did not pass validation:" << username; + qCWarning(dcUserManager) << "Authenticate: Username did not pass validation:" << username; return QByteArray(); } - QString passwordQuery = QString("SELECT password, salt FROM users WHERE lower(username) = \"%1\";").arg(username.toLower()); - QSqlQuery result = m_db.exec(passwordQuery); - if (!result.first()) { + QSqlQuery passwordQuery(m_db); + passwordQuery.prepare("SELECT password, salt FROM users WHERE lower(username) = ?;"); + passwordQuery.addBindValue(username.toLower()); + passwordQuery.exec(); + if (!passwordQuery.first()) { qCWarning(dcUserManager) << "No such username" << username; return QByteArray(); } - QByteArray salt = result.value("salt").toByteArray(); - QByteArray hashedPassword = result.value("password").toByteArray(); + QByteArray salt = passwordQuery.value("salt").toByteArray(); + QByteArray hashedPassword = passwordQuery.value("password").toByteArray(); if (hashedPassword != QCryptographicHash::hash(QString(password + salt).toUtf8(), QCryptographicHash::Sha512).toBase64()) { qCWarning(dcUserManager) << "Authentication error for user:" << username; @@ -309,50 +357,47 @@ void UserManager::cancelPushButtonAuth(int transactionId) } -UserInfo UserManager::userInfo(const QByteArray &token) const +/*! Request UserInfo. + The UserInfo for the given username is returned. +*/ +UserInfo UserManager::userInfo(const QString &username) const { - TokenInfo tokenInfo = this->tokenInfo(token); - if (tokenInfo.id().isNull()) { - qCWarning(dcUserManager) << "Cannot fetch user info for invalid token:" << token; - return UserInfo(); - } - - // OK, this seems pointless, but data structures are prepared to have more details about users than just the username - // i.e. permissions etc will be in here at some point - QString getUserQuery = QString("SELECT username FROM users WHERE lower(username) = \"%1\";") - .arg(tokenInfo.username().toLower()); + QString getUserQuery = QString("SELECT * FROM users WHERE lower(username) = \"%1\";") + .arg(username); QSqlQuery result = m_db.exec(getUserQuery); if (m_db.lastError().type() != QSqlError::NoError) { - qCWarning(dcUserManager) << "Query for token failed:" << m_db.lastError().databaseText() << m_db.lastError().driverText() << getUserQuery; + qCWarning(dcUserManager) << "Query for user" << username << "failed:" << m_db.lastError().databaseText() << m_db.lastError().driverText() << getUserQuery; return UserInfo(); } if (!result.first()) { return UserInfo(); } - return UserInfo(result.value("username").toString()); + UserInfo userInfo = UserInfo(result.value("username").toString()); + userInfo.setEmail(result.value("email").toString()); + userInfo.setDisplayName(result.value("displayName").toString()); + userInfo.setScopes(Types::scopesFromStringList(result.value("scopes").toString().split(','))); + + return userInfo; } QList UserManager::tokens(const QString &username) const { QList ret; - if (!validateUsername(username)) { - qCWarning(dcUserManager) << "Username did not pass validation:" << username; - return ret; - } - QString getTokensQuery = QString("SELECT id, username, creationdate, deviceName FROM tokens WHERE lower(username) = \"%1\";") - .arg(username.toLower()); - QSqlQuery result = m_db.exec(getTokensQuery); + QSqlQuery query(m_db); + query.prepare("SELECT id, username, creationdate, deviceName FROM tokens WHERE lower(username) = ?;"); + query.addBindValue(username.toLower()); + query.exec(); if (m_db.lastError().type() != QSqlError::NoError) { - qCWarning(dcUserManager) << "Query for tokens failed:" << m_db.lastError().databaseText() << m_db.lastError().driverText() << getTokensQuery; + qCWarning(dcUserManager) << "Query for tokens failed:" << query.lastError().databaseText() << query.lastError().driverText() << query.executedQuery(); return ret; } - while (result.next()) { - ret << TokenInfo(result.value("id").toUuid(), result.value("username").toString(), result.value("creationdate").toDateTime(), result.value("devicename").toString()); + while (query.next()) { + ret << TokenInfo(query.value("id").toUuid(), query.value("username").toString(), query.value("creationdate").toDateTime(), query.value("devicename").toString()); } return ret; } @@ -441,30 +486,137 @@ bool UserManager::initDB() m_db.close(); if (!m_db.open()) { - qCWarning(dcUserManager()) << "Can't open user database. Init failed."; + dumpDBError("Can't open user database. Init failed."); return false; } + int currentVersion = -1; + int newVersion = 1; + if (m_db.tables().contains("metadata")) { + QSqlQuery query = m_db.exec("SELECT data FROM metadata WHERE `key` = 'version';"); + if (query.next()) { + currentVersion = query.value("data").toInt(); + } + } + if (!m_db.tables().contains("users")) { qCDebug(dcUserManager()) << "Empty user database. Setting up metadata..."; - m_db.exec("CREATE TABLE users (username VARCHAR(40) UNIQUE, password VARCHAR(100), salt VARCHAR(100));"); + m_db.exec("CREATE TABLE users (username VARCHAR(40) UNIQUE PRIMARY KEY, email VARCHAR(40), displayName VARCHAR(40), password VARCHAR(100), salt VARCHAR(100), scopes TEXT);"); if (m_db.lastError().isValid()) { - qCWarning(dcUserManager) << "Error initualizing user database. Driver error:" << m_db.lastError().driverText() << "Database error:" << m_db.lastError().databaseText(); + dumpDBError("Error initializing user database (table users)."); m_db.close(); return false; } + } else { + if (currentVersion < 1) { + m_db.exec("ALTER TABLE users ADD COLUMN scopes TEXT;"); + if (m_db.lastError().isValid()) { + dumpDBError("Error migrating user database (table users)."); + m_db.close(); + return false; + } + // Migrated existing users from before multiuser support are admins by default + QSqlQuery query(m_db); + query.prepare("UPDATE users SET scopes = ?;"); + query.addBindValue(Types::scopesToStringList(Types::PermissionScopeAdmin).join(',')); + query.exec(); + + if (query.lastError().isValid()) { + dumpDBError("Error migrating user database (updating existing users)."); + m_db.close(); + return false; + } + + m_db.exec("ALTER TABLE users ADD COLUMN email VARCHAR(40);"); + if (m_db.lastError().isValid()) { + dumpDBError("Error migrating user database (table users)."); + m_db.close(); + return false; + } + m_db.exec("ALTER TABLE users ADD COLUMN displayName VARCHAR(40);"); + if (m_db.lastError().isValid()) { + dumpDBError("Error migrating user database (table users)."); + m_db.close(); + return false; + } + + // Up until schema 1, username was an email. Copy it to initialize the email field. + m_db.exec("UPDATE users SET email = username;"); + if (m_db.lastError().isValid()) { + dumpDBError("Error migrating user database (table users)."); + m_db.close(); + return false; + } + currentVersion = 1; + } } if (!m_db.tables().contains("tokens")) { qCDebug(dcUserManager()) << "Empty user database. Setting up metadata..."; m_db.exec("CREATE TABLE tokens (id VARCHAR(40) UNIQUE, username VARCHAR(40), token VARCHAR(100) UNIQUE, creationdate DATETIME, devicename VARCHAR(40));"); if (m_db.lastError().isValid()) { - qCWarning(dcUserManager()) << "Error initializing user database. Driver error:" << m_db.lastError().driverText() << "Database error:" << m_db.lastError().databaseText(); + dumpDBError("Error initializing user database (table tokens)."); m_db.close(); return false; } } + if (m_db.tables().contains("metadata")) { + if (currentVersion < newVersion) { + m_db.exec(QString("UPDATE metadata SET data = %1 WHERE `key` = 'version')").arg(newVersion)); + if (m_db.lastError().isValid()) { + dumpDBError("Error updating up user database schema version!"); + m_db.close(); + return false; + } + qCInfo(dcUserManager()) << "Successfully migrated user database."; + } + } else { + m_db.exec("CREATE TABLE metadata (`key` VARCHAR(10), data VARCHAR(40));"); + if (m_db.lastError().isValid()) { + dumpDBError("Error setting up user database (table metadata)!"); + m_db.close(); + return false; + } + m_db.exec(QString("INSERT INTO metadata (`key`, `data`) VALUES ('version', %1);").arg(newVersion)); + if (m_db.lastError().isValid()) { + dumpDBError("Error setting up user database (setting version metadata)!"); + m_db.close(); + return false; + } + qCInfo(dcUserManager()) << "Successfully initialized user database."; + } + + + // Migration from before 1.0: + // Push button tokens were given out without an explicit user name + // If we have push button tokens (userId "") but no explicit user, let's create it as admin + // Users without valid username will have password login disabled. + QSqlQuery query(m_db); + query.prepare("SELECT * FROM tokens WHERE username = \"\";"); + query.exec(); + if (query.lastError().type() == QSqlError::NoError && query.next()) { + QSqlQuery query(m_db); + query.prepare("SELECT * FROM users WHERE username = \"\";"); + query.exec(); + if (!query.next()) { + qCDebug(dcUserManager()) << "Tokens existing but no user. Creating token admin user"; + QSqlQuery query(m_db); + query.prepare("INSERT INTO users(username, email, displayName, password, salt, scopes) values(?, ?, ?, ?, ?, ?);"); + query.addBindValue(""); + query.addBindValue(""); + query.addBindValue("Admin"); + query.addBindValue(""); + query.addBindValue(""); + query.addBindValue(Types::scopeToString(Types::PermissionScopeAdmin)); + query.exec(); + if (query.lastError().type() != QSqlError::NoError) { + qCWarning(dcUserManager) << "Error creating push button user:" << query.lastError().databaseText() << query.lastError().driverText(); + } + } + } + + qCDebug(dcUserManager()) << "User database initialized successfully."; return true; } @@ -486,7 +638,7 @@ void UserManager::rotate(const QString &dbName) bool UserManager::validateUsername(const QString &username) const { - QRegExp validator("(^[a-zA-Z0-9_\\.+-]+@[a-zA-Z0-9-_]+(\\.[a-zA-Z]+){1,2}$)"); + QRegExp validator("[a-zA-Z0-9_\\.+-@]{3,}"); return validator.exactMatch(username); } @@ -513,6 +665,11 @@ bool UserManager::validateToken(const QByteArray &token) const return validator.exactMatch(token); } +void UserManager::dumpDBError(const QString &message) +{ + qCCritical(dcUserManager) << message << "Driver error:" << m_db.lastError().driverText() << "Database error:" << m_db.lastError().databaseText(); +} + void UserManager::onPushButtonPressed() { if (m_pushButtonTransaction.first == -1) { @@ -520,6 +677,24 @@ void UserManager::onPushButtonPressed() return; } + // Creating a user without username and password. It won't be able to log in via user/password + QSqlQuery query(m_db); + query.prepare("SELECT * FROM users WHERE username = \"\";"); + query.exec(); + if (!query.next()) { + qCDebug(dcUserManager()) << "Creating token admin user"; + QSqlQuery query(m_db); + query.prepare("INSERT INTO users(username, password, salt, scopes) values(?, ?, ?, ?);"); + query.addBindValue(""); + query.addBindValue(""); + query.addBindValue(""); + query.addBindValue(Types::scopeToString(Types::PermissionScopeAdmin)); + query.exec(); + if (query.lastError().type() != QSqlError::NoError) { + qCWarning(dcUserManager) << "Error creating push button user:" << query.lastError().databaseText() << query.lastError().driverText(); + } + } + QByteArray token = QCryptographicHash::hash(QUuid::createUuid().toByteArray(), QCryptographicHash::Sha256).toBase64(); QString storeTokenQuery = QString("INSERT INTO tokens(id, username, token, creationdate, devicename) VALUES(\"%1\", \"%2\", \"%3\", \"%4\", \"%5\");") .arg(QUuid::createUuid().toString()) diff --git a/libnymea-core/usermanager/usermanager.h b/libnymea-core/usermanager/usermanager.h index 547c7354..085e306c 100644 --- a/libnymea-core/usermanager/usermanager.h +++ b/libnymea-core/usermanager/usermanager.h @@ -59,11 +59,13 @@ public: explicit UserManager(const QString &dbName, QObject *parent = nullptr); bool initRequired() const; - QStringList users() const; + UserInfoList users() const; - UserError createUser(const QString &username, const QString &password); + UserError createUser(const QString &username, const QString &password, const QString &email, const QString &displayName, Types::PermissionScopes scopes); UserError changePassword(const QString &username, const QString &newPassword); UserError removeUser(const QString &username); + UserError setUserScopes(const QString &username, Types::PermissionScopes scopes); + UserError setUserInfo(const QString &username, const QString &email, const QString &displayName); bool pushButtonAuthAvailable() const; @@ -71,7 +73,7 @@ public: int requestPushButtonAuth(const QString &deviceName); void cancelPushButtonAuth(int transactionId); - UserInfo userInfo(const QByteArray &token) const; + UserInfo userInfo(const QString &username = QString()) const; TokenInfo tokenInfo(const QByteArray &token) const; TokenInfo tokenInfo(const QUuid &tokenId) const; QList tokens(const QString &username) const; @@ -82,6 +84,9 @@ public: bool verifyToken(const QByteArray &token); signals: + void userAdded(const QString &username); + void userRemoved(const QString &username); + void userChanged(const QString &username); void pushButtonAuthFinished(int transactionId, bool success, const QByteArray &token); private: @@ -91,6 +96,8 @@ private: bool validatePassword(const QString &password) const; bool validateToken(const QByteArray &token) const; + void dumpDBError(const QString &message); + private slots: void onPushButtonPressed(); diff --git a/libnymea/jsonrpc/jsonhandler.cpp b/libnymea/jsonrpc/jsonhandler.cpp index d513e2b1..f1852f6e 100644 --- a/libnymea/jsonrpc/jsonhandler.cpp +++ b/libnymea/jsonrpc/jsonhandler.cpp @@ -147,12 +147,13 @@ void JsonHandler::registerObject(const QString &name, const QVariantMap &object) m_objects.insert(name, object); } -void JsonHandler::registerMethod(const QString &name, const QString &description, const QVariantMap ¶ms, const QVariantMap &returns, const QString &deprecationInfo) +void JsonHandler::registerMethod(const QString &name, const QString &description, const QVariantMap ¶ms, const QVariantMap &returns, Types::PermissionScope permissionScope, const QString &deprecationInfo) { QVariantMap methodData; methodData.insert("description", description); methodData.insert("params", params); methodData.insert("returns", returns); + methodData.insert("permissionScope", enumValueName(permissionScope)); if (!deprecationInfo.isEmpty()) { methodData.insert("deprecated", deprecationInfo); } @@ -285,7 +286,8 @@ QVariant JsonHandler::pack(const QMetaObject &metaObject, const void *value) con int flagValue = propertyValue.toInt(); QStringList flags; for (int i = 0; i < metaFlag.keyCount(); i++) { - if ((metaFlag.value(i) & flagValue) > 0) { + int flag = metaFlag.value(i) & flagValue; + if (flag == metaFlag.value(i) && flag > 0) { flags.append(metaFlag.key(i)); } } diff --git a/libnymea/jsonrpc/jsonhandler.h b/libnymea/jsonrpc/jsonhandler.h index e1258c44..c60e75ef 100644 --- a/libnymea/jsonrpc/jsonhandler.h +++ b/libnymea/jsonrpc/jsonhandler.h @@ -40,6 +40,7 @@ #include "jsonreply.h" #include "jsoncontext.h" +#include "typeutils.h" class JsonHandler : public QObject { @@ -108,7 +109,7 @@ protected: // Deprecated QString based registerObject void registerObject(const QString &name, const QVariantMap &object); - void registerMethod(const QString &name, const QString &description, const QVariantMap ¶ms, const QVariantMap &returns, const QString &deprecationInfo = QString()); + void registerMethod(const QString &name, const QString &description, const QVariantMap ¶ms, const QVariantMap &returns, Types::PermissionScope permissionScope = Types::PermissionScopeAdmin, const QString &deprecationInfo = QString()); void registerNotification(const QString &name, const QString &description, const QVariantMap ¶ms, const QString &deprecationInfo = QString()); JsonReply *createReply(const QVariantMap &data) const; diff --git a/libnymea/libnymea.pro b/libnymea/libnymea.pro index 2ea08f4c..b1ddf358 100644 --- a/libnymea/libnymea.pro +++ b/libnymea/libnymea.pro @@ -200,6 +200,7 @@ SOURCES += \ types/event.cpp \ types/eventdescriptor.cpp \ types/thingclass.cpp \ + types/typeutils.cpp \ types/vendor.cpp \ types/paramtype.cpp \ types/param.cpp \ diff --git a/libnymea/types/typeutils.cpp b/libnymea/types/typeutils.cpp new file mode 100644 index 00000000..698ff1fe --- /dev/null +++ b/libnymea/types/typeutils.cpp @@ -0,0 +1,70 @@ +/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * +* +* Copyright 2013 - 2021, nymea GmbH +* Contact: contact@nymea.io +* +* This file is part of nymea. +* This project including source code and documentation is protected by +* copyright law, and remains the property of nymea GmbH. All rights, including +* reproduction, publication, editing and translation, are reserved. The use of +* this project is subject to the terms of a license agreement to be concluded +* with nymea GmbH in accordance with the terms of use of nymea GmbH, available +* under https://nymea.io/license +* +* GNU Lesser General Public License Usage +* Alternatively, this project may be redistributed and/or modified under the +* terms of the GNU Lesser General Public License as published by the Free +* Software Foundation; version 3. This project is distributed in the hope that +* it will be useful, but WITHOUT ANY WARRANTY; without even the implied +* warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +* Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser General Public License +* along with this project. If not, see . +* +* For any further details and any questions please contact us under +* contact@nymea.io or see our FAQ/Licensing Information on +* https://nymea.io/license/faq +* +* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */ + +#include "typeutils.h" + +#include + +QStringList Types::scopesToStringList(Types::PermissionScopes scopes) +{ + QStringList ret; + QMetaEnum metaEnum = QMetaEnum::fromType(); + for (int i = 0; i < metaEnum.keyCount(); i++) { + if (scopes.testFlag(static_cast(metaEnum.value(i)))) { + ret << metaEnum.key(i); + } + } + return ret; +} + +QString Types::scopeToString(Types::PermissionScope scope) +{ + QMetaEnum metaEnum = QMetaEnum::fromType(); + return metaEnum.valueToKey(scope); +} + +Types::PermissionScope Types::scopeFromString(const QString &scopeString) +{ + QMetaEnum metaEnum = QMetaEnum::fromType(); + return static_cast(metaEnum.keyToValue(scopeString.toUtf8())); +} + +Types::PermissionScopes Types::scopesFromStringList(const QStringList &scopeList) +{ + PermissionScopes ret; + QMetaEnum metaEnum = QMetaEnum::fromType(); + for (int i = 0; i < metaEnum.keyCount(); i++) { + if (scopeList.contains(metaEnum.key(i))) { + ret |= static_cast(metaEnum.value(i)); + } + } + return ret; +} + diff --git a/libnymea/typeutils.h b/libnymea/typeutils.h index d94d6e83..282e53a2 100644 --- a/libnymea/typeutils.h +++ b/libnymea/typeutils.h @@ -1,6 +1,6 @@ /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -* Copyright 2013 - 2020, nymea GmbH +* Copyright 2013 - 2021, nymea GmbH * Contact: contact@nymea.io * * This file is part of nymea. @@ -182,6 +182,23 @@ public: StateValueFilterAdaptive }; Q_ENUM(StateValueFilter) + + enum PermissionScope { + PermissionScopeNone = 0x0000, + PermissionScopeControlThings = 0x0001, + PermissionScopeConfigureThings = 0x0003, + PermissionScopeExecuteRules = 0x0010, + PermissionScopeConfigureRules = 0x0030, + PermissionScopeAdmin = 0xFFFF, + }; + Q_ENUM(PermissionScope) + Q_DECLARE_FLAGS(PermissionScopes, PermissionScope) + Q_FLAG(PermissionScopes) + + static PermissionScopes scopesFromStringList(const QStringList &scopeList); + static PermissionScope scopeFromString(const QString &scopeString); + static QStringList scopesToStringList(PermissionScopes scopes); + static QString scopeToString(PermissionScope scope); }; Q_DECLARE_METATYPE(Types::InputType) diff --git a/tests/auto/api.json b/tests/auto/api.json index 4c3eafb4..7ce29dd3 100644 --- a/tests/auto/api.json +++ b/tests/auto/api.json @@ -162,6 +162,14 @@ "NetworkManagerStateConnectedSite", "NetworkManagerStateConnectedGlobal" ], + "PermissionScope": [ + "PermissionScopeNone", + "PermissionScopeControlThings", + "PermissionScopeConfigureThings", + "PermissionScopeExecuteRules", + "PermissionScopeConfigureRules", + "PermissionScopeAdmin" + ], "RemovePolicy": [ "RemovePolicyCascade", "RemovePolicyUpdate" @@ -403,6 +411,9 @@ "flags": { "CreateMethods": [ "$ref:CreateMethod" + ], + "PermissionScopes": [ + "$ref:PermissionScope" ] }, "methods": { @@ -413,6 +424,7 @@ "key": "String", "o:group": "String" }, + "permissionScope": "PermissionScopeAdmin", "returns": { "value": "String" } @@ -425,6 +437,7 @@ "o:group": "String", "value": "String" }, + "permissionScope": "PermissionScopeAdmin", "returns": { } }, @@ -433,6 +446,7 @@ "params": { "clientId": "String" }, + "permissionScope": "PermissionScopeAdmin", "returns": { "configurationError": "$ref:ConfigurationError" } @@ -442,6 +456,7 @@ "params": { "id": "String" }, + "permissionScope": "PermissionScopeAdmin", "returns": { "configurationError": "$ref:ConfigurationError" } @@ -451,6 +466,7 @@ "params": { "id": "String" }, + "permissionScope": "PermissionScopeAdmin", "returns": { "configurationError": "$ref:ConfigurationError" } @@ -460,6 +476,7 @@ "params": { "id": "String" }, + "permissionScope": "PermissionScopeAdmin", "returns": { "configurationError": "$ref:ConfigurationError" } @@ -469,6 +486,7 @@ "params": { "id": "String" }, + "permissionScope": "PermissionScopeAdmin", "returns": { "configurationError": "$ref:ConfigurationError" } @@ -478,6 +496,7 @@ "description": "Returns a list of locale codes available for the server. i.e. en_US, de_AT", "params": { }, + "permissionScope": "PermissionScopeNone", "returns": { "languages": [ "String" @@ -488,6 +507,7 @@ "description": "Get all configuration parameters of the server.", "params": { }, + "permissionScope": "PermissionScopeAdmin", "returns": { "basicConfiguration": { "d:language": "String", @@ -515,6 +535,7 @@ "description": "Get all MQTT broker policies.", "params": { }, + "permissionScope": "PermissionScopeAdmin", "returns": { "mqttPolicies": [ "$ref:MqttPolicy" @@ -525,6 +546,7 @@ "description": "Get all MQTT Server configurations.", "params": { }, + "permissionScope": "PermissionScopeAdmin", "returns": { "mqttServerConfigurations": [ "$ref:ServerConfiguration" @@ -536,6 +558,7 @@ "description": "Get the list of available timezones.", "params": { }, + "permissionScope": "PermissionScopeNone", "returns": { "timeZones": [ "String" @@ -547,6 +570,7 @@ "params": { "enabled": "Bool" }, + "permissionScope": "PermissionScopeAdmin", "returns": { "configurationError": "$ref:ConfigurationError" } @@ -556,6 +580,7 @@ "params": { "enabled": "String" }, + "permissionScope": "PermissionScopeAdmin", "returns": { "configurationError": "$ref:ConfigurationError" } @@ -566,6 +591,7 @@ "params": { "language": "String" }, + "permissionScope": "PermissionScopeAdmin", "returns": { "configurationError": "$ref:ConfigurationError" } @@ -575,6 +601,7 @@ "params": { "policy": "$ref:MqttPolicy" }, + "permissionScope": "PermissionScopeAdmin", "returns": { "configurationError": "$ref:ConfigurationError" } @@ -584,6 +611,7 @@ "params": { "configuration": "$ref:ServerConfiguration" }, + "permissionScope": "PermissionScopeAdmin", "returns": { "configurationError": "$ref:ConfigurationError" } @@ -593,6 +621,7 @@ "params": { "serverName": "String" }, + "permissionScope": "PermissionScopeAdmin", "returns": { "configurationError": "$ref:ConfigurationError" } @@ -602,6 +631,7 @@ "params": { "configuration": "$ref:ServerConfiguration" }, + "permissionScope": "PermissionScopeAdmin", "returns": { "configurationError": "$ref:ConfigurationError" } @@ -612,6 +642,7 @@ "params": { "timeZone": "String" }, + "permissionScope": "PermissionScopeAdmin", "returns": { "configurationError": "$ref:ConfigurationError" } @@ -621,6 +652,7 @@ "params": { "configuration": "$ref:WebServerConfiguration" }, + "permissionScope": "PermissionScopeAdmin", "returns": { "configurationError": "$ref:ConfigurationError" } @@ -630,6 +662,7 @@ "params": { "configuration": "$ref:ServerConfiguration" }, + "permissionScope": "PermissionScopeAdmin", "returns": { "configurationError": "$ref:ConfigurationError" } @@ -642,6 +675,7 @@ "o:thingParams": "$ref:ParamList", "thingClassId": "Uuid" }, + "permissionScope": "PermissionScopeConfigureThings", "returns": { "o:displayMessage": "String", "o:thingId": "Uuid", @@ -654,6 +688,7 @@ "o:itemId": "String", "thingId": "Uuid" }, + "permissionScope": "PermissionScopeNone", "returns": { "items": [ "$ref:BrowserItem" @@ -669,6 +704,7 @@ "o:username": "String", "pairingTransactionId": "Uuid" }, + "permissionScope": "PermissionScopeConfigureThings", "returns": { "o:displayMessage": "String", "o:thingId": "Uuid", @@ -684,6 +720,7 @@ "outputStateTypeId": "Uuid", "outputThingId": "Uuid" }, + "permissionScope": "PermissionScopeConfigureThings", "returns": { "o:ioConnectionId": "Uuid", "thingError": "$ref:ThingError" @@ -694,6 +731,7 @@ "params": { "ioConnectionId": "Uuid" }, + "permissionScope": "PermissionScopeConfigureThings", "returns": { "thingError": "$ref:ThingError" } @@ -704,6 +742,7 @@ "o:discoveryParams": "$ref:ParamList", "thingClassId": "Uuid" }, + "permissionScope": "PermissionScopeConfigureThings", "returns": { "o:displayMessage": "String", "o:thingDescriptors": "$ref:ThingDescriptors", @@ -716,6 +755,7 @@ "name": "String", "thingId": "Uuid" }, + "permissionScope": "PermissionScopeConfigureThings", "returns": { "thingError": "$ref:ThingError" } @@ -727,6 +767,7 @@ "o:params": "$ref:ParamList", "thingId": "Uuid" }, + "permissionScope": "PermissionScopeControlThings", "returns": { "o:displayMessage": "String", "thingError": "$ref:ThingError" @@ -738,6 +779,7 @@ "itemId": "String", "thingId": "Uuid" }, + "permissionScope": "PermissionScopeControlThings", "returns": { "o:displayMessage": "String", "thingError": "$ref:ThingError" @@ -751,6 +793,7 @@ "o:params": "$ref:ParamList", "thingId": "Uuid" }, + "permissionScope": "PermissionScopeControlThings", "returns": { "o:displayMessage": "String", "thingError": "$ref:ThingError" @@ -761,6 +804,7 @@ "params": { "thingClassId": "Uuid" }, + "permissionScope": "PermissionScopeNone", "returns": { "actionTypes": "$ref:ActionTypes" } @@ -771,6 +815,7 @@ "o:itemId": "String", "thingId": "Uuid" }, + "permissionScope": "PermissionScopeNone", "returns": { "o:displayMessage": "String", "o:item": "$ref:BrowserItem", @@ -782,6 +827,7 @@ "params": { "thingClassId": "Uuid" }, + "permissionScope": "PermissionScopeNone", "returns": { "eventTypes": "$ref:EventTypes" } @@ -791,6 +837,7 @@ "params": { "o:thingId": "Uuid" }, + "permissionScope": "PermissionScopeNone", "returns": { "ioConnections": "$ref:IOConnections" } @@ -800,6 +847,7 @@ "params": { "pluginId": "Uuid" }, + "permissionScope": "PermissionScopeConfigureThings", "returns": { "o:configuration": "$ref:ParamList", "thingError": "$ref:ThingError" @@ -809,6 +857,7 @@ "description": "Returns a list of loaded plugins.", "params": { }, + "permissionScope": "PermissionScopeNone", "returns": { "plugins": "$ref:IntegrationPlugins" } @@ -818,6 +867,7 @@ "params": { "thingClassId": "Uuid" }, + "permissionScope": "PermissionScopeNone", "returns": { "stateTypes": "$ref:StateTypes" } @@ -828,6 +878,7 @@ "stateTypeId": "Uuid", "thingId": "Uuid" }, + "permissionScope": "PermissionScopeNone", "returns": { "o:value": "Variant", "thingError": "$ref:ThingError" @@ -838,6 +889,7 @@ "params": { "thingId": "Uuid" }, + "permissionScope": "PermissionScopeNone", "returns": { "o:values": "$ref:States", "thingError": "$ref:ThingError" @@ -851,6 +903,7 @@ ], "o:vendorId": "Uuid" }, + "permissionScope": "PermissionScopeNone", "returns": { "o:thingClasses": "$ref:ThingClasses", "thingError": "$ref:ThingError" @@ -861,6 +914,7 @@ "params": { "o:thingId": "Uuid" }, + "permissionScope": "PermissionScopeNone", "returns": { "o:things": "$ref:Things", "thingError": "$ref:ThingError" @@ -870,6 +924,7 @@ "description": "Returns a list of supported Vendors.", "params": { }, + "permissionScope": "PermissionScopeNone", "returns": { "vendors": "$ref:Vendors" } @@ -883,6 +938,7 @@ "o:thingId": "Uuid", "o:thingParams": "$ref:ParamList" }, + "permissionScope": "PermissionScopeConfigureThings", "returns": { "o:displayMessage": "String", "o:oAuthUrl": "String", @@ -899,6 +955,7 @@ "o:thingId": "Uuid", "o:thingParams": "$ref:ParamList" }, + "permissionScope": "PermissionScopeConfigureThings", "returns": { "o:displayMessage": "String", "thingError": "$ref:ThingError" @@ -916,6 +973,7 @@ ], "thingId": "Uuid" }, + "permissionScope": "PermissionScopeConfigureThings", "returns": { "o:ruleIds": [ "Uuid" @@ -930,6 +988,7 @@ "eventTypeId": "Uuid", "thingId": "Uuid" }, + "permissionScope": "PermissionScopeConfigureThings", "returns": { "thingError": "$ref:ThingError" } @@ -940,6 +999,7 @@ "configuration": "$ref:ParamList", "pluginId": "Uuid" }, + "permissionScope": "PermissionScopeConfigureThings", "returns": { "thingError": "$ref:ThingError" } @@ -951,6 +1011,7 @@ "stateTypeId": "Uuid", "thingId": "Uuid" }, + "permissionScope": "PermissionScopeConfigureThings", "returns": { "thingError": "$ref:ThingError" } @@ -961,51 +1022,60 @@ "settings": "$ref:ParamList", "thingId": "Uuid" }, + "permissionScope": "PermissionScopeConfigureThings", "returns": { "thingError": "$ref:ThingError" } }, "JSONRPC.Authenticate": { - "deprecated": "Use Users.Authenticate instead.", "description": "Authenticate a client to the api via user & password challenge. Provide a device name which allows the user to identify the client and revoke the token in case the device is lost or stolen. This will return a new token to be used to authorize a client at the API.", "params": { "deviceName": "String", "password": "String", "username": "String" }, + "permissionScope": "PermissionScopeNone", "returns": { + "o:scopes": "$ref:PermissionScopes", "o:token": "String", + "o:username": "String", "success": "Bool" } }, "JSONRPC.CreateUser": { - "deprecated": "Use Users.CreateUser instead.", - "description": "Create a new user in the API. Currently this is only allowed to be called once when a new nymea instance is set up. Call Authenticate after this to obtain a device token for this user.", + "description": "Create a new user in the API. This is only allowed to be called when the initial setup is required. To create additional users, use Users.CreateUser instead. Call Authenticate after this to obtain a device token for the newly created user.", "params": { + "o:displayName": "String", + "o:email": "String", "password": "String", "username": "String" }, + "permissionScope": "PermissionScopeAdmin", "returns": { "error": "$ref:UserError" } }, "JSONRPC.Hello": { - "description": "Initiates a connection. Use this method to perform an initial handshake of the connection. Optionally, a parameter \"locale\" is can be passed to set up the used locale for this connection. Strings such as ThingClass displayNames etc will be localized to this locale. If this parameter is omitted, the default system locale (depending on the configuration) is used. The reply of this method contains information about this core instance such as version information, uuid and its name. The locale valueindicates the locale used for this connection. Note: This method can be called multiple times. The locale used in the last call for this connection will be used. Other values, like initialSetupRequired might change if the setup has been performed in the meantime.\n The field cacheHashes may contain a map of methods and MD5 hashes. As long as the hash for a method does not change, a client may use a previously cached copy of the call instead of fetching the content again.", + "description": "Initiates a connection. Use this method to perform an initial handshake of the connection. Optionally, a parameter \"locale\" is can be passed to set up the used locale for this connection. Strings such as ThingClass displayNames etc will be localized to this locale. If this parameter is omitted, the default system locale (depending on the configuration) is used. The reply of this method contains information about this core instance such as version information, uuid and its name. The locale valueindicates the locale used for this connection. Note: This method can be called multiple times. The locale used in the last call for this connection will be used. Other values, like initialSetupRequired might change if the setup has been performed in the meantime.\n The field cacheHashes may contain a map of methods and MD5 hashes. As long as the hash for a method does not change, a client may use a previously cached copy of the call instead of fetching the content again. While the Hello call doesn't necessarily require a token, this can be called with a token. If a token is provided, it will be verified and the reply contains information about the tokens validity and the user and permissions for the given token.", "params": { "o:locale": "String" }, + "permissionScope": "PermissionScopeNone", "returns": { "authenticationRequired": "Bool", "initialSetupRequired": "Bool", "language": "String", "locale": "String", "name": "String", + "o:authenticated": "Bool", "o:cacheHashes": [ "$ref:CacheHash" ], "o:experiences": [ "$ref:Experience" ], + "o:permissionScopes": "$ref:PermissionScopes", + "o:username": "String", "protocol version": "String", "pushButtonAuthAvailable": "Bool", "server": "String", @@ -1017,6 +1087,7 @@ "description": "Introspect this API.", "params": { }, + "permissionScope": "PermissionScopeNone", "returns": { "methods": "Object", "notifications": "Object", @@ -1027,6 +1098,7 @@ "description": "Check whether the cloud is currently connected. \"connected\" will be true whenever connectionState equals CloudConnectionStateConnected and is deprecated. Please use the connectionState value instead.", "params": { }, + "permissionScope": "PermissionScopeAdmin", "returns": { "connectionState": "$ref:CloudConnectionState", "d:connected": "Bool" @@ -1037,27 +1109,18 @@ "params": { "sessionId": "String" }, + "permissionScope": "PermissionScopeNone", "returns": { "sessionId": "String", "success": "Bool" } }, - "JSONRPC.RemoveToken": { - "deprecated": "Use Users.RemoveToken instead.", - "description": "Revoke access for a given token.", - "params": { - "tokenId": "Uuid" - }, - "returns": { - "error": "$ref:UserError" - } - }, "JSONRPC.RequestPushButtonAuth": { - "deprecated": "Use Users.RequestPushButtonAuth instead.", "description": "Authenticate a client to the api via Push Button method. Provide a device name which allows the user to identify the client and revoke the token in case the device is lost or stolen. If push button hardware is available, this will return with success and start listening for push button presses. When the push button is pressed, the PushButtonAuthFinished notification will be sent to the requesting client. The procedure will be cancelled when the connection is interrupted. If another client requests push button authentication while a procedure is still going on, the second call will take over and the first one will be notified by the PushButtonAuthFinished signal about the error. The application should make it clear to the user to not press the button when the procedure fails as this can happen for 2 reasons: a) a second user is trying to auth at the same time and only the currently active user should press the button or b) it might indicate an attacker trying to take over and snooping in for tokens.", "params": { "deviceName": "String" }, + "permissionScope": "PermissionScopeNone", "returns": { "success": "Bool", "transactionId": "Int" @@ -1069,6 +1132,7 @@ "d:o:enabled": "Bool", "o:namespaces": "StringList" }, + "permissionScope": "PermissionScopeNone", "returns": { "d:enabled": "Bool", "namespaces": "StringList" @@ -1083,6 +1147,7 @@ "publicKey": "String", "rootCA": "String" }, + "permissionScope": "PermissionScopeAdmin", "returns": { "success": "Bool" } @@ -1093,26 +1158,17 @@ "idToken": "String", "userId": "String" }, + "permissionScope": "PermissionScopeAdmin", "returns": { "message": "String", "status": "Int" } }, - "JSONRPC.Tokens": { - "deprecated": "Use Users.GetTokens instead.", - "description": "Return a list of TokenInfo objects of all the tokens for the current user.", - "params": { - }, - "returns": { - "tokenInfoList": [ - "$ref:TokenInfo" - ] - } - }, "JSONRPC.Version": { "description": "Version of this nymea/JSONRPC interface.", "params": { }, + "permissionScope": "PermissionScopeNone", "returns": { "protocol version": "String", "qtBuildVersion": "String", @@ -1150,6 +1206,7 @@ "Variant" ] }, + "permissionScope": "PermissionScopeAdmin", "returns": { "count": "Int", "loggingError": "$ref:LoggingError", @@ -1168,6 +1225,7 @@ "stopBits": "$ref:SerialPortStopBits", "timeout": "Uint" }, + "permissionScope": "PermissionScopeAdmin", "returns": { "modbusError": "$ref:ModbusRtuError", "o:modbusUuid": "Uuid" @@ -1177,6 +1235,7 @@ "description": "Get the list of configured modbus RTU masters.", "params": { }, + "permissionScope": "PermissionScopeAdmin", "returns": { "modbusError": "$ref:ModbusRtuError", "o:modbusRtuMasters": [ @@ -1188,6 +1247,7 @@ "description": "Get the list of available serial ports from the host system.", "params": { }, + "permissionScope": "PermissionScopeAdmin", "returns": { "serialPorts": "$ref:SerialPorts" } @@ -1204,6 +1264,7 @@ "stopBits": "$ref:SerialPortStopBits", "timeout": "Uint" }, + "permissionScope": "PermissionScopeAdmin", "returns": { "modbusError": "$ref:ModbusRtuError" } @@ -1213,6 +1274,7 @@ "params": { "modbusUuid": "Uuid" }, + "permissionScope": "PermissionScopeAdmin", "returns": { "modbusError": "$ref:ModbusRtuError" } @@ -1224,6 +1286,7 @@ "o:password": "String", "ssid": "String" }, + "permissionScope": "PermissionScopeAdmin", "returns": { "networkManagerError": "$ref:NetworkManagerError" } @@ -1233,6 +1296,7 @@ "params": { "interface": "String" }, + "permissionScope": "PermissionScopeAdmin", "returns": { "networkManagerError": "$ref:NetworkManagerError" } @@ -1242,6 +1306,7 @@ "params": { "enable": "Bool" }, + "permissionScope": "PermissionScopeAdmin", "returns": { "networkManagerError": "$ref:NetworkManagerError" } @@ -1251,6 +1316,7 @@ "params": { "enable": "Bool" }, + "permissionScope": "PermissionScopeAdmin", "returns": { "networkManagerError": "$ref:NetworkManagerError" } @@ -1259,6 +1325,7 @@ "description": "Get the list of current network devices.", "params": { }, + "permissionScope": "PermissionScopeAdmin", "returns": { "networkManagerError": "$ref:NetworkManagerError", "o:wiredNetworkDevices": [ @@ -1273,6 +1340,7 @@ "description": "Get the current network manager status.", "params": { }, + "permissionScope": "PermissionScopeAdmin", "returns": { "networkManagerError": "$ref:NetworkManagerError", "o:status": { @@ -1287,6 +1355,7 @@ "params": { "interface": "String" }, + "permissionScope": "PermissionScopeAdmin", "returns": { "networkManagerError": "$ref:NetworkManagerError", "o:wirelessAccessPoints": [ @@ -1299,6 +1368,7 @@ "params": { "interface": "String" }, + "permissionScope": "PermissionScopeAdmin", "returns": { "networkManagerError": "$ref:NetworkManagerError" } @@ -1310,6 +1380,7 @@ "password": "String", "ssid": "String" }, + "permissionScope": "PermissionScopeAdmin", "returns": { "networkManagerError": "$ref:NetworkManagerError" } @@ -1332,6 +1403,7 @@ "o:stateEvaluator": "$ref:StateEvaluator", "o:timeDescriptor": "$ref:TimeDescriptor" }, + "permissionScope": "PermissionScopeAdmin", "returns": { "o:ruleId": "Uuid", "ruleError": "$ref:RuleError" @@ -1342,6 +1414,7 @@ "params": { "ruleId": "Uuid" }, + "permissionScope": "PermissionScopeAdmin", "returns": { "ruleError": "$ref:RuleError" } @@ -1365,6 +1438,7 @@ "o:timeDescriptor": "$ref:TimeDescriptor", "ruleId": "Uuid" }, + "permissionScope": "PermissionScopeAdmin", "returns": { "o:rule": "$ref:Rule", "ruleError": "$ref:RuleError" @@ -1375,6 +1449,7 @@ "params": { "ruleId": "Uuid" }, + "permissionScope": "PermissionScopeAdmin", "returns": { "ruleError": "$ref:RuleError" } @@ -1384,6 +1459,7 @@ "params": { "ruleId": "Uuid" }, + "permissionScope": "PermissionScopeAdmin", "returns": { "ruleError": "$ref:RuleError" } @@ -1393,6 +1469,7 @@ "params": { "ruleId": "Uuid" }, + "permissionScope": "PermissionScopeAdmin", "returns": { "ruleError": "$ref:RuleError" } @@ -1402,6 +1479,7 @@ "params": { "thingId": "Uuid" }, + "permissionScope": "PermissionScopeAdmin", "returns": { "ruleIds": [ "Uuid" @@ -1413,6 +1491,7 @@ "params": { "ruleId": "Uuid" }, + "permissionScope": "PermissionScopeAdmin", "returns": { "o:rule": "$ref:Rule", "ruleError": "$ref:RuleError" @@ -1422,6 +1501,7 @@ "description": "Get the descriptions of all configured rules. If you need more information about a specific rule use the method Rules.GetRuleDetails.", "params": { }, + "permissionScope": "PermissionScopeConfigureRules", "returns": { "ruleDescriptions": [ "$ref:RuleDescription" @@ -1433,6 +1513,7 @@ "params": { "ruleId": "Uuid" }, + "permissionScope": "PermissionScopeAdmin", "returns": { "ruleError": "$ref:RuleError" } @@ -1443,6 +1524,7 @@ "content": "String", "name": "String" }, + "permissionScope": "PermissionScopeAdmin", "returns": { "o:errors": "StringList", "o:script": "$ref:Script", @@ -1456,6 +1538,7 @@ "o:content": "String", "o:name": "String" }, + "permissionScope": "PermissionScopeAdmin", "returns": { "o:errors": "StringList", "scriptError": "$ref:ScriptError" @@ -1466,6 +1549,7 @@ "params": { "id": "Uuid" }, + "permissionScope": "PermissionScopeAdmin", "returns": { "o:content": "String", "scriptError": "$ref:ScriptError" @@ -1475,6 +1559,7 @@ "description": "Get all script, that is, their names and properties, but no content.", "params": { }, + "permissionScope": "PermissionScopeAdmin", "returns": { "scripts": "$ref:Scripts" } @@ -1484,6 +1569,7 @@ "params": { "id": "Uuid" }, + "permissionScope": "PermissionScopeAdmin", "returns": { "scriptError": "$ref:ScriptError" } @@ -1492,6 +1578,7 @@ "description": "Instruct the system to poll the server for updates. Normally the system should automatically do this in regular intervals, however, if the client wants to allow the user to manually check for new updates now, this can be called. Returns true if the operation has been started successfully and the update manager will become busy. In order to know whether there are updates available, clients should walk through the list of packages retrieved from GetPackages and check whether there are packages with the updateAvailable flag set to true.", "params": { }, + "permissionScope": "PermissionScopeAdmin", "returns": { "success": "Bool" } @@ -1502,6 +1589,7 @@ "enabled": "Bool", "repositoryId": "String" }, + "permissionScope": "PermissionScopeAdmin", "returns": { "success": "Bool" } @@ -1510,6 +1598,7 @@ "description": "Get the list of capabilites on this system. The property \"powerManagement\" indicates whether restarting nymea and rebooting or shutting down is supported on this system. The property \"updateManagement indicates whether system update features are available in this system. The property \"timeManagement\" indicates whether the system time can be configured on this system. Note that GetTime will be available in any case.", "params": { }, + "permissionScope": "PermissionScopeAdmin", "returns": { "powerManagement": "Bool", "timeManagement": "Bool", @@ -1520,6 +1609,7 @@ "description": "Get the list of packages currently available to the system. This might include installed available but not installed packages. Installed packages will have the installedVersion set to a non-empty value.", "params": { }, + "permissionScope": "PermissionScopeAdmin", "returns": { "packages": "$ref:Packages" } @@ -1528,6 +1618,7 @@ "description": "Get the list of repositories currently available to the system.", "params": { }, + "permissionScope": "PermissionScopeAdmin", "returns": { "repositories": "$ref:Repositories" } @@ -1536,6 +1627,7 @@ "description": "Returns information about the system nymea is running on.", "params": { }, + "permissionScope": "PermissionScopeAdmin", "returns": { "deviceSerialNumber": "String" } @@ -1544,6 +1636,7 @@ "description": "Get the system time and configuraton. The \"time\" and \"timeZone\" properties give the current server time and time zone. \"automaticTimeAvailable\" indicates whether this system supports automatically setting the clock (e.g. using NTP). \"automaticTime\" will be true if the system is configured to automatically update the clock.", "params": { }, + "permissionScope": "PermissionScopeAdmin", "returns": { "automaticTime": "Bool", "automaticTimeAvailable": "Bool", @@ -1555,6 +1648,7 @@ "description": "Returns the list of IANA specified time zone IDs which can be used to select a time zone. It is not required to use this method if the client toolkit already provides means to obtain a list of IANA time zone ids.", "params": { }, + "permissionScope": "PermissionScopeAdmin", "returns": { "timeZones": "StringList" } @@ -1563,6 +1657,7 @@ "description": "Get the current status of the update system. \"busy\" indicates that the system is current busy with an operation regarding updates. This does not necessarily mean an actual update is running. When this is true, update related functions on the client should be marked as busy and no interaction with update components shall be allowed. An example for such a state is when the system queries the server if there are updates available, typically after a call to CheckForUpdates. \"updateRunning\" on the other hand indicates an actual update process is ongoing. The user should be informed about it, the system also might restart at any point while an update is running.", "params": { }, + "permissionScope": "PermissionScopeAdmin", "returns": { "busy": "Bool", "updateRunning": "Bool" @@ -1572,6 +1667,7 @@ "description": "Initiate a reboot of the system. The return value will indicate whether the procedure has been initiated successfully.", "params": { }, + "permissionScope": "PermissionScopeAdmin", "returns": { "success": "Bool" } @@ -1583,6 +1679,7 @@ "String" ] }, + "permissionScope": "PermissionScopeAdmin", "returns": { "success": "Bool" } @@ -1591,6 +1688,7 @@ "description": "Initiate a restart of the nymea service. The return value will indicate whether the procedure has been initiated successfully.", "params": { }, + "permissionScope": "PermissionScopeAdmin", "returns": { "success": "Bool" } @@ -1602,6 +1700,7 @@ "String" ] }, + "permissionScope": "PermissionScopeAdmin", "returns": { "success": "Bool" } @@ -1613,6 +1712,7 @@ "o:time": "Uint", "o:timeZone": "String" }, + "permissionScope": "PermissionScopeAdmin", "returns": { "success": "Bool" } @@ -1621,6 +1721,7 @@ "description": "Initiate a shutdown of the system. The return value will indicate whether the procedure has been initiated successfully.", "params": { }, + "permissionScope": "PermissionScopeAdmin", "returns": { "success": "Bool" } @@ -1632,6 +1733,7 @@ "String" ] }, + "permissionScope": "PermissionScopeAdmin", "returns": { "success": "Bool" } @@ -1641,6 +1743,7 @@ "params": { "tag": "$ref:Tag" }, + "permissionScope": "PermissionScopeAdmin", "returns": { "tagError": "$ref:TagError" } @@ -1653,6 +1756,7 @@ "o:tagId": "String", "o:thingId": "Uuid" }, + "permissionScope": "PermissionScopeAdmin", "returns": { "o:tags": "$ref:Tags", "tagError": "$ref:TagError" @@ -1663,37 +1767,31 @@ "params": { "tag": "$ref:Tag" }, + "permissionScope": "PermissionScopeAdmin", "returns": { "tagError": "$ref:TagError" } }, - "Users.Authenticate": { - "description": "Authenticate a client to the api via user & password challenge. Provide a device name which allows the user to identify the client and revoke the token in case the device is lost or stolen. This will return a new token to be used to authorize a client at the API.", - "params": { - "deviceName": "String", - "password": "String", - "username": "String" - }, - "returns": { - "o:token": "String", - "success": "Bool" - } - }, "Users.ChangePassword": { "description": "Change the password for the currently logged in user.", "params": { "newPassword": "String" }, + "permissionScope": "PermissionScopeAdmin", "returns": { "error": "$ref:UserError" } }, "Users.CreateUser": { - "description": "Create a new user in the API. Currently this is only allowed to be called once when a new nymea instance is set up. Call Authenticate after this to obtain a device token for this user.", + "description": "Create a new user in the API with the given username and password. Use scopes to define the permissions for the new user. If no scopes are given, this user will be an admin user. Call Authenticate after this to obtain a device token for this user.", "params": { + "o:displayName": "String", + "o:email": "String", + "o:scopes": "$ref:PermissionScopes", "password": "String", "username": "String" }, + "permissionScope": "PermissionScopeAdmin", "returns": { "error": "$ref:UserError" } @@ -1702,6 +1800,7 @@ "description": "Get all the tokens for the current user.", "params": { }, + "permissionScope": "PermissionScopeAdmin", "returns": { "error": "$ref:UserError", "o:tokenInfoList": "$ref:TokenInfoList" @@ -1711,28 +1810,62 @@ "description": "Get info about the current token (the currently logged in user).", "params": { }, + "permissionScope": "PermissionScopeNone", "returns": { "error": "$ref:UserError", "o:userInfo": "$ref:UserInfo" } }, + "Users.GetUsers": { + "description": "Return a list of all users in the system.", + "params": { + }, + "permissionScope": "PermissionScopeAdmin", + "returns": { + "users": "$ref:UserInfoList" + } + }, "Users.RemoveToken": { "description": "Revoke access for a given token.", "params": { "tokenId": "Uuid" }, + "permissionScope": "PermissionScopeAdmin", "returns": { "error": "$ref:UserError" } }, - "Users.RequestPushButtonAuth": { - "description": "Authenticate a client to the api via Push Button method. Provide a device name which allows the user to identify the client and revoke the token in case the device is lost or stolen. If push button hardware is available, this will return with success and start listening for push button presses. When the push button is pressed, the PushButtonAuthFinished notification will be sent to the requesting client. The procedure will be cancelled when the connection is interrupted. If another client requests push button authentication while a procedure is still going on, the second call will take over and the first one will be notified by the PushButtonAuthFinished signal about the error. The application should make it clear to the user to not press the button when the procedure fails as this can happen for 2 reasons: a) a second user is trying to auth at the same time and only the currently active user should press the button or b) it might indicate an attacker trying to take over and snooping in for tokens.", + "Users.RemoveUser": { + "description": "Remove a user from the system.", "params": { - "deviceName": "String" + "username": "String" }, + "permissionScope": "PermissionScopeAdmin", "returns": { - "success": "Bool", - "transactionId": "Int" + "error": "$ref:UserError" + } + }, + "Users.SetUserInfo": { + "description": "Change user info. If username is given, info for the respective user is changed, otherwise the current user info is edited. Requires admin permissions to edit user info other than the own.", + "params": { + "o:displayName": "String", + "o:email": "String", + "o:username": "String" + }, + "permissionScope": "PermissionScopeAdmin", + "returns": { + "error": "$ref:UserError" + } + }, + "Users.SetUserScopes": { + "description": "Set the permissions (scopes) for a given user.", + "params": { + "scopes": "$ref:PermissionScopes", + "username": "String" + }, + "permissionScope": "PermissionScopeAdmin", + "returns": { + "error": "$ref:UserError" } }, "Zigbee.AddNetwork": { @@ -1743,6 +1876,7 @@ "o:channelMask": "Uint", "serialPort": "String" }, + "permissionScope": "PermissionScopeAdmin", "returns": { "o:networkUuid": "Uuid", "zigbeeError": "$ref:ZigbeeError" @@ -1753,6 +1887,7 @@ "params": { "networkUuid": "Uuid" }, + "permissionScope": "PermissionScopeAdmin", "returns": { "zigbeeError": "$ref:ZigbeeError" } @@ -1761,6 +1896,7 @@ "description": "Get the list of available ZigBee adapters and serial ports in order to set up the ZigBee network on the desired interface. The 'serialPort' property can be used as unique identifier for an adapter. If an adapter hardware has been recognized as a well known ZigBee adapter, the 'hardwareRecognized' property will be true and the 'baudRate' and 'backend' configurations can be used as they where given, otherwise the user might set the backend and baud rate manually. The available backends can be fetched using the GetAvailableBackends method.", "params": { }, + "permissionScope": "PermissionScopeAdmin", "returns": { "adapters": "$ref:ZigbeeAdapters" } @@ -1769,6 +1905,7 @@ "description": "Get the list of available ZigBee backends.", "params": { }, + "permissionScope": "PermissionScopeAdmin", "returns": { "backends": [ "String" @@ -1779,6 +1916,7 @@ "description": "Returns the list of configured ZigBee networks in the system.", "params": { }, + "permissionScope": "PermissionScopeAdmin", "returns": { "zigbeeNetworks": [ "$ref:ZigbeeNetwork" @@ -1790,6 +1928,7 @@ "params": { "networkUuid": "Uuid" }, + "permissionScope": "PermissionScopeAdmin", "returns": { "o:zigbeeNodes": [ "$ref:ZigbeeNode" @@ -1802,6 +1941,7 @@ "params": { "networkUuid": "Uuid" }, + "permissionScope": "PermissionScopeAdmin", "returns": { "zigbeeError": "$ref:ZigbeeError" } @@ -1812,6 +1952,7 @@ "ieeeAddress": "String", "networkUuid": "Uuid" }, + "permissionScope": "PermissionScopeAdmin", "returns": { "zigbeeError": "$ref:ZigbeeError" } @@ -1823,6 +1964,7 @@ "networkUuid": "Uuid", "o:shortAddress": "Uint" }, + "permissionScope": "PermissionScopeAdmin", "returns": { "zigbeeError": "$ref:ZigbeeError" } @@ -1994,7 +2136,6 @@ } }, "JSONRPC.PushButtonAuthFinished": { - "deprecated": "Use Users.PushButtonAuthFinished instead.", "description": "Emitted when a push button authentication reaches final state. NOTE: This notification is special. It will only be emitted to connections that did actively request a push button authentication, but also it will be emitted regardless of the notification settings. ", "params": { "o:token": "String", @@ -2232,6 +2373,24 @@ "transactionId": "Int" } }, + "Users.UserAdded": { + "description": "Emitted when a user is added to the system.", + "params": { + "userInfo": "$ref:UserInfo" + } + }, + "Users.UserChanged": { + "description": "Emitted whenever a user is changed.", + "params": { + "userInfo": "$ref:UserInfo" + } + }, + "Users.UserRemoved": { + "description": "Emitted when a user is removed from the system.", + "params": { + "username": "String" + } + }, "Zigbee.AdapterAdded": { "description": "Emitted whenever a new ZigBee adapter or serial port has been detected in the system.", "params": { @@ -2672,8 +2831,14 @@ "$ref:TokenInfo" ], "UserInfo": { + "r:displayName": "String", + "r:email": "String", + "r:scopes": "$ref:PermissionScopes", "r:username": "String" }, + "UserInfoList": [ + "$ref:UserInfo" + ], "Vendor": { "displayName": "String", "id": "Uuid", diff --git a/tests/auto/jsonrpc/testjsonrpc.cpp b/tests/auto/jsonrpc/testjsonrpc.cpp index eb2ce9dc..5b3725cd 100644 --- a/tests/auto/jsonrpc/testjsonrpc.cpp +++ b/tests/auto/jsonrpc/testjsonrpc.cpp @@ -235,8 +235,8 @@ void TestJSONRPC::testHandshakeLocale() void TestJSONRPC::testInitialSetup() { - foreach (const QString &user, NymeaCore::instance()->userManager()->users()) { - NymeaCore::instance()->userManager()->removeUser(user); + foreach (const UserInfo &userInfo, NymeaCore::instance()->userManager()->users()) { + NymeaCore::instance()->userManager()->removeUser(userInfo.username()); } NymeaCore::instance()->userManager()->removeUser(""); @@ -245,7 +245,6 @@ void TestJSONRPC::testInitialSetup() QSignalSpy spy(m_mockTcpServer, SIGNAL(outgoingData(QUuid,QByteArray))); QVERIFY(spy.isValid()); - QSignalSpy connectedSpy(m_mockTcpServer, &MockTcpServer::clientConnected); QSignalSpy disconnectedSpy(m_mockTcpServer, &MockTcpServer::clientDisconnected); // Introspect call should work in any case @@ -293,11 +292,10 @@ void TestJSONRPC::testInitialSetup() if (disconnectedSpy.count() == 0) disconnectedSpy.wait(); QCOMPARE(disconnectedSpy.count(), 1); qCDebug(dcTests()) << "Mock client disconnected"; - connectedSpy.clear(); + + // The connection will be locked down for 3 seconds + QTest::qWait(3200); emit m_mockTcpServer->clientConnected(m_clientId); - if (connectedSpy.count() == 0) connectedSpy.wait(); - QCOMPARE(connectedSpy.count(), 1); - qCDebug(dcTests()) << "Mock client connected"; spy.clear(); m_mockTcpServer->injectData(m_clientId, "{\"id\": 0, \"method\": \"JSONRPC.Hello\"}"); @@ -311,7 +309,7 @@ void TestJSONRPC::testInitialSetup() // But it should still fail when giving a an invalid username spy.clear(); qCDebug(dcTests()) << "Calling CreateUser, expecting failure (bad username)"; - m_mockTcpServer->injectData(m_clientId, "{\"id\": 555, \"method\": \"JSONRPC.CreateUser\", \"params\": {\"username\": \"dummy\", \"password\": \"DummyPW1!\"}}\n"); + m_mockTcpServer->injectData(m_clientId, "{\"id\": 555, \"method\": \"JSONRPC.CreateUser\", \"params\": {\"username\": \"a\", \"password\": \"DummyPW1!\"}}\n"); if (spy.count() == 0) { spy.wait(); } @@ -325,7 +323,7 @@ void TestJSONRPC::testInitialSetup() // or when giving a bad password spy.clear(); qCDebug(dcTests()) << "Calling CreateUser, expecting failure (bad password)"; - m_mockTcpServer->injectData(m_clientId, "{\"id\": 555, \"method\": \"JSONRPC.CreateUser\", \"params\": {\"username\": \"dummy@guh.io\", \"password\": \"weak\"}}\n"); + m_mockTcpServer->injectData(m_clientId, "{\"id\": 555, \"method\": \"JSONRPC.CreateUser\", \"params\": {\"username\": \"nymea\", \"password\": \"weak\"}}\n"); if (spy.count() == 0) { spy.wait(); } @@ -381,12 +379,10 @@ void TestJSONRPC::testInitialSetup() // Connection should terminate if (disconnectedSpy.count() == 0) disconnectedSpy.wait(); QCOMPARE(disconnectedSpy.count(), 1); - qCDebug(dcTests()) << "Mock client disconnected"; - connectedSpy.clear(); + + // The connection will be locked down for 3 secs + QTest::qWait(3200); emit m_mockTcpServer->clientConnected(m_clientId); - if (connectedSpy.count() == 0) connectedSpy.wait(); - QCOMPARE(connectedSpy.count(), 1); - qCDebug(dcTests()) << "Mock client connected"; spy.clear(); m_mockTcpServer->injectData(m_clientId, "{\"id\": 0, \"method\": \"JSONRPC.Hello\"}"); @@ -414,6 +410,7 @@ void TestJSONRPC::testInitialSetup() // Now lets authenticate with a wrong password spy.clear(); + disconnectedSpy.clear(); qCDebug(dcTests()) << "Calling Authenticate, expecting failure (bad password)"; m_mockTcpServer->injectData(m_clientId, "{\"id\": 555, \"method\": \"JSONRPC.Authenticate\", \"params\": {\"username\": \"Dummy@guh.io\", \"password\": \"wrongpw\", \"deviceName\": \"testcase\"}}\n"); if (spy.count() == 0) { @@ -422,11 +419,24 @@ void TestJSONRPC::testInitialSetup() QVERIFY(spy.count() == 1); jsonDoc = QJsonDocument::fromJson(spy.first().at(1).toByteArray()); response = jsonDoc.toVariant().toMap(); - qCDebug(dcTests()) << "Calling Authenticate with wrong password:" << response.value("params").toMap().value("success").toString() << response.value("params").toMap().value("token").toString(); QCOMPARE(response.value("status").toString(), QStringLiteral("success")); QCOMPARE(response.value("params").toMap().value("success").toBool(), false); QVERIFY(response.value("params").toMap().value("token").toByteArray().isEmpty()); + // Connection should terminate + if (disconnectedSpy.count() == 0) disconnectedSpy.wait(); + QCOMPARE(disconnectedSpy.count(), 1); + + // The connection will be locked down for 3 secs + QTest::qWait(3200); + emit m_mockTcpServer->clientConnected(m_clientId); + + spy.clear(); + m_mockTcpServer->injectData(m_clientId, "{\"id\": 0, \"method\": \"JSONRPC.Hello\"}"); + if (spy.count() == 0) { + spy.wait(); + } + QVERIFY(spy.count() == 1); // Now lets authenticate for real (but intentionally use a lowercase email here, should still work) spy.clear(); @@ -465,19 +475,17 @@ void TestJSONRPC::testRevokeToken() QVERIFY(spy.isValid()); QSignalSpy disconnectedSpy(m_mockTcpServer, &MockTcpServer::clientDisconnected); QVERIFY(disconnectedSpy.isValid()); - QSignalSpy connectedSpy(m_mockTcpServer, &MockTcpServer::clientConnected); - QVERIFY(connectedSpy.isValid()); // Now get all the tokens spy.clear(); - m_mockTcpServer->injectData(m_clientId, "{\"id\": 123, \"token\": \"" + m_apiToken + "\", \"method\": \"JSONRPC.Tokens\"}\n"); + qCDebug(dcTests()) << "Getting existing Tokens"; + m_mockTcpServer->injectData(m_clientId, "{\"id\": 123, \"token\": \"" + m_apiToken + "\", \"method\": \"Users.GetTokens\"}\n"); if (spy.count() == 0) { spy.wait(); } QVERIFY(spy.count() == 1); QJsonDocument jsonDoc = QJsonDocument::fromJson(spy.first().at(1).toByteArray()); QVariantMap response = jsonDoc.toVariant().toMap(); - qCDebug(dcTests()) << "Getting existing Tokens" << response.value("status").toString() << response; QCOMPARE(response.value("status").toString(), QStringLiteral("success")); QVariantList tokenList = response.value("params").toMap().value("tokenInfoList").toList(); QCOMPARE(tokenList.count(), 1); @@ -485,6 +493,7 @@ void TestJSONRPC::testRevokeToken() // Authenticate and create a new token spy.clear(); + qCDebug(dcTests()) << "Calling Authenticate with valid credentials" ; m_mockTcpServer->injectData(m_clientId, "{\"id\": 555, \"method\": \"JSONRPC.Authenticate\", \"params\": {\"username\": \"dummy@guh.io\", \"password\": \"DummyPW1!\", \"deviceName\": \"testcase\"}}\n"); if (spy.count() == 0) { spy.wait(); @@ -492,7 +501,6 @@ void TestJSONRPC::testRevokeToken() QVERIFY(spy.count() == 1); jsonDoc = QJsonDocument::fromJson(spy.first().at(1).toByteArray()); response = jsonDoc.toVariant().toMap(); - qCDebug(dcTests()) << "Calling Authenticate with valid credentials:" << response.value("params").toMap().value("success").toString() << response.value("params").toMap().value("token").toString(); QCOMPARE(response.value("status").toString(), QStringLiteral("success")); QCOMPARE(response.value("params").toMap().value("success").toBool(), true); QByteArray newToken = response.value("params").toMap().value("token").toByteArray(); @@ -512,14 +520,14 @@ void TestJSONRPC::testRevokeToken() // Now get all the tokens using the old token spy.clear(); - m_mockTcpServer->injectData(m_clientId, "{\"id\": 123, \"token\": \"" + m_apiToken + "\", \"method\": \"JSONRPC.Tokens\"}\n"); + qCDebug(dcTests()) << "Calling Tokens"; + m_mockTcpServer->injectData(m_clientId, "{\"id\": 123, \"token\": \"" + m_apiToken + "\", \"method\": \"Users.GetTokens\"}\n"); if (spy.count() == 0) { spy.wait(); } QVERIFY(spy.count() == 1); jsonDoc = QJsonDocument::fromJson(spy.first().at(1).toByteArray()); response = jsonDoc.toVariant().toMap(); - qCDebug(dcTests()) << "Calling Tokens" << response.value("status").toString(); QCOMPARE(response.value("status").toString(), QStringLiteral("success")); tokenList = response.value("params").toMap().value("tokenInfoList").toList(); QCOMPARE(tokenList.count(), 2); @@ -535,19 +543,20 @@ void TestJSONRPC::testRevokeToken() // Revoke the new token spy.clear(); - m_mockTcpServer->injectData(m_clientId, "{\"id\": 123, \"token\": \"" + m_apiToken + "\", \"method\": \"JSONRPC.RemoveToken\", \"params\": {\"tokenId\": \"" + newTokenId.toByteArray() + "\"}}\n"); + qCDebug(dcTests()) << "Calling RemoveToken"; + m_mockTcpServer->injectData(m_clientId, "{\"id\": 123, \"token\": \"" + m_apiToken + "\", \"method\": \"Users.RemoveToken\", \"params\": {\"tokenId\": \"" + newTokenId.toByteArray() + "\"}}\n"); if (spy.count() == 0) { spy.wait(); } QVERIFY(spy.count() == 1); jsonDoc = QJsonDocument::fromJson(spy.first().at(1).toByteArray()); response = jsonDoc.toVariant().toMap(); - qCDebug(dcTests()) << "Calling RemoveToken" << response.value("status").toString() << response; QCOMPARE(response.value("status").toString(), QStringLiteral("success")); // Do a call with the now removed token, it should be forbidden spy.clear(); disconnectedSpy.clear(); + qCDebug(dcTests()) << "Calling Version with now removed token"; m_mockTcpServer->injectData(m_clientId, "{\"id\": 555, \"token\": \"" + newToken + "\", \"method\": \"JSONRPC.Version\"}\n"); if (spy.count() == 0) { spy.wait(); @@ -555,18 +564,16 @@ void TestJSONRPC::testRevokeToken() QVERIFY(spy.count() == 1); jsonDoc = QJsonDocument::fromJson(spy.first().at(1).toByteArray()); response = jsonDoc.toVariant().toMap(); - qCDebug(dcTests()) << "Calling Version with valid token:" << response.value("status").toString() << response.value("error").toString(); QCOMPARE(response.value("status").toString(), QStringLiteral("unauthorized")); // And connection should drop if (disconnectedSpy.count() == 0) disconnectedSpy.wait(); QCOMPARE(disconnectedSpy.count(), 1); + QTest::qWait(3200); + // Connect again to not impact subsequent tests... - connectedSpy.clear(); emit m_mockTcpServer->clientConnected(m_clientId); - if (connectedSpy.count() == 0) connectedSpy.wait(); - QCOMPARE(connectedSpy.count(), 1); injectAndWait("JSONRPC.Hello"); } @@ -1176,8 +1183,8 @@ void TestJSONRPC::testPushButtonAuthConnectionDrop() void TestJSONRPC::testInitialSetupWithPushButtonAuth() { - foreach (const QString &user, NymeaCore::instance()->userManager()->users()) { - NymeaCore::instance()->userManager()->removeUser(user); + foreach (const UserInfo &userInfo, NymeaCore::instance()->userManager()->users()) { + NymeaCore::instance()->userManager()->removeUser(userInfo.username()); } NymeaCore::instance()->userManager()->removeUser(""); QVERIFY(NymeaCore::instance()->userManager()->initRequired()); @@ -1247,11 +1254,11 @@ void TestJSONRPC::testInitialSetupWithPushButtonAuth() QCOMPARE(response.toMap().value("params").toMap().value("initialSetupRequired").toBool(), false); - // CreateUser without a token should fail now even though there are 0 users in the DB + // CreateUser without a token should fail now that there is the push button generated user spy.clear(); QSignalSpy disconnectedSpy(m_mockTcpServer, &MockTcpServer::clientDisconnected); - qCDebug(dcTests()) << "Calling CreateUser on uninitialized instance"; - m_mockTcpServer->injectData(m_clientId, "{\"id\": 555, \"method\": \"JSONRPC.CreateUser\", \"params\": {\"username\": \"Dummy@guh.io\", \"password\": \"DummyPW1!\"}}\n"); + qCDebug(dcTests()) << "Calling CreateUser on pushbutton initialized instance"; + m_mockTcpServer->injectData(m_clientId, "{\"id\": 555, \"method\": \"JSONRPC.CreateUser\", \"params\": {\"username\": \"nymea\", \"password\": \"DummyPW1!\"}}\n"); if (spy.count() == 0) { spy.wait(); } @@ -1260,12 +1267,14 @@ void TestJSONRPC::testInitialSetupWithPushButtonAuth() response = jsonDoc.toVariant(); qCDebug(dcTests()) << "Result:" << response.toMap().value("status").toString() << response.toMap().value("error").toString(); QCOMPARE(response.toMap().value("status").toString(), QStringLiteral("unauthorized")); - QCOMPARE(NymeaCore::instance()->userManager()->users().count(), 0); + QCOMPARE(NymeaCore::instance()->userManager()->users().count(), 1); // Connection should drop if (disconnectedSpy.isEmpty()) disconnectedSpy.wait(); QCOMPARE(disconnectedSpy.count(), 1); + QTest::qWait(3200); + // Reconnect to not impact subsequent tests m_mockTcpServer->clientConnected(m_clientId); spy.clear(); diff --git a/tests/auto/usermanager/testusermanager.cpp b/tests/auto/usermanager/testusermanager.cpp index 9e5407d5..5901a222 100644 --- a/tests/auto/usermanager/testusermanager.cpp +++ b/tests/auto/usermanager/testusermanager.cpp @@ -121,9 +121,7 @@ TestUsermanager::TestUsermanager(QObject *parent): NymeaTestBase(parent) void TestUsermanager::initTestCase() { NymeaDBusService::setBusType(QDBusConnection::SessionBus); - NymeaTestBase::initTestCase(); - - QLoggingCategory::setFilterRules("*.debug=false\n" + NymeaTestBase::initTestCase("*.debug=false\n" "Application.debug=true\n" "Tests.debug=true\n" "UserManager.debug=true\n" @@ -134,12 +132,11 @@ void TestUsermanager::initTestCase() void TestUsermanager::init() { UserManager *userManager = NymeaCore::instance()->userManager(); - foreach (const QString &user, userManager->users()) { - qCDebug(dcTests()) << "Removing user" << user; - userManager->removeUser(user); + foreach (const UserInfo &userInfo, userManager->users()) { + qCDebug(dcTests()) << "Removing user" << userInfo.username(); + userManager->removeUser(userInfo.username()); } userManager->removeUser(""); - } void TestUsermanager::loginValidation_data() { @@ -151,12 +148,9 @@ void TestUsermanager::loginValidation_data() { QTest::newRow("foo@bar.co.uk, Bla1234*, NoError") << "foo@bar.co.uk" << "Bla1234*" << UserManager::UserErrorNoError; QTest::newRow("foo@bar.com.au, Bla1234*, NoError") << "foo@bar.com.au" << "Bla1234*" << UserManager::UserErrorNoError; - QTest::newRow("foo, Bla1234*, InvalidUserId") << "foo" << "Bla1234*" << UserManager::UserErrorInvalidUserId; - QTest::newRow("@, Bla1234*, InvalidUserId") << "@" << "Bla1234*" << UserManager::UserErrorInvalidUserId; - QTest::newRow("foo@, Bla1234*, InvalidUserId") << "foo@" << "Bla1234*" << UserManager::UserErrorInvalidUserId; - QTest::newRow("foo@bar, Bla1234*, InvalidUserId") << "foo@bar" << "Bla1234*" << UserManager::UserErrorInvalidUserId; - QTest::newRow("foo@bar., Bla1234*, InvalidUserId") << "foo@bar." << "Bla1234*" << UserManager::UserErrorInvalidUserId; - QTest::newRow("foo@bar.co.uk.au, Bla1234*, InvalidUserId") << "foo@bar.co.uk.au" << "Bla1234*" << UserManager::UserErrorInvalidUserId; + QTest::newRow("n, Bla1234*, InvalidUserId") << "n" << "Bla1234*" << UserManager::UserErrorInvalidUserId; + QTest::newRow("@, Bla1234*, InvalidUserId") << "@" << "Bla1234*" << UserManager::UserErrorInvalidUserId; + QTest::newRow("nymea, Bla1234*, InvalidUserId") << "nymea" << "Bla1234*" << UserManager::UserErrorNoError; QTest::newRow("foo@bar.baz, a, BadPassword") << "foo@bar.baz" << "a" << UserManager::UserErrorBadPassword; QTest::newRow("foo@bar.baz, a1, BadPassword") << "foo@bar.baz" << "a1" << UserManager::UserErrorBadPassword; @@ -184,10 +178,9 @@ void TestUsermanager::loginValidation() QFETCH(UserManager::UserError, expectedError); UserManager *userManager = NymeaCore::instance()->userManager(); - UserManager::UserError error = userManager->createUser(username, password); + UserManager::UserError error = userManager->createUser(username, password, "", "", Types::PermissionScopeAdmin); qDebug() << "Error:" << error << "Expected:" << expectedError; QCOMPARE(error, expectedError); - } void TestUsermanager::createUser() @@ -195,7 +188,7 @@ void TestUsermanager::createUser() QVariantMap params; params.insert("username", "valid@user.test"); params.insert("password", "Bla1234*"); - QVariant response = injectAndWait("Users.CreateUser", params); + QVariant response = injectAndWait("JSONRPC.CreateUser", params); QVERIFY2(response.toMap().value("status").toString() == "success", "Error creating user"); QVERIFY2(response.toMap().value("params").toMap().value("error").toString() == "UserErrorNoError", "Error creating user"); @@ -210,7 +203,7 @@ void TestUsermanager::authenticate() params.insert("username", "valid@user.test"); params.insert("password", "Bla1234*"); params.insert("deviceName", "autotests"); - QVariant response = injectAndWait("Users.Authenticate", params); + QVariant response = injectAndWait("JSONRPC.Authenticate", params); m_apiToken = response.toMap().value("params").toMap().value("token").toByteArray(); @@ -225,7 +218,7 @@ void TestUsermanager::authenticatePushButton() QVariantMap params; params.insert("deviceName", "pbtestdevice"); - QVariant response = injectAndWait("Users.RequestPushButtonAuth", params); + QVariant response = injectAndWait("JSONRPC.RequestPushButtonAuth", params); qCDebug(dcTests()) << "Pushbutton auth response:" << qUtf8Printable(QJsonDocument::fromVariant(response).toJson(QJsonDocument::Indented)); QCOMPARE(response.toMap().value("params").toMap().value("success").toBool(), true); int transactionId = response.toMap().value("params").toMap().value("transactionId").toInt(); @@ -236,7 +229,7 @@ void TestUsermanager::authenticatePushButton() pushButtonAgent.sendButtonPressed(); if (clientSpy.count() == 0) clientSpy.wait(); - QVariantMap rsp = checkNotification(clientSpy, "Users.PushButtonAuthFinished").toMap(); + QVariantMap rsp = checkNotification(clientSpy, "JSONRPC.PushButtonAuthFinished").toMap(); for (int i = 0; i < clientSpy.count(); i++) { qCDebug(dcTests()) << "Notification:" << clientSpy.at(i); @@ -267,7 +260,7 @@ void TestUsermanager::authenticatePushButtonAuthInterrupt() // request push button auth for client 1 (alice) and check for OK reply QVariantMap params; params.insert("deviceName", "alice"); - QVariant response = injectAndWait("Users.RequestPushButtonAuth", params, aliceId); + QVariant response = injectAndWait("JSONRPC.RequestPushButtonAuth", params, aliceId); QCOMPARE(response.toMap().value("params").toMap().value("success").toBool(), true); int transactionId1 = response.toMap().value("params").toMap().value("transactionId").toInt(); @@ -276,7 +269,7 @@ void TestUsermanager::authenticatePushButtonAuthInterrupt() clientSpy.clear(); params.clear(); params.insert("deviceName", "mallory"); - response = injectAndWait("Users.RequestPushButtonAuth", params, malloryId); + response = injectAndWait("JSONRPC.RequestPushButtonAuth", params, malloryId); QCOMPARE(response.toMap().value("params").toMap().value("success").toBool(), true); int transactionId2 = response.toMap().value("params").toMap().value("transactionId").toInt(); @@ -292,7 +285,7 @@ void TestUsermanager::authenticatePushButtonAuthInterrupt() // alice should have received a failed notification. She knows something's wrong. QVariantMap notification = QJsonDocument::fromJson(clientSpy.first().at(1).toByteArray()).toVariant().toMap(); QCOMPARE(clientSpy.first().first().toUuid(), aliceId); - QCOMPARE(notification.value("notification").toString(), QLatin1String("Users.PushButtonAuthFinished")); + QCOMPARE(notification.value("notification").toString(), QLatin1String("JSONRPC.PushButtonAuthFinished")); QCOMPARE(notification.value("params").toMap().value("transactionId").toInt(), transactionId1); QCOMPARE(notification.value("params").toMap().value("success").toBool(), false); @@ -306,7 +299,7 @@ void TestUsermanager::authenticatePushButtonAuthInterrupt() clientSpy.clear(); params.clear(); params.insert("deviceName", "alice"); - response = injectAndWait("Users.RequestPushButtonAuth", params, aliceId); + response = injectAndWait("JSONRPC.RequestPushButtonAuth", params, aliceId); QCOMPARE(response.toMap().value("params").toMap().value("success").toBool(), true); int transactionId3 = response.toMap().value("params").toMap().value("transactionId").toInt(); @@ -321,7 +314,7 @@ void TestUsermanager::authenticatePushButtonAuthInterrupt() // mallory should have received a failed notification. She knows something's wrong. notification = QJsonDocument::fromJson(clientSpy.first().at(1).toByteArray()).toVariant().toMap(); QCOMPARE(clientSpy.first().first().toUuid(), malloryId); - QCOMPARE(notification.value("notification").toString(), QLatin1String("Users.PushButtonAuthFinished")); + QCOMPARE(notification.value("notification").toString(), QLatin1String("JSONRPC.PushButtonAuthFinished")); QCOMPARE(notification.value("params").toMap().value("transactionId").toInt(), transactionId2); QCOMPARE(notification.value("params").toMap().value("success").toBool(), false); @@ -345,7 +338,7 @@ void TestUsermanager::authenticatePushButtonAuthInterrupt() QCOMPARE(clientSpy.count(), 1); notification = QJsonDocument::fromJson(clientSpy.first().at(1).toByteArray()).toVariant().toMap(); QCOMPARE(clientSpy.first().first().toUuid(), aliceId); - QCOMPARE(notification.value("notification").toString(), QLatin1String("Users.PushButtonAuthFinished")); + QCOMPARE(notification.value("notification").toString(), QLatin1String("JSONRPC.PushButtonAuthFinished")); QCOMPARE(notification.value("params").toMap().value("transactionId").toInt(), transactionId3); QCOMPARE(notification.value("params").toMap().value("success").toBool(), true); QVERIFY2(!notification.value("params").toMap().value("token").toByteArray().isEmpty(), "Token is empty while it shouldn't be"); @@ -368,7 +361,7 @@ void TestUsermanager::authenticatePushButtonAuthConnectionDrop() // request push button auth for client 1 (alice) and check for OK reply QVariantMap params; params.insert("deviceName", "alice"); - QVariant response = injectAndWait("Users.RequestPushButtonAuth", params, aliceId); + QVariant response = injectAndWait("JSONRPC.RequestPushButtonAuth", params, aliceId); QCOMPARE(response.toMap().value("params").toMap().value("success").toBool(), true); // Disconnect alice @@ -385,7 +378,7 @@ void TestUsermanager::authenticatePushButtonAuthConnectionDrop() // request push button auth for client 2 (bob) and check for OK reply params.clear(); params.insert("deviceName", "bob"); - response = injectAndWait("Users.RequestPushButtonAuth", params, bobId); + response = injectAndWait("JSONRPC.RequestPushButtonAuth", params, bobId); QCOMPARE(response.toMap().value("params").toMap().value("success").toBool(), true); int transactionId = response.toMap().value("params").toMap().value("transactionId").toInt(); @@ -402,7 +395,7 @@ void TestUsermanager::authenticatePushButtonAuthConnectionDrop() QCOMPARE(clientSpy.count(), 1); QVariantMap notification = QJsonDocument::fromJson(clientSpy.first().at(1).toByteArray()).toVariant().toMap(); QCOMPARE(clientSpy.first().first().toUuid(), bobId); - QCOMPARE(notification.value("notification").toString(), QLatin1String("Users.PushButtonAuthFinished")); + QCOMPARE(notification.value("notification").toString(), QLatin1String("JSONRPC.PushButtonAuthFinished")); QCOMPARE(notification.value("params").toMap().value("transactionId").toInt(), transactionId); QCOMPARE(notification.value("params").toMap().value("success").toBool(), true); QVERIFY2(!notification.value("params").toMap().value("token").toByteArray().isEmpty(), "Token is empty while it shouldn't be"); @@ -469,7 +462,7 @@ void TestUsermanager::authenticateAfterPasswordChangeOK() params.insert("username", "valid@user.test"); params.insert("password", "Blubb123"); // New password, should be ok params.insert("deviceName", "autotests"); - QVariant response = injectAndWait("Users.Authenticate", params); + QVariant response = injectAndWait("JSONRPC.Authenticate", params); m_apiToken = response.toMap().value("params").toMap().value("token").toByteArray(); QVERIFY2(!m_apiToken.isEmpty(), "Token should not be empty"); @@ -481,16 +474,27 @@ void TestUsermanager::authenticateAfterPasswordChangeFail() { changePassword(); + QSignalSpy disconnectedSpy(m_mockTcpServer, &MockTcpServer::clientDisconnected); + QVariantMap params; params.insert("username", "valid@user.test"); params.insert("password", "Bla1234*"); // Original password, should not be ok params.insert("deviceName", "autotests"); - QVariant response = injectAndWait("Users.Authenticate", params); + QVariant response = injectAndWait("JSONRPC.Authenticate", params); m_apiToken = response.toMap().value("params").toMap().value("token").toByteArray(); QVERIFY2(m_apiToken.isEmpty(), "Token should be empty"); QVERIFY2(response.toMap().value("status").toString() == "success", "Error authenticating"); QCOMPARE(response.toMap().value("params").toMap().value("success").toString(), QString("false")); + + // Connection should drop + if (disconnectedSpy.count() == 0) disconnectedSpy.wait(); + QVERIFY2(disconnectedSpy.count() == 1, "Connection should have dropped"); + + QTest::qWait(3200); + m_mockTcpServer->clientConnected(m_clientId); + injectAndWait("JSONRPC.Hello"); + } void TestUsermanager::getUserInfo() @@ -522,8 +526,9 @@ void TestUsermanager::unauthenticatedCallAfterTokenRemove() } QVERIFY2(spy.count() == 1, "Connection should be terminated!"); - // need to restart as our connection dies - restartServer(); + QTest::qWait(3200); + m_mockTcpServer->clientConnected(m_clientId); + injectAndWait("JSONRPC.Hello"); } #include "testusermanager.moc" diff --git a/tests/testlib/nymeatestbase.cpp b/tests/testlib/nymeatestbase.cpp index 32ea0847..69d70c3c 100644 --- a/tests/testlib/nymeatestbase.cpp +++ b/tests/testlib/nymeatestbase.cpp @@ -88,9 +88,9 @@ void NymeaTestBase::initTestCase(const QString &loggingRules) qCDebug(dcTests()) << "Nymea core instance initialized. Creating dummy user."; // Yes, we're intentionally mixing upper/lower case email here... username should not be case sensitive - NymeaCore::instance()->userManager()->removeUser("dummy@guh.io"); - NymeaCore::instance()->userManager()->createUser("dummy@guh.io", "DummyPW1!"); - m_apiToken = NymeaCore::instance()->userManager()->authenticate("Dummy@guh.io", "DummyPW1!", "testcase"); + NymeaCore::instance()->userManager()->removeUser("dummy"); + NymeaCore::instance()->userManager()->createUser("dummy", "DummyPW1!", "dummy@guh.io", "Dummy", Types::PermissionScopeAdmin); + m_apiToken = NymeaCore::instance()->userManager()->authenticate("Dummy", "DummyPW1!", "testcase"); if (MockTcpServer::servers().isEmpty()) { qCWarning(dcTests) << "no mock tcp server found";