Merge branch 'master' of github.com:pcbsd/sysadm

This commit is contained in:
Ken Moore
2016-01-15 15:36:58 -05:00
9 changed files with 1393 additions and 36 deletions

View File

@@ -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

View File

@@ -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"
}

View File

@@ -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

View File

@@ -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;

View File

@@ -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);

View File

@@ -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<User> 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<Group> 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<cusers.length(); i++){
if(cusers[i].section(" on ",0,0) == user.UserName){ available = true; break; } //disk is connected to the system
}
if(!available){
//Warn the user that they need to plug in their USB stick first
qWarning("PersonaCrypt Device Not Found, Please ensure that your PersonaCrypt device is connected to the system and try again.");
return;
}
if(password.isEmpty()){ return; } //cancelled
//Save the password to a temporary file
QTemporaryFile tmpfile("/tmp/.XXXXXXXXXXXXXXXXXXXX");
if( !tmpfile.open() ){ return; } //could not create a temporary file (extremely rare)
QTextStream out(&tmpfile);
out << password;
tmpfile.close();
//Now run the PersonaCrypt command
args.clear();
args << "remove";
args << "\"" + user.UserName + "\"";
args << "\"" + tmpfile.fileName() + "\"";
if(0 == General::RunCommand("personacrypt",args) ){
//Success
qDebug("Success; The data for this user has been merged onto the system and the system key has been disabled");
}else{
//Failure
qWarning("Failure; The PersonaCrypt user data could not be merged onto the system. Invalid Password?" );
}
}
void UserManager::initPCDevice(User user, QString home, QString password)
{
//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; }
//Prompt for the user to select a device
QStringList args;
args << "list";
args << "-r";
QStringList devlist = General::RunCommand("personacrypt",args).split("\n");
for(int i=0; i<devlist.length(); i++){
//qDebug() << "Devlist:" << devlist[i];
if(devlist[i].isEmpty() || devlist[i].startsWith("gpart:"))
{
devlist.removeAt(i);
i--;
}
}
if(devlist.isEmpty() || devlist.join("").simplified().isEmpty()){
qWarning("No Devices Found; Please connect a removable device and try again");
return;
}
args.clear();
args << "-h";
args << user.HomeFolder;
bool ok = false;
QString space = General::RunCommand("df -h "+home).split("\n").filter(home).join("");
space.replace("\t"," ");
space = space.section(" ",2,2,QString::SectionSkipEmpty);
if(!ok || home.isEmpty()){ return; }
home = home.section(":",0,0); //only need the raw device
//Save the password to a temporary file (for input to personacrypt)
QTemporaryFile tmpfile("/tmp/.XXXXXXXXXXXXXXX");
if(!tmpfile.open()){
//Error: could not open a temporary file
qWarning("Error; Could not create a temporary file for personacrypt");
return;
}
QTextStream out(&tmpfile);
out << password;
tmpfile.close();
//Now start the process of setting up the device
bool success = false;
args.clear();
args << "init";
args << "\""+user.UserName + "\"";
args << "\"" + tmpfile.fileName() + "\"";
args << home;
QStringList output = General::RunCommand(success,"personacrypt",args).split("\n");
if(success){
//Success
qDebug("Success; The PersonaCrypt device was successfully initialized");
}else{
//Failure - make sure the key was not created before the failure
if(QFile::exists("/var/db/personacrypt/"+user.UserName+".key")){
QFile::remove("/var/db/personacrypt/"+user.UserName+".key");
}
//Now show the error message with the log
qWarning("Failure; The PersonaCrypt device could not be initialized");
}
}

View File

