diff --git a/lib/command.js b/lib/command.js index 63c333b35..c7becedb7 100644 --- a/lib/command.js +++ b/lib/command.js @@ -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 diff --git a/lib/utils/check-root-user.js b/lib/utils/check-root-user.js index c7600bd28..39191a24e 100644 --- a/lib/utils/check-root-user.js +++ b/lib/utils/check-root-user.js @@ -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); } } diff --git a/test/unit/utils/check-root-user-spec.js b/test/unit/utils/check-root-user-spec.js index 672fade9a..32f0bef2a 100644 --- a/test/unit/utils/check-root-user-spec.js +++ b/test/unit/utils/check-root-user-spec.js @@ -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 () { @@ -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; }); @@ -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; @@ -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/); }); });