diff --git a/api/classes/lifepreserver.rst b/api/classes/lifepreserver.rst index a601378..6963b2c 100644 --- a/api/classes/lifepreserver.rst +++ b/api/classes/lifepreserver.rst @@ -21,12 +21,15 @@ Every lifepreserver class request contains the following parameters: | | | | +---------------------------------+---------------+----------------------------------------------------------------------------------------------------------------------+ | action | | supported actions include "listcron", "cronsnap", "cronscrub", "listsnap", "revertsnap", "removesnap", | -| | | "addreplication", "settings", and "savesettings" | +| | | "addreplication", "removereplication", "listreplication", "runreplication", "initreplication", "settings", and | +| | | "savesettings" | | | | | +---------------------------------+---------------+----------------------------------------------------------------------------------------------------------------------+ The rest of this section provides examples of the available *actions* for each type of request, along with their responses. +.. index:: listcron, Life Preserver + .. _List Schedules: List Schedules @@ -93,6 +96,8 @@ the time that snapshots are taken. If scrubs have been configured on that ZFS po "namespace": "sysadm" } +.. index:: cronsnap, Life Preserver + .. _Create a Snapshot Schedule: Create a Snapshot Schedule @@ -175,6 +180,8 @@ The "cronsnap" action is used to create snapshot schedules for Life Preserver. T "namespace": "sysadm" } +.. index:: cronscrub, Life Preserver + .. _Create a Scrub Schedule: Create a Scrub Schedule @@ -249,6 +256,8 @@ The "cronscrub" action is used to schedule a ZFS scrub. This action supports the "namespace": "sysadm" } +.. index:: listsnap, Life Preserver + .. _List Snapshots: List Snapshots @@ -329,6 +338,8 @@ The "listsnap" action retrieves the list of saved snapshots. "namespace": "sysadm" } +.. index:: revertsnap, Life Preserver + .. _Revert a Snapshot: Revert a Snapshot @@ -394,6 +405,8 @@ The "revertsnap" action is used to rollback the contents of the specified datase "namespace": "sysadm" } +.. index:: removesnap, Life Preserver + .. _Remove a Snapshot: Remove a Snapshot @@ -456,6 +469,8 @@ The "removesnap" action is used to remove a ZFS snapshot from the specified data "namespace": "sysadm" } +.. index:: addreplication, Life Preserver + .. _Add Replication: Add Replication @@ -564,6 +579,301 @@ The "addreplication" action is used to create a replication task in Life Preserv "namespace": "sysadm" } +.. index:: removereplication, Life Preserver + +.. _Remove Replication: + +Remove Replication +=============== + +The "removereplication" action is used to delete an existing replication task. Note that this action only deletes the task--it does not remove any already replicated data from the +remote system. + +This action supports the following parameters: + ++---------------------------------+----------------------------------------------------------------------------------------------------------------------+ +| **Parameter** | **Description** | +| | | ++=================================+======================================================================================================================+ +| host | remote hostname or IP address | +| | | ++---------------------------------+----------------------------------------------------------------------------------------------------------------------+ +| dataset | name of local dataset to remove from replication | +| | | ++---------------------------------+----------------------------------------------------------------------------------------------------------------------+ + +**REST Request** + +.. code-block:: json + + PUT /sysadm/lifepreserver + { + "dataset" : "tank", + "host" : "192.168.0.10", + "action" : "removereplication" + } + +**REST Response** + +.. code-block:: json + + { + "args": { + "removereplication": { + "dataset": "tank", + "host": "192.168.0.10" + } + } + } + +**WebSocket Request** + +.. code-block:: json + + { + "id" : "fooid", + "args" : { + "action" : "removereplication", + "dataset" : "tank", + "host" : "192.168.0.10" + }, + "name" : "lifepreserver", + "namespace" : "sysadm" + } + +**WebSocket Response** + +.. code-block:: json + + { + "args": { + "removereplication": { + "dataset": "tank", + "host": "192.168.0.10" + } + }, + "id": "fooid", + "name": "response", + "namespace": "sysadm" + } + +.. index:: listreplication, Life Preserver + +.. _List Replications: + +List Replications +================= + +The "listreplication" action is used to retrieve the settings of configured replication tasks. For each task, the response includes the name of the local ZFS pool or dataset to replicate, +the IP address and listening port number of the remote system to replicate to, when the replication occurs (see the "frequency" description in :ref:`Add Replication`), the name of the +dataset on the remote system to store the replicated data ("rdset"), and the name of the replication user account. + +**REST Request** + +.. code-block:: json + + PUT /sysadm/lifepreserver + { + "action" : "listreplication" + } + +**REST Response** + +.. code-block:: json + + { + "args": { + "listreplication": { + "tank1->192.168.0.9": { + "dataset": "tank1", + "frequency": "22", + "host": "192.168.0.9", + "port": "22", + "rdset": "tank/backups", + "user": "backups" + } + } + } + } + +**WebSocket Request** + +.. code-block:: json + + { + "namespace" : "sysadm", + "args" : { + "action" : "listreplication" + }, + "id" : "fooid", + "name" : "lifepreserver" + } + +**WebSocket Response** + +.. code-block:: json + + { + "args": { + "listreplication": { + "tank1->192.168.0.9": { + "dataset": "tank1", + "frequency": "22", + "host": "192.168.0.9", + "port": "22", + "rdset": "tank/backups", + "user": "backups" + } + } + }, + "id": "fooid", + "name": "response", + "namespace": "sysadm" + } + +.. index:: runreplication, Life Preserver + +.. _Start Replication: + +Start Replication +================= + +The "runreplication" action can be used to manually replicate the specified dataset to the specified remote server. + +**REST Request** + +.. code-block:: json + + PUT /sysadm/lifepreserver + { + "host" : "10.0.10.100", + "dataset" : "mypool", + "action" : "runreplication" + } + +**REST Response** + +.. code-block:: json + + { + "args": { + "runreplication": { + "dataset": "mypool", + "host": "10.0.10.100" + } + } + } + +**WebSocket Request** + +.. code-block:: json + + { + "id" : "fooid", + "name" : "lifepreserver", + "args" : { + "host" : "10.0.10.100", + "dataset" : "mypool", + "action" : "runreplication" + }, + "namespace" : "sysadm" + } + +**WebSocket Response** + +.. code-block:: json + + { + "args": { + "runreplication": { + "dataset": "mypool", + "host": "10.0.10.100" + } + }, + "id": "fooid", + "name": "response", + "namespace": "sysadm" + } + +.. index:: initreplication, Life Preserver + +.. _Initialize Replication: + +Initialize Replication +====================== + +The "initreplication" action can be used to clear the replication data on the remote server. This is useful if a replication becomes stuck. After running this action, issue a +"runreplication" action to start a new replication. + +The "initreplication" action supports the following parameters: + ++---------------------------------+----------------------------------------------------------------------------------------------------------------------+ +| **Parameter** | **Description** | +| | | ++=================================+======================================================================================================================+ +| host | remote hostname or IP address | +| | | ++---------------------------------+----------------------------------------------------------------------------------------------------------------------+ +| dataset | name of local dataset or pool being replicated | +| | | ++---------------------------------+----------------------------------------------------------------------------------------------------------------------+ + +**REST Request** + +.. code-block:: json + + PUT /sysadm/lifepreserver + { + "dataset" : "tank1", + "host" : "192.168.0.9", + "action" : "initreplication" + } + +**REST Response** + +.. code-block:: json + + { + "args": { + "initreplication": { + "dataset": "tank1", + "host": "192.168.0.9" + } + } + } + +**WebSocket Request** + +.. code-block:: json + + { + "id" : "fooid", + "args" : { + "host" : "192.168.0.9", + "dataset" : "tank1", + "action" : "initreplication" + }, + "namespace" : "sysadm", + "name" : "lifepreserver" + } + +**WebSocket Response** + +.. code-block:: json + + { + "args": { + "initreplication": { + "dataset": "tank1", + "host": "192.168.0.9" + } + }, + "id": "fooid", + "name": "response", + "namespace": "sysadm" + } + +.. index:: settings, Life Preserver .. _View Settings: @@ -630,6 +940,8 @@ Run :command:`lpreserver help set` for more information about each available set "namespace": "sysadm" } +.. index:: savesettings, Life Preserver + .. _Save Settings: Save Settings diff --git a/api/connection.rst b/api/connection.rst index 22894c6..53bd018 100644 --- a/api/connection.rst +++ b/api/connection.rst @@ -1,21 +1,17 @@ -.. _Connection: - -Connection -========== - -Some intro text here... - .. _Getting Started: Getting Started ---------------- +*************** + +Some intro text here... + Add some links to docs on websockets and json.... .. _Authentication: Authentication --------------- +============== Describe how to authenticate to websockets via Local / Remote, local connections do not need username / password... @@ -44,7 +40,7 @@ request contains the following parameters: Several methods are available for authentication. Here is an example of a login using a username and password: -**Request** +**WebSocket Request** .. code-block:: json @@ -60,7 +56,7 @@ Several methods are available for authentication. Here is an example of a login Here is an example of using token authentication, where the token is invalidated after 5 minutes of inactivity: -**Request** +**WebSocket Request** .. code-block:: json @@ -75,7 +71,7 @@ Here is an example of using token authentication, where the token is invalidated A successful authentication will provide a reply similar to this: -**Reply** +**WebSocket Reply** .. code-block:: json @@ -95,7 +91,7 @@ A successful authentication will provide a reply similar to this: An invalid authentication, or a system request after the user session has timed out due to inactivity, looks like this: -**Reply** +**WebSocket Reply** .. code-block:: json @@ -111,7 +107,7 @@ An invalid authentication, or a system request after the user session has timed To clear a pre-saved authentication token, such as signing out, use this request: -**Request** +**WebSocket Request** .. code-block:: json @@ -121,3 +117,79 @@ To clear a pre-saved authentication token, such as signing out, use this request "id" : "sampleID", "args" : "junk argument" } + +.. _Server Subsystems: + +Server Subsystems +================= + +An RPC query can be issued to probe all the known subsystems and return which ones are currently available and what level of read and write access the user has. +A query contains the following parameters: + ++---------------------------------+---------------+----------------------------------------------------------------------------------------------------------------------+ +| **Parameter** | **Value** | **Description** | +| | | | ++=================================+===============+======================================================================================================================+ +| id | | any unique value for the request; examples include a hash, checksum, or uuid | +| | | | ++---------------------------------+---------------+----------------------------------------------------------------------------------------------------------------------+ +| name | query | | +| | | | ++---------------------------------+---------------+----------------------------------------------------------------------------------------------------------------------+ +| namespace | rpc | | +| | | | ++---------------------------------+---------------+----------------------------------------------------------------------------------------------------------------------+ +| args | | can be any data | +| | | | ++---------------------------------+---------------+----------------------------------------------------------------------------------------------------------------------+ + +**REST Request** + +.. code-block:: json + + PUT /rpc/query + { + "junk" : "junk" + } + +**REST Response** + +.. code-block:: json + + { + "args": { + "rpc/dispatcher": "read/write", + "rpc/syscache": "read", + "sysadm/lifepreserver": "read/write", + "sysadm/network": "read/write" + } + } + +**WebSocket Request** + +.. code-block:: json + + { + "id" : "fooid", + "name" : "query", + "namespace" : "rpc", + "args" : { + "junk" : "junk" + } + } + +**WebSocket Response** + +.. code-block:: json + + { + "args": { + "rpc/dispatcher": "read/write", + "rpc/syscache": "read", + "sysadm/lifepreserver": "read/write", + "sysadm/network": "read/write" + }, + "id": "fooid", + "name": "response", + "namespace": "rpc" + } \ No newline at end of file diff --git a/src/library/library.pro b/src/library/library.pro index 769fca9..c08f0aa 100644 --- a/src/library/library.pro +++ b/src/library/library.pro @@ -14,17 +14,19 @@ HEADERS += sysadm-global.h \ sysadm-general.h \ sysadm-lifepreserver.h \ sysadm-network.h \ - sysadm-firewall.h + sysadm-firewall.h \ + sysadm-usermanager.h SOURCES += sysadm-general.cpp \ sysadm-lifepreserver.cpp \ sysadm-network.cpp \ NetDevice.cpp \ - sysadm-firewall.cpp + sysadm-firewall.cpp \ + sysadm-usermanager.cpp include.path=/usr/local/include/ include.files=sysadm-*.h -INSTALLS += target include +INSTALLS += target include QMAKE_LIBDIR = /usr/local/lib/qt5 /usr/local/lib diff --git a/src/library/sysadm-lifepreserver.cpp b/src/library/sysadm-lifepreserver.cpp index ab4c73a..d5ca03e 100644 --- a/src/library/sysadm-lifepreserver.cpp +++ b/src/library/sysadm-lifepreserver.cpp @@ -83,6 +83,46 @@ QJsonObject LifePreserver::addReplication(QJsonObject jsin) { return values; } +// Re-init the LP replication target +QJsonObject LifePreserver::initReplication(QJsonObject jsin) { + QJsonObject retObject; + QString dset, rhost; + + QStringList keys = jsin.keys(); + if(! keys.contains("dataset") || ! keys.contains("host")){ + retObject.insert("error", "Missing dataset or host key"); + return retObject; + } + + // Check which pool we are looking at + dset = jsin.value("dataset").toString(); + rhost = jsin.value("host").toString(); + + // Make sure we have the pool key + if ( dset.isEmpty() || rhost.isEmpty()) { + retObject.insert("error", "Missing dataset or host key"); + return retObject; + } + + // TODO - This command can take a LONG TIME. Find a way to queue / background it and return an event + // via websockets later, or block here and return when finished if this is REST + QStringList output = General::RunCommand("lpreserver replicate init " + dset + " " + rhost).split("\n"); + + // Check for any errors + for ( int i = 0; i < output.size(); i++) + { + if ( output.at(i).indexOf("ERROR:") != -1 ) { + retObject.insert("error", output.at(i)); + return retObject; + } + } + + QJsonObject values; + values.insert("dataset", dset); + values.insert("host", rhost); + return values; +} + // Build list of scheduled cron snapshot jobs QJsonObject LifePreserver::listCron() { QJsonObject retObject; @@ -146,6 +186,54 @@ QJsonObject LifePreserver::listCron() { return retObject; } +// Return a list of replication targets +QJsonObject LifePreserver::listReplication() { + QJsonObject retObject; + + QStringList output = General::RunCommand("lpreserver replicate list").split("\n"); + QStringList setitems; + QString tmpkey; + QRegExp sep("\\s+"); + + // Parse the output + bool inSection = false; + for ( int i = 0; i < output.size(); i++) + { + if ( output.at(i).indexOf("-----------------") != -1 ) { + inSection = true; + continue; + } + + if (!inSection) + continue; + + if ( output.at(i).isEmpty() || output.at(i).indexOf("-----------------") != -1 ) + break; + + // Breakdown the settings + QJsonObject values; + tmpkey = ""; + QString dset, rdset, user, host, port, parseline, time; + dset = output.at(i).section(sep, 0, 0).simplified(); + parseline = output.at(i).section(sep, 2, 2).simplified(); + user = parseline.section("@", 0, 0); + host = parseline.section("@", 1, 1).section("[", 0, 0); + port = parseline.section("@", 1, 1).section("[", 1, 1).section("]", 0, 0); + rdset = parseline.section(":", 1, 1); + time = output.at(i).section(sep, 4, 4).simplified(); + + values.insert("dataset", dset); + values.insert("user", user); + values.insert("port", port); + values.insert("host", host); + values.insert("rdset", rdset); + values.insert("frequency", time); + retObject.insert(dset + "->" + host, values); + } + + return retObject; +} + // Return a list of snapshots on a particular pool / dataset QJsonObject LifePreserver::listSnap(QJsonObject jsin) { QJsonObject retObject; @@ -199,6 +287,47 @@ QJsonObject LifePreserver::listSnap(QJsonObject jsin) { return retObject; } +// Remove a replication task +QJsonObject LifePreserver::removeReplication(QJsonObject jsin) { + QJsonObject retObject; + QString dataset, host; + + QStringList keys = jsin.keys(); + if(! keys.contains("dataset") || ! keys.contains("host")){ + retObject.insert("error", "Requires dataset and host keys"); + return retObject; + } + + // Get the dataset / host + dataset = jsin.value("dataset").toString(); + host = jsin.value("host").toString(); + + // Make sure we have the dataset / host key(s) + if ( dataset.isEmpty() || host.isEmpty() ) { + retObject.insert("error", "Empty dataset or host keys "); + return retObject; + } + + QStringList output; + output = General::RunCommand("lpreserver replicate remove " + dataset + " " + host).split("\n"); + + // Check for any errors + for ( int i = 0; i < output.size(); i++) + { + if ( output.at(i).indexOf("ERROR:") != -1 ) { + retObject.insert("error", output.at(i)); + return retObject; + } + } + + // Got to the end, return the good json + QJsonObject values; + values.insert("dataset", dataset); + values.insert("host", host); + + return values; +} + // Remove a snapshot QJsonObject LifePreserver::removeSnapshot(QJsonObject jsin) { QJsonObject retObject; @@ -240,6 +369,47 @@ QJsonObject LifePreserver::removeSnapshot(QJsonObject jsin) { return values; } +// Run a replication task +QJsonObject LifePreserver::runReplication(QJsonObject jsin) { + QJsonObject retObject; + QString dataset, host; + + QStringList keys = jsin.keys(); + if(! keys.contains("dataset") || ! keys.contains("host")){ + retObject.insert("error", "Requires dataset and host keys"); + return retObject; + } + + // Get the dataset / host + dataset = jsin.value("dataset").toString(); + host = jsin.value("host").toString(); + + // Make sure we have the dataset / host key(s) + if ( dataset.isEmpty() || host.isEmpty() ) { + retObject.insert("error", "Empty dataset or host keys "); + return retObject; + } + + QStringList output; + output = General::RunCommand("lpreserver replicate run " + dataset + " " + host).split("\n"); + + // Check for any errors + for ( int i = 0; i < output.size(); i++) + { + if ( output.at(i).indexOf("ERROR:") != -1 ) { + retObject.insert("error", output.at(i)); + return retObject; + } + } + + // Got to the end, return the good json + QJsonObject values; + values.insert("dataset", dataset); + values.insert("host", host); + + return values; +} + // Revert to a snapshot QJsonObject LifePreserver::revertSnapshot(QJsonObject jsin) { QJsonObject retObject; diff --git a/src/library/sysadm-lifepreserver.h b/src/library/sysadm-lifepreserver.h index 3954d4f..4f9c0ba 100644 --- a/src/library/sysadm-lifepreserver.h +++ b/src/library/sysadm-lifepreserver.h @@ -15,10 +15,14 @@ namespace sysadm{ class LifePreserver{ public: static QJsonObject addReplication(QJsonObject jsin); + static QJsonObject initReplication(QJsonObject jsin); static QJsonObject listCron(); + static QJsonObject listReplication(); static QJsonObject listSnap(QJsonObject jsin); + static QJsonObject removeReplication(QJsonObject jsin); static QJsonObject removeSnapshot(QJsonObject jsin); static QJsonObject revertSnapshot(QJsonObject jsin); + static QJsonObject runReplication(QJsonObject jsin); static QJsonObject saveSettings(QJsonObject jsin); static QJsonObject scheduleSnapshot(QJsonObject jsin); static QJsonObject scheduleScrub(QJsonObject jsin); diff --git a/src/library/sysadm-usermanager.cpp b/src/library/sysadm-usermanager.cpp new file mode 100644 index 0000000..71f30ce --- /dev/null +++ b/src/library/sysadm-usermanager.cpp @@ -0,0 +1,546 @@ +//=========================================== +// PC-BSD source code +// Copyright (c) 2015, PC-BSD Software/iXsystems +// Available under the 3-clause BSD license +// See the LICENSE file for full details +//=========================================== +#include "sysadm-usermanager.h" +#include "sysadm-general.h" +using namespace sysadm; + +UserManager::UserManager(QString chroot) +{ + this->chroot = chroot; + loadUsers(); + loadGroups(); + loadShells(); +} + +void UserManager::NewUser(QString fullName, QString userName, QString password, QString home, QString shell, int uid, int gid, bool encrypt) +{ + User user; + user.UserName = userName; + user.FullName = fullName; + user.HomeFolder = (home.isEmpty())?"/usr/home/"+userName : home; + user.Shell = shell; + + //Add User + qDebug() << "Adding user " << userName; + // Create the zfs dataset associated with the home directory + if ( chroot.isEmpty() ) + { + QStringList args; + args.append(user.HomeFolder); + General::RunCommand("/usr/local/share/pcbsd/scripts/mkzfsdir.sh",args); + } + + QStringList args; + if ( ! chroot.isEmpty() ) //if chroot is not empty the command starts with chroot instead of pw + args << chroot << "pw"; //and thus we have to add it as an argument + args << "useradd"; //create a user + args << userName; //with this userName + args << "-c"; //sets the comment field + args << "\""+ fullName+"\""; //with the full name of the user + args << "-m"; //create the user's home directory + if(!home.isEmpty()) + { + args << "-d"; //set the home directory to + args << home; //this + } + args << "-s"; //set the user's shell + args << shell; //to this + if(gid != -1) + { + args << "-g"; //set the group id to + args << QString::number(gid); //this + } + if(uid != -1) + { + args << "-u"; //set the user id to + args << QString::number(uid); //this + } + args << "-G"; //additionally add the user to + args << "operator"; //the operator's group + + if ( ! chroot.isEmpty() ) //if we're operating with a chroot call + General::RunCommand("chroot", args); + else //otherwise + General::RunCommand("pw", args); + + ChangeUserPassword(user,password); + + //enable flash for the user + if ( chroot.isEmpty() ) { //if we're not in a chroot + qDebug() << "Enabling Flash Plugin for " << userName; + args.clear(); + args << userName; //run command as this user + args << "-c"; //with the command + args << "\"flashpluginctl on\""; //turn on flashpluginctl + General::RunCommand("su",args); + } + + //if we're going to PersonaCrypt the home directory + if(encrypt) + initPCDevice(user,home,password); + + //reloads the groups and users so that the internal model is consistent + loadUsers(); + loadGroups(); +} + +void UserManager::DeleteUser(User user) +{ + //Delete User + qDebug() << "Deleting user " << user.UserName; + + //remove the dataset associated with the home folder + QStringList args; + args << user.HomeFolder; + General::RunCommand("/usr/local/share/pcbsd/scripts/rmzfsdir.sh",args); + + //delete the user and their home directory + args.clear(); + if ( ! chroot.isEmpty() ) //if we're in a chroot we need to use chroot before pw + args << chroot << "pw"; + args << "userdel"; //delete a user + args << user.UserName; //this user + args << "-r"; //remove the contents of the user's home directory + if ( ! chroot.isEmpty() ) + General::RunCommand("chroot", args); + else + General::RunCommand("pw", args); + + loadUsers(); + loadGroups(); +} + +const QVector UserManager::GetUsers() +{ + return users; +} + +const User UserManager::GetUser(int id) +{ + for(User user: users) + { + if(user.ID == id) + return user; + } + return User(); +} + +const User UserManager::GetUser(QString userName) +{ + for(User user: users) + { + if(user.UserName == userName) + return user; + } + return User(); +} + +void UserManager::ChangeUserPassword(User user, QString newPassword) +{ + //Don't Change the password of a user with an encrypted Home directory + if( !QFile::exists("/var/db/personacrypt/"+user.UserName+".key") ){ return; } + + //Create a temporary file to store the password in + QTemporaryFile nfile("/tmp/.XXXXXXXX"); + if ( nfile.open() ) + { + QTextStream stream( &nfile ); + stream << newPassword; + nfile.close(); + } + + //set the user password + QStringList args; + args.append(nfile.fileName()); //the temp file holding the password + args.append("|"); //which we're going to pipe to the stdin of + if ( ! chroot.isEmpty() ) //if we're in a chroot + { + args << "chroot"; //a chroot + args << chroot; //located here + } + args << "pw"; //change users + args << "usermod"; //where we're going to modify a user + args << user.UserName;//this user + args << "-h"; //set the user's password + args << "0"; //using stdin + General::RunCommand("cat",args); + + //remove the temp file holding the password + nfile.remove(); + +} + +void UserManager::ChangeUserShell(User user, QString shell) +{ + if(shells.contains(shell)) + { + qDebug("Shell found"); + QStringList args; + args << "usermod"; // modify the user + args << "-n"; //specify a user name + args << user.UserName; //for this user + args << "-s"; //set the shell to + args << shell; //this shell + General::RunCommand("pw",args); + } + else + qDebug("Shell not found"); + + loadUsers(); +} + +void UserManager::ChangeUserFullName(User user, QString newName) +{ + QStringList args; + args << "usermod"; //modify the user + args << user.UserName; //for this user + args << "-c"; //change the gecos field to + args << newName; //this name + General::RunCommand("pw",args); + loadUsers(); +} + +void UserManager::AddUserToGroup(User user, Group group) +{ + QStringList args; + args << "groupmod"; //modify a group + args << "-n"; //modify for a group + args << group.Name;//this group + args << "-m";//by adding a member + args << user.UserName; //this user + General::RunCommand("pw",args); + + loadGroups(); +} + +void UserManager::RemoveUserFromGroup(User user, Group group) +{ + QStringList args; + args << "groupmod"; //modify a group + args << "-n"; //modify for a group + args << group.Name; //this group + args << "-d"; //by removing a user + args << user.UserName ; //this user + General::RunCommand("pw", args); + + loadGroups(); +} + +void UserManager::NewGroup(QString name, QStringList members) +{ + QStringList args; + qDebug() << "Adding group " << name; + if ( ! chroot.isEmpty() ) //if we're in a chroot need to add chroot before pw + args << chroot << "pw"; + args << "groupadd"; //create a new group + args << name; // with this name + args << "-M"; //with this list of users + args << members.join(","); //these guys + if ( ! chroot.isEmpty() ) //if we're in a chroot + General::RunCommand("chroot", args); + else + General::RunCommand("pw", args); + + loadGroups(); +} + +void UserManager::DeleteGroup(Group group) +{ + QStringList args; + qDebug() << "Deleting group " << group.Name; + if ( ! chroot.isEmpty() ) //if we're in a chroot need to add chroot before pw + args << chroot << "pw"; + args << "groupdel"; //delete a group + args << group.Name; //of this name + if ( ! chroot.isEmpty() ) //if we're in a chroot + General::RunCommand("chroot", args); + else + General::RunCommand("pw", args); + + loadGroups(); +} + +const QVector UserManager::GetGroups() +{ + return groups; +} + +const Group UserManager::getGroup(int id) +{ + for(Group group : groups) + { + if(group.ID == id) + return group; + } + return Group(); +} + +const Group UserManager::getGroup(QString name) +{ + for(Group group : groups) + { + if(group.Name == name) + return group; + } + return Group(); +} + +const QStringList UserManager::GetShells() +{ + return shells; +} + +void UserManager::loadUsers() +{ + users.clear(); + QStringList userStrings; + QStringList args; + if(!chroot.isEmpty()) + { + args << chroot; + args << "pw"; + } + args << "usershow"; + args << "-a"; + if(chroot.isEmpty()) + userStrings = General::RunCommand("pw",args).split("\n"); + else + userStrings = General::RunCommand("chroot",args).split("\n"); + + //remove the empty string at the end + userStrings.removeLast(); + + for(QString line : userStrings) + { + User user; + user.UserName = line.section(":",0,0); + user.ID = line.section(":",2,2).toInt(); + user.GroupID = line.section(":",3,3).toInt(); + user.HomeFolder = line.section(":",8,8); + user.Shell = line.section(":",9,9); + user.FullName = line.section(":",7,7); + + users.append(user); + } +} + +void UserManager::loadGroups() +{ + groups.clear(); + QStringList groupStrings; + QStringList args; + if(!chroot.isEmpty()) + { + args << chroot; + args << "pw"; + } + args << "groupshow"; + args << "-a"; + if(chroot.isEmpty()) + groupStrings = General::RunCommand("pw",args).split("\n"); + else + groupStrings = General::RunCommand("chroot",args).split("\n"); + + //remove the empty string at the end + groupStrings.removeLast(); + + for(QString line : groupStrings) + { + Group group; + group.Name = line.section(":",0,0); + group.ID = line.section(":",2,2).toInt(); + QString memberString = line.section(":",3,3); + group.Members = memberString.split(","); + + groups.append(group); + } +} + +void UserManager::loadShells() +{ + shells.clear(); + QFile shellFile(chroot + "/etc/shells"); + if ( shellFile.open(QIODevice::ReadOnly) ) { + QTextStream stream(&shellFile); + stream.setCodec("UTF-8"); + + QString line; + + while ( !stream.atEnd() ) { + line = stream.readLine(); + + if ( !line.startsWith("#") && !line.isEmpty() ) { //Make sure it isn't a comment or blank + shells.append(line); + } + } + } else { + //Unable to open file error + qWarning("Error! Unable to open /etc/shells"); + } + + // Add /sbin/nologin as well + shells.append("/sbin/nologin"); +} + + +void UserManager::importPCKey(User user, QString filename){ + //Double check that the key does not exist (button should have been hidden earlier if invalid) + if( QFile::exists("/var/db/personacrypt/"+user.UserName+".key") ){ return; } + + //if the location is empty cancel + if(filename.isEmpty()){ return; } + + //Now run the import command + QStringList args; + args << "import"; + args << "\""+filename + "\""; + if( 0 == General::RunCommand("personacrypt",args) ){ + //Success + qDebug("The key file was imported successfully."); + }else{ + //Failure + qWarning("The key file could not be imported. Please ensure you are using a valid file."); + } +} + +void UserManager::exportPCKey(User user, QString filename){ + //Double check that the key exists (button should have been hidden earlier if invalid) + if( !QFile::exists("/var/db/personacrypt/"+user.UserName+".key") ){ return; } + + if(filename.isEmpty()){ return; } //cancelled + if( !filename.endsWith(".key") ){ filename.append(".key"); } + //Now get/save the key file + QStringList args; + args << "export"; + args << "\"" + user.UserName + "\""; + QString key = General::RunCommand("personacrypt",args); + + QFile file(filename); + if( !file.open(QIODevice::WriteOnly | QIODevice::Truncate) ){ + //Could not open output file + qWarning() <<"Output file could not be opened:\n\n" << filename; + return; + } + QTextStream out(&file); + out << key; + file.close(); + qDebug() << "The PersonaCrypt key has been saved successfully: \n\n" << filename; +} + +void UserManager::disablePCKey(User user){ +//Double check that the key exists (button should have been hidden earlier if invalid) + if( !QFile::exists("/var/db/personacrypt/"+user.UserName+".key") ){ return; } + + if( QFile::remove("/var/db/personacrypt/"+user.UserName+".key") ){ + //Success + qDebug("The PersonaCrypt user key has been disabled." ); + }else{ + //Failure (should almost never happen, since this utility runs as root and just needs to delete a file) + qDebug("The PersonaCrypt user key could not be removed. Do you have the proper permissions?" ); + } +} + +void UserManager::disableAndCopyPCKey(User user, QString password){ + QStringList args; + args << "list"; + QStringList cusers = General::RunCommand("personacrypt",args).split("\n"); + bool available = false; + for(int i=0; i