@@ -0,0 +1,231 @@
//===========================================
// 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
//===========================================
#ifndef USERMANAGER_H
#define USERMANAGER_H
#include<QtCore>
namespace sysadm{
class User
{
public:
User()
{
FullName = "";
UserName = "";
ID = -1;
HomeFolder = "";
Shell = "";
GroupID = -1;
Encrypted = false;
}
QString FullName;
QString UserName;
int ID;
QString HomeFolder;
QString Shell;
int GroupID;
bool Encrypted;
friend bool operator<(const User lhs, const User rhs){
return std::tie(lhs.ID,lhs.UserName) < std::tie(rhs.ID,rhs.UserName);
}
friend bool operator>(const User lhs, const User rhs)
{ return rhs < lhs;}
friend bool operator==(const User lhs, const User rhs)
{
return lhs.ID == rhs.ID && lhs.UserName == rhs.UserName;
}
friend bool operator !=(const User lhs, const User rhs)
{ return !(lhs == rhs);}
};
class Group
{
public:
Group()
{
ID = -1;
Name = "";
Members = QStringList();
}
int ID;
QString Name;
//While the object model would be more "correct" if
//Users were to be a Vector of User pointers, it's
//expensive to wire up and we don't really gain anything
//from doing so
QStringList Members;
};
class UserManager
{
public:
UserManager(QString chroot = "");
//#section user actions
/**
* @brief NewUser Create a new user
* @param fullName The full name of the user
* @param userName The username of the user
* @param password The user's password
* @param home the location of the home directory
* @param shell the user's shell, defaults to /bin/tcsh
* @param uid the user id of the user
* @param gid the group id of the user
* @param encrypt whether to personaCrypt the User's home directory
*/
void NewUser(QString fullName, QString userName, QString password, QString home = "", QString shell = "/bin/tcsh", int uid = -1, int gid = -1, bool encrypt=false);
/**
* @brief DeleteUser Deletes a user
* @param user the user to delete
*/
void DeleteUser(User user);
/**
* @brief GetUsers getter for the users vector
* @return a QVector<Users> that is a copy of the current state
* do not modify it, instead call functions on this class to change
* things and then get another copy of the vector
*/
const QVector<User> GetUsers();
/**
* @brief GetUser get a particular user by their UID
* @param id the UID of the user to get
* @return the user with the UID specified, if not found
* returns a blank User
*/
const User GetUser(int id);
/**
* @brief GetUser get a particular user by their UID
* @param userName the username of the user to get
* @return the user with the user name specified, if not found
* returns a blank User
*/
const User GetUser(QString userName);
/**
* @brief ChangeUserPassword changes the specified user's password
* @param user the user to change the password of
* @param newPassword the new password
*/
void ChangeUserPassword(User user, QString newPassword);
/**
* @brief ChangeUserShell change a specified user's shell
* @param user the user to change the shell for
* @param shell the shell to change to, note that if the shell
* is not in the shells list then it does nothing
*/
void ChangeUserShell(User user, QString shell);
/**
* @brief ChangeUserFullName change the gecos field of a user to a new name
* @param user the user to change the name of
* @param newName the name to change to
*/
void ChangeUserFullName(User user, QString newName);
//#endsection
//#section group actions
/**
* @brief AddUserToGroup add the specified user to the specified group
* @param user the user to add to the group
* @param group the group to add the user to
*/
void AddUserToGroup(User user, Group group);
/**
* @brief RemoveUserFromGroup removes the specified user from the specified group
* @param user the user to remove from the group
* @param group the group to remove the user from
*/
void RemoveUserFromGroup(User user, Group group);
/**
* @brief NewGroup creates a new group
* @param name the name of the new group
* @param Users a list of users to add to the group
*/
void NewGroup(QString name, QStringList Users = QStringList());
/**
* @brief DeleteGroup delete a specified group
* @param group the group to delete
*/
void DeleteGroup(Group group);
/**
* @brief GetGroups get the internal list of groups
* @return a QVector<Group> that is a copy of the current state
* do not modify it, instead call functions on this class to change
* things and then get another copy of the vector
*/
const QVector<Group> GetGroups();
/**
* @brief getGroup get a specified group by their gid
* @param id the gid of the group to get
* @return the group with the specified gid
*/
const Group getGroup(int id);
/**
* @brief getGroup get a specified group by their name
* @param name the name of the group to get
* @return the group with the specified name
*/
const Group getGroup(QString name);
//#endsection
/**
* @brief GetShells the list of shells that are currently installed on the system
* @return a QStringList of shells on the system
*/
const QStringList GetShells();
/**
* @brief initPCDevice Initiate PersonaCrypt for the user
* @param user the user to initiate PersonaCrypt for
* @param home the location of the home directory
* @param password the password of the user
*/
void initPCDevice(User user, QString home, QString password);
/**
* @brief importPCKey Import a PersonaCrypt Key
* @param user the user to import the key for
* @param filename the location of the key
*/
void importPCKey(User user, QString filename);
/**
* @brief exportPCKey Export a PersonaCrypt Key
* @param user the user to export the key for
* @param filename the file to export to
*/
void exportPCKey(User user, QString filename);
/**
* @brief disablePCKey Disables a PersonaCrypt key
* @param user the user to diable PersonaCrypt for
*/
void disablePCKey(User user);
/**
* @brief disableAndCopyPCKey Disable a PersonaCrypt Key
* @param user for this user
* @param password password for the PersonaCrypt
*/
void disableAndCopyPCKey(User user, QString password);
private:
QVector<User> users;
QVector<Group> groups;
QStringList shells;
QString chroot;
//loads the users from /etc/passwd
void loadUsers();
//load the groups from /etc/group
void loadGroups();
//load the shells from /etc/shells
void loadShells();
};
}
#endif // USERMANAGER_H

View File

