Skip to content

Commit

Permalink
fix(root-user): improve error messages installs set up with root (#631)
Browse files Browse the repository at this point in the history
no issue
- detect DigitalOcean One-Click install and render a message how to migrate their installation to a non-root user
- detect root installs and render message to migrate their installation to a non-root user
- allow ghost `start`, `stop` and `restart` to be executed as root without exiting, but showing the error message
  • Loading branch information
aileen authored and acburdine committed Feb 7, 2018
1 parent 05a4171 commit b161432
Show file tree
Hide file tree
Showing 3 changed files with 216 additions and 15 deletions.
2 changes: 1 addition & 1 deletion lib/command.js
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ class Command {
if (!this.allowRoot) {
const checkRootUser = require('./utils/check-root-user');
// Check if user is trying to install as `root`
checkRootUser();
checkRootUser(commandName);
}

// Set process title
Expand Down
35 changes: 29 additions & 6 deletions lib/utils/check-root-user.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,41 @@

const os = require('os');
const chalk = require('chalk');
const fs = require('fs');
const includes = require('lodash/includes');

function checkRootUser() {
// Skip if we're on windows
if (os.platform() !== 'linux') {
const isRootInstall = function isRootInstall() {
const path = require('path');
const cliFile = path.join(process.cwd(), '.ghost-cli');

return fs.existsSync(cliFile) && fs.statSync(cliFile).uid === 0;
}

function checkRootUser(command) {
const allowedCommands = ['stop', 'start', 'restart'];
const isOneClickInstall = fs.existsSync('/root/.digitalocean_password');

if (os.platform() !== 'linux' || process.getuid() !== 0) {
return;
}

if (process.getuid() === 0) {
if (isOneClickInstall) {
// We have a Digitalocean one click installation
console.error(`${chalk.yellow('We discovered that you are using the Digitalocean One-Click install.')}
You need to create a user with regular account privileges and migrate your installation to use this user.
Please follow the steps here: ${chalk.underline.green('https://docs.ghost.org/docs/troubleshooting#section-fix-root-user')} to fix your setup.\n`);
} else if (isRootInstall()) {
console.error(`${chalk.yellow('It seems Ghost was installed using the root user.')}
You need to create a user with regular account privileges and migrate your installation to use this user.
Please follow the steps here: ${chalk.underline.green('https://docs.ghost.org/docs/troubleshooting#section-fix-root-user')} to fix your setup.\n`);
} else {
console.error(`${chalk.yellow('Can\'t run command as \'root\' user.')}
Please create a new user with regular account privileges and use this user to run the command.
See ${chalk.underline.blue('https://docs.ghost.org/docs/install#section-create-a-new-user')} for more information`);
Please use the user you set up in the installation process, or create a new user with regular account privileges and use this user to run 'ghost ${command}'.
See ${chalk.underline.green('https://docs.ghost.org/docs/install#section-create-a-new-user')} for more information\n`);
}

// TODO: remove this 4 versions after 1.5.0
if (!includes(allowedCommands, command)) {
process.exit(1);
}
}
Expand Down
194 changes: 186 additions & 8 deletions test/unit/utils/check-root-user-spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
const expect = require('chai').expect;
const sinon = require('sinon');
const os = require('os');
const fs = require('fs');
const checkRootUser = require('../../../lib/utils/check-root-user');

describe('Unit: Utils > checkRootUser', function () {
Expand All @@ -16,7 +17,7 @@ describe('Unit: Utils > checkRootUser', function () {
const osStub = sandbox.stub(os, 'platform').returns('win32');
const processStub = sandbox.stub(process, 'getuid').returns(0);

checkRootUser('test');
checkRootUser('install');
expect(osStub.calledOnce).to.be.true;
expect(processStub.called).to.be.false;
});
Expand All @@ -25,22 +26,139 @@ describe('Unit: Utils > checkRootUser', function () {
const osStub = sandbox.stub(os, 'platform').returns('darwin');
const processStub = sandbox.stub(process, 'getuid').returns(0);

checkRootUser('test');
checkRootUser('doctor');
expect(osStub.calledOnce).to.be.true;
expect(processStub.called).to.be.false;
});

it('throws error command run with root', function () {
it('skips check if command run as non root user', function () {
const osStub = sandbox.stub(os, 'platform').returns('linux');
const processStub = sandbox.stub(process, 'getuid').returns(501);
const exitStub = sandbox.stub(process, 'exit').throws();
const errorStub = sandbox.stub(console, 'error');

checkRootUser('update');
expect(osStub.calledOnce).to.be.true;
expect(processStub.calledOnce).to.be.true;
expect(errorStub.calledOnce).to.be.false;
expect(exitStub.calledOnce).to.be.false;
});

it('shows special message for DigitalOcean One-Click installs', function () {
const osStub = sandbox.stub(os, 'platform').returns('linux');
const fsStub = sandbox.stub(fs, 'existsSync');
const processStub = sandbox.stub(process, 'getuid').returns(0);
const exitStub = sandbox.stub(process, 'exit').throws();
const errorStub = sandbox.stub(console, 'error');

fsStub.withArgs('/root/.digitalocean_password').returns(true);
fsStub.withArgs('/var/www/ghost/.ghost-cli').returns(true);

try {
checkRootUser('test');
checkRootUser('ls');
throw new Error('should not be thrown');
} catch (e) {
expect(e.message).to.not.equal('should not be thrown');
expect(fsStub.calledWithExactly('/root/.digitalocean_password')).to.be.true;
expect(osStub.calledOnce).to.be.true;
expect(processStub.calledOnce).to.be.true;
expect(errorStub.calledOnce).to.be.true;
expect(exitStub.calledOnce).to.be.true;
expect(errorStub.args[0][0]).to.match(/We discovered that you are using the Digitalocean One-Click install./);
}
});

it('shows special message for DigitalOcean One-Click installs, but doesn\'t exit on `stop`', function () {
const osStub = sandbox.stub(os, 'platform').returns('linux');
const fsStub = sandbox.stub(fs, 'existsSync');
const processStub = sandbox.stub(process, 'getuid').returns(0);
const exitStub = sandbox.stub(process, 'exit').throws();
const errorStub = sandbox.stub(console, 'error');

fsStub.withArgs('/root/.digitalocean_password').returns(true);
fsStub.withArgs('/var/www/ghost/.ghost-cli').returns(true);

checkRootUser('stop');
expect(fsStub.calledWithExactly('/root/.digitalocean_password')).to.be.true;
expect(osStub.calledOnce).to.be.true;
expect(processStub.calledOnce).to.be.true;
expect(errorStub.calledOnce).to.be.true;
expect(exitStub.calledOnce).to.be.false;
expect(errorStub.args[0][0]).to.match(/We discovered that you are using the Digitalocean One-Click install./);
});

it('shows special message for root installs', function () {
const osStub = sandbox.stub(os, 'platform').returns('linux');
const cwdStub = sandbox.stub(process, 'cwd').returns('/var/www/ghost');
const fsStub = sandbox.stub(fs, 'existsSync');
const fsStatStub = sandbox.stub(fs, 'statSync').returns({uid: 0});
const processStub = sandbox.stub(process, 'getuid').returns(0);
const exitStub = sandbox.stub(process, 'exit').throws();
const errorStub = sandbox.stub(console, 'error');

fsStub.withArgs('/root/.digitalocean_password').returns(false);
fsStub.withArgs('/var/www/ghost/.ghost-cli').returns(true);

try {
checkRootUser('ls');
throw new Error('should not be thrown');
} catch (e) {
expect(e.message).to.not.equal('should not be thrown');
expect(cwdStub.calledOnce).to.be.true;
expect(fsStub.calledWithExactly('/root/.digitalocean_password')).to.be.true;
expect(fsStub.calledWithExactly('/var/www/ghost/.ghost-cli')).to.be.true;
expect(fsStatStub.calledWithExactly('/var/www/ghost/.ghost-cli')).to.be.true;
expect(osStub.calledOnce).to.be.true;
expect(processStub.calledOnce).to.be.true;
expect(errorStub.calledOnce).to.be.true;
expect(exitStub.calledOnce).to.be.true;
expect(errorStub.args[0][0]).to.match(/It seems Ghost was installed using the root user./);
}
});

it('shows special message for root installs, but doesn\'t exit on `start`', function () {
const osStub = sandbox.stub(os, 'platform').returns('linux');
const cwdStub = sandbox.stub(process, 'cwd').returns('/var/www/ghost');
const fsStub = sandbox.stub(fs, 'existsSync');
const fsStatStub = sandbox.stub(fs, 'statSync').returns({uid: 0});
const processStub = sandbox.stub(process, 'getuid').returns(0);
const exitStub = sandbox.stub(process, 'exit').throws();
const errorStub = sandbox.stub(console, 'error');

fsStub.withArgs('/root/.digitalocean_password').returns(false);
fsStub.withArgs('/var/www/ghost/.ghost-cli').returns(true);

checkRootUser('start');
expect(cwdStub.calledOnce).to.be.true;
expect(fsStub.calledWithExactly('/root/.digitalocean_password')).to.be.true;
expect(fsStub.calledWithExactly('/var/www/ghost/.ghost-cli')).to.be.true;
expect(fsStatStub.calledWithExactly('/var/www/ghost/.ghost-cli')).to.be.true;
expect(osStub.calledOnce).to.be.true;
expect(processStub.calledOnce).to.be.true;
expect(errorStub.calledOnce).to.be.true;
expect(exitStub.calledOnce).to.be.false;
expect(errorStub.args[0][0]).to.match(/It seems Ghost was installed using the root user./);
});

it('throws error command run with root for non-root installs', function () {
const osStub = sandbox.stub(os, 'platform').returns('linux');
const cwdStub = sandbox.stub(process, 'cwd').returns('/var/www/ghost');
const fsStub = sandbox.stub(fs, 'existsSync');
const fsStatStub = sandbox.stub(fs, 'statSync').returns({uid: 501});
const processStub = sandbox.stub(process, 'getuid').returns(0);
const exitStub = sandbox.stub(process, 'exit').throws();
const errorStub = sandbox.stub(console, 'error');

fsStub.withArgs('/root/.digitalocean_password').returns(false);
fsStub.withArgs('/var/www/ghost/.ghost-cli').returns(true);

try {
checkRootUser('update');
throw new Error('should not be thrown');
} catch (e) {
expect(e.message).to.not.equal('should not be thrown');
expect(cwdStub.calledOnce).to.be.true;
expect(fsStatStub.calledWithExactly('/var/www/ghost/.ghost-cli')).to.be.true;
expect(osStub.calledOnce).to.be.true;
expect(processStub.calledOnce).to.be.true;
expect(errorStub.calledOnce).to.be.true;
Expand All @@ -49,16 +167,76 @@ describe('Unit: Utils > checkRootUser', function () {
}
});

it('doesn\'t do anything if command run as non root user', function () {
it('throws error command run with root for non-root installs, but doesn\'t exit on `restart`', function () {
const osStub = sandbox.stub(os, 'platform').returns('linux');
const processStub = sandbox.stub(process, 'getuid').returns(501);
const cwdStub = sandbox.stub(process, 'cwd').returns('/var/www/ghost');
const fsStub = sandbox.stub(fs, 'existsSync');
const fsStatStub = sandbox.stub(fs, 'statSync').returns({uid: 501});
const processStub = sandbox.stub(process, 'getuid').returns(0);
const exitStub = sandbox.stub(process, 'exit').throws();
const errorStub = sandbox.stub(console, 'error');

checkRootUser('test');
fsStub.withArgs('/root/.digitalocean_password').returns(false);
fsStub.withArgs('/var/www/ghost/.ghost-cli').returns(true);

checkRootUser('restart');
expect(cwdStub.calledOnce).to.be.true;
expect(fsStatStub.calledWithExactly('/var/www/ghost/.ghost-cli')).to.be.true;
expect(osStub.calledOnce).to.be.true;
expect(processStub.calledOnce).to.be.true;
expect(errorStub.calledOnce).to.be.false;
expect(errorStub.calledOnce).to.be.true;
expect(exitStub.calledOnce).to.be.false;
expect(errorStub.args[0][0]).to.match(/Can't run command as 'root' user/);
});

it('throws error command run with root outside of valid ghost installation', function () {
const osStub = sandbox.stub(os, 'platform').returns('linux');
const cwdStub = sandbox.stub(process, 'cwd').returns('/var/www/');
const fsStub = sandbox.stub(fs, 'existsSync');
const fsStatStub = sandbox.stub(fs, 'statSync').returns({uid: 501});
const processStub = sandbox.stub(process, 'getuid').returns(0);
const exitStub = sandbox.stub(process, 'exit').throws();
const errorStub = sandbox.stub(console, 'error');

fsStub.withArgs('/root/.digitalocean_password').returns(false);
fsStub.withArgs('/var/www/.ghost-cli').returns(false);

try {
checkRootUser('update');
throw new Error('should not be thrown');
} catch (e) {
expect(e.message).to.not.equal('should not be thrown');
expect(cwdStub.calledOnce).to.be.true;
expect(fsStub.calledWithExactly('/var/www/.ghost-cli')).to.be.true;
expect(fsStatStub.calledOnce).to.be.false;
expect(osStub.calledOnce).to.be.true;
expect(processStub.calledOnce).to.be.true;
expect(errorStub.calledOnce).to.be.true;
expect(exitStub.calledOnce).to.be.true;
expect(errorStub.args[0][0]).to.match(/Can't run command as 'root' user/);
}
});

it('throws error command run with root outside of valid ghost installation, but doesn\'t exit on `restart`', function () {
const osStub = sandbox.stub(os, 'platform').returns('linux');
const cwdStub = sandbox.stub(process, 'cwd').returns('/var/www/');
const fsStub = sandbox.stub(fs, 'existsSync');
const fsStatStub = sandbox.stub(fs, 'statSync').returns({uid: 501});
const processStub = sandbox.stub(process, 'getuid').returns(0);
const exitStub = sandbox.stub(process, 'exit').throws();
const errorStub = sandbox.stub(console, 'error');

fsStub.withArgs('/root/.digitalocean_password').returns(false);
fsStub.withArgs('/var/www/.ghost-cli').returns(false);

checkRootUser('restart');
expect(cwdStub.calledOnce).to.be.true;
expect(fsStub.calledWithExactly('/var/www/.ghost-cli')).to.be.true;
expect(fsStatStub.calledOnce).to.be.false;
expect(osStub.calledOnce).to.be.true;
expect(processStub.calledOnce).to.be.true;
expect(errorStub.calledOnce).to.be.true;
expect(exitStub.calledOnce).to.be.false;
expect(errorStub.args[0][0]).to.match(/Can't run command as 'root' user/);
});
});

0 comments on commit b161432

Please sign in to comment.