@@ -189,14 +189,26 @@ RestOutputStruct::ExitCode WebSocket::EvaluateSysadmLifePreserverRequest(const Q
ok = true;
out->insert("cronsnap", sysadm::LifePreserver::scheduleSnapshot(in_args.toObject()));
}
if(act=="initreplication"){
ok = true;
out->insert("initreplication", sysadm::LifePreserver::initReplication(in_args.toObject()));
}
if(act=="listcron"){
ok = true;
out->insert("listcron", sysadm::LifePreserver::listCron());
}
if(act=="listreplication"){
ok = true;
out->insert("listreplication", sysadm::LifePreserver::listReplication());
}
if(act=="listsnap"){
ok = true;
out->insert("listsnap", sysadm::LifePreserver::listSnap(in_args.toObject()));
}
if(act=="removereplication"){
ok = true;
out->insert("removereplication", sysadm::LifePreserver::removeReplication(in_args.toObject()));
}
if(act=="removesnap"){
ok = true;
out->insert("removesnap", sysadm::LifePreserver::removeSnapshot(in_args.toObject()));
@@ -205,6 +217,10 @@ RestOutputStruct::ExitCode WebSocket::EvaluateSysadmLifePreserverRequest(const Q
ok = true;
out->insert("revertsnap", sysadm::LifePreserver::revertSnapshot(in_args.toObject()));
}
if(act=="runreplication"){
ok = true;
out->insert("runreplication", sysadm::LifePreserver::runReplication(in_args.toObject()));
}
if(act=="savesettings"){
ok = true;
out->insert("savesettings", sysadm::LifePreserver::saveSettings(in_args.toObject()));

View File

@@ -70,17 +70,21 @@ echo ""
# Source our resty functions
. ./utils/resty -W "https://127.0.0.1:12151" -H "Accept: application/json" -H "Content-Type: application/json" -u ${fuser}:${fpass}
# Check the reply of this REST query
echo ""
echo "REST Request:"
echo "-------------------------------"
echo "PUT /${namesp}/${name}"
echo "${payload}" | perl -0007 -MJSON -ne'print to_json(from_json($_, {allow_nonref=>1}),{pretty=>1})."\n"'
# Save output to a file in addition to stdout
ofile="/tmp/api-response"
echo "" > /tmp/api-response
echo ""
echo "REST Response:"
echo "-------------------------------"
PUT /${namesp}/${name} "${payload}" -v -k 2>/tmp/.rstErr
# Check the reply of this REST query
echo "" | tee -a $ofile
echo "REST Request:" | tee -a $ofile
echo "-------------------------------" | tee -a $ofile
echo "PUT /${namesp}/${name}" | tee -a $ofile
echo "${payload}" | perl -0007 -MJSON -ne'print to_json(from_json($_, {allow_nonref=>1}),{pretty=>1})."\n"' | tee -a $ofile
echo "" | tee -a $ofile
echo "REST Response:" | tee -a $ofile
echo "-------------------------------" | tee -a $ofile
PUT /${namesp}/${name} "${payload}" -v -k 2>/tmp/.rstErr | tee -a $ofile
if [ $? -ne 0 ] ; then
echo "Failed.. Error output:"
cat /tmp/.rstErr
@@ -90,12 +94,12 @@ fi
# Now check the response via WebSockets
export NODE_TLS_REJECT_UNAUTHORIZED=0
echo ""
echo "WebSocket Request:"
echo "-------------------------------"
echo "{ \"namespace\":\"${namesp}\", \"name\":\"${name}\", \"id\":\"fooid\", \"args\":${payload} }" | perl -0007 -MJSON -ne'print to_json(from_json($_, {allow_nonref=>1}),{pretty=>1})."\n"'
echo "" | tee -a $ofile
echo "WebSocket Request:" | tee -a $ofile
echo "-------------------------------" | tee -a $ofile
echo "{ \"namespace\":\"${namesp}\", \"name\":\"${name}\", \"id\":\"fooid\", \"args\":${payload} }" | perl -0007 -MJSON -ne'print to_json(from_json($_, {allow_nonref=>1}),{pretty=>1})."\n"' | tee -a $ofile
echo ""
echo "WebSocket Response:"
echo "-------------------------------"
echo "{ \"namespace\":\"${namesp}\", \"name\":\"${name}\", \"id\":\"fooid\", \"args\":${payload} }" | node sendwebsocket.js "$fuser" "$fpass"
echo "" | tee -a $ofile
echo "WebSocket Response:" | tee -a $ofile
echo "-------------------------------" | tee -a $ofile
echo "{ \"namespace\":\"${namesp}\", \"name\":\"${name}\", \"id\":\"fooid\", \"args\":${payload} }" | node sendwebsocket.js "$fuser" "$fpass" | tee -a $ofile