Skip to content

Commit b161432

Browse files
aileenacburdine
authored andcommitted
fix(root-user): improve error messages installs set up with root (#631)
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
1 parent 05a4171 commit b161432

File tree

3 files changed

+216
-15
lines changed

3 files changed

+216
-15
lines changed

lib/command.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ class Command {
117117
if (!this.allowRoot) {
118118
const checkRootUser = require('./utils/check-root-user');
119119
// Check if user is trying to install as `root`
120-
checkRootUser();
120+
checkRootUser(commandName);
121121
}
122122

123123
// Set process title

lib/utils/check-root-user.js

+29-6
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,41 @@
22

33
const os = require('os');
44
const chalk = require('chalk');
5+
const fs = require('fs');
6+
const includes = require('lodash/includes');
57

6-
function checkRootUser() {
7-
// Skip if we're on windows
8-
if (os.platform() !== 'linux') {
8+
const isRootInstall = function isRootInstall() {
9+
const path = require('path');
10+
const cliFile = path.join(process.cwd(), '.ghost-cli');
11+
12+
return fs.existsSync(cliFile) && fs.statSync(cliFile).uid === 0;
13+
}
14+
15+
function checkRootUser(command) {
16+
const allowedCommands = ['stop', 'start', 'restart'];
17+
const isOneClickInstall = fs.existsSync('/root/.digitalocean_password');
18+
19+
if (os.platform() !== 'linux' || process.getuid() !== 0) {
920
return;
1021
}
1122

12-
if (process.getuid() === 0) {
23+
if (isOneClickInstall) {
24+
// We have a Digitalocean one click installation
25+
console.error(`${chalk.yellow('We discovered that you are using the Digitalocean One-Click install.')}
26+
You need to create a user with regular account privileges and migrate your installation to use this user.
27+
Please follow the steps here: ${chalk.underline.green('https://docs.ghost.org/docs/troubleshooting#section-fix-root-user')} to fix your setup.\n`);
28+
} else if (isRootInstall()) {
29+
console.error(`${chalk.yellow('It seems Ghost was installed using the root user.')}
30+
You need to create a user with regular account privileges and migrate your installation to use this user.
31+
Please follow the steps here: ${chalk.underline.green('https://docs.ghost.org/docs/troubleshooting#section-fix-root-user')} to fix your setup.\n`);
32+
} else {
1333
console.error(`${chalk.yellow('Can\'t run command as \'root\' user.')}
14-
Please create a new user with regular account privileges and use this user to run the command.
15-
See ${chalk.underline.blue('https://docs.ghost.org/docs/install#section-create-a-new-user')} for more information`);
34+
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}'.
35+
See ${chalk.underline.green('https://docs.ghost.org/docs/install#section-create-a-new-user')} for more information\n`);
36+
}
1637

38+
// TODO: remove this 4 versions after 1.5.0
39+
if (!includes(allowedCommands, command)) {
1740
process.exit(1);
1841
}
1942
}

test/unit/utils/check-root-user-spec.js

+186-8
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
const expect = require('chai').expect;
44
const sinon = require('sinon');
55
const os = require('os');
6+
const fs = require('fs');
67
const checkRootUser = require('../../../lib/utils/check-root-user');
78

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

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

28-
checkRootUser('test');
29+
checkRootUser('doctor');
2930
expect(osStub.calledOnce).to.be.true;
3031
expect(processStub.called).to.be.false;
3132
});
3233

33-
it('throws error command run with root', function () {
34+
it('skips check if command run as non root user', function () {
3435
const osStub = sandbox.stub(os, 'platform').returns('linux');
36+
const processStub = sandbox.stub(process, 'getuid').returns(501);
37+
const exitStub = sandbox.stub(process, 'exit').throws();
38+
const errorStub = sandbox.stub(console, 'error');
39+
40+
checkRootUser('update');
41+
expect(osStub.calledOnce).to.be.true;
42+
expect(processStub.calledOnce).to.be.true;
43+
expect(errorStub.calledOnce).to.be.false;
44+
expect(exitStub.calledOnce).to.be.false;
45+
});
46+
47+
it('shows special message for DigitalOcean One-Click installs', function () {
48+
const osStub = sandbox.stub(os, 'platform').returns('linux');
49+
const fsStub = sandbox.stub(fs, 'existsSync');
3550
const processStub = sandbox.stub(process, 'getuid').returns(0);
3651
const exitStub = sandbox.stub(process, 'exit').throws();
3752
const errorStub = sandbox.stub(console, 'error');
3853

54+
fsStub.withArgs('/root/.digitalocean_password').returns(true);
55+
fsStub.withArgs('/var/www/ghost/.ghost-cli').returns(true);
56+
3957
try {
40-
checkRootUser('test');
58+
checkRootUser('ls');
4159
throw new Error('should not be thrown');
4260
} catch (e) {
4361
expect(e.message).to.not.equal('should not be thrown');
62+
expect(fsStub.calledWithExactly('/root/.digitalocean_password')).to.be.true;
63+
expect(osStub.calledOnce).to.be.true;
64+
expect(processStub.calledOnce).to.be.true;
65+
expect(errorStub.calledOnce).to.be.true;
66+
expect(exitStub.calledOnce).to.be.true;
67+
expect(errorStub.args[0][0]).to.match(/We discovered that you are using the Digitalocean One-Click install./);
68+
}
69+
});
70+
71+
it('shows special message for DigitalOcean One-Click installs, but doesn\'t exit on `stop`', function () {
72+
const osStub = sandbox.stub(os, 'platform').returns('linux');
73+
const fsStub = sandbox.stub(fs, 'existsSync');
74+
const processStub = sandbox.stub(process, 'getuid').returns(0);
75+
const exitStub = sandbox.stub(process, 'exit').throws();
76+
const errorStub = sandbox.stub(console, 'error');
77+
78+
fsStub.withArgs('/root/.digitalocean_password').returns(true);
79+
fsStub.withArgs('/var/www/ghost/.ghost-cli').returns(true);
80+
81+
checkRootUser('stop');
82+
expect(fsStub.calledWithExactly('/root/.digitalocean_password')).to.be.true;
83+
expect(osStub.calledOnce).to.be.true;
84+
expect(processStub.calledOnce).to.be.true;
85+
expect(errorStub.calledOnce).to.be.true;
86+
expect(exitStub.calledOnce).to.be.false;
87+
expect(errorStub.args[0][0]).to.match(/We discovered that you are using the Digitalocean One-Click install./);
88+
});
89+
90+
it('shows special message for root installs', function () {
91+
const osStub = sandbox.stub(os, 'platform').returns('linux');
92+
const cwdStub = sandbox.stub(process, 'cwd').returns('/var/www/ghost');
93+
const fsStub = sandbox.stub(fs, 'existsSync');
94+
const fsStatStub = sandbox.stub(fs, 'statSync').returns({uid: 0});
95+
const processStub = sandbox.stub(process, 'getuid').returns(0);
96+
const exitStub = sandbox.stub(process, 'exit').throws();
97+
const errorStub = sandbox.stub(console, 'error');
98+
99+
fsStub.withArgs('/root/.digitalocean_password').returns(false);
100+
fsStub.withArgs('/var/www/ghost/.ghost-cli').returns(true);
101+
102+
try {
103+
checkRootUser('ls');
104+
throw new Error('should not be thrown');
105+
} catch (e) {
106+
expect(e.message).to.not.equal('should not be thrown');
107+
expect(cwdStub.calledOnce).to.be.true;
108+
expect(fsStub.calledWithExactly('/root/.digitalocean_password')).to.be.true;
109+
expect(fsStub.calledWithExactly('/var/www/ghost/.ghost-cli')).to.be.true;
110+
expect(fsStatStub.calledWithExactly('/var/www/ghost/.ghost-cli')).to.be.true;
111+
expect(osStub.calledOnce).to.be.true;
112+
expect(processStub.calledOnce).to.be.true;
113+
expect(errorStub.calledOnce).to.be.true;
114+
expect(exitStub.calledOnce).to.be.true;
115+
expect(errorStub.args[0][0]).to.match(/It seems Ghost was installed using the root user./);
116+
}
117+
});
118+
119+
it('shows special message for root installs, but doesn\'t exit on `start`', function () {
120+
const osStub = sandbox.stub(os, 'platform').returns('linux');
121+
const cwdStub = sandbox.stub(process, 'cwd').returns('/var/www/ghost');
122+
const fsStub = sandbox.stub(fs, 'existsSync');
123+
const fsStatStub = sandbox.stub(fs, 'statSync').returns({uid: 0});
124+
const processStub = sandbox.stub(process, 'getuid').returns(0);
125+
const exitStub = sandbox.stub(process, 'exit').throws();
126+
const errorStub = sandbox.stub(console, 'error');
127+
128+
fsStub.withArgs('/root/.digitalocean_password').returns(false);
129+
fsStub.withArgs('/var/www/ghost/.ghost-cli').returns(true);
130+
131+
checkRootUser('start');
132+
expect(cwdStub.calledOnce).to.be.true;
133+
expect(fsStub.calledWithExactly('/root/.digitalocean_password')).to.be.true;
134+
expect(fsStub.calledWithExactly('/var/www/ghost/.ghost-cli')).to.be.true;
135+
expect(fsStatStub.calledWithExactly('/var/www/ghost/.ghost-cli')).to.be.true;
136+
expect(osStub.calledOnce).to.be.true;
137+
expect(processStub.calledOnce).to.be.true;
138+
expect(errorStub.calledOnce).to.be.true;
139+
expect(exitStub.calledOnce).to.be.false;
140+
expect(errorStub.args[0][0]).to.match(/It seems Ghost was installed using the root user./);
141+
});
142+
143+
it('throws error command run with root for non-root installs', function () {
144+
const osStub = sandbox.stub(os, 'platform').returns('linux');
145+
const cwdStub = sandbox.stub(process, 'cwd').returns('/var/www/ghost');
146+
const fsStub = sandbox.stub(fs, 'existsSync');
147+
const fsStatStub = sandbox.stub(fs, 'statSync').returns({uid: 501});
148+
const processStub = sandbox.stub(process, 'getuid').returns(0);
149+
const exitStub = sandbox.stub(process, 'exit').throws();
150+
const errorStub = sandbox.stub(console, 'error');
151+
152+
fsStub.withArgs('/root/.digitalocean_password').returns(false);
153+
fsStub.withArgs('/var/www/ghost/.ghost-cli').returns(true);
154+
155+
try {
156+
checkRootUser('update');
157+
throw new Error('should not be thrown');
158+
} catch (e) {
159+
expect(e.message).to.not.equal('should not be thrown');
160+
expect(cwdStub.calledOnce).to.be.true;
161+
expect(fsStatStub.calledWithExactly('/var/www/ghost/.ghost-cli')).to.be.true;
44162
expect(osStub.calledOnce).to.be.true;
45163
expect(processStub.calledOnce).to.be.true;
46164
expect(errorStub.calledOnce).to.be.true;
@@ -49,16 +167,76 @@ describe('Unit: Utils > checkRootUser', function () {
49167
}
50168
});
51169

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

58-
checkRootUser('test');
179+
fsStub.withArgs('/root/.digitalocean_password').returns(false);
180+
fsStub.withArgs('/var/www/ghost/.ghost-cli').returns(true);
181+
182+
checkRootUser('restart');
183+
expect(cwdStub.calledOnce).to.be.true;
184+
expect(fsStatStub.calledWithExactly('/var/www/ghost/.ghost-cli')).to.be.true;
59185
expect(osStub.calledOnce).to.be.true;
60186
expect(processStub.calledOnce).to.be.true;
61-
expect(errorStub.calledOnce).to.be.false;
187+
expect(errorStub.calledOnce).to.be.true;
188+
expect(exitStub.calledOnce).to.be.false;
189+
expect(errorStub.args[0][0]).to.match(/Can't run command as 'root' user/);
190+
});
191+
192+
it('throws error command run with root outside of valid ghost installation', function () {
193+
const osStub = sandbox.stub(os, 'platform').returns('linux');
194+
const cwdStub = sandbox.stub(process, 'cwd').returns('/var/www/');
195+
const fsStub = sandbox.stub(fs, 'existsSync');
196+
const fsStatStub = sandbox.stub(fs, 'statSync').returns({uid: 501});
197+
const processStub = sandbox.stub(process, 'getuid').returns(0);
198+
const exitStub = sandbox.stub(process, 'exit').throws();
199+
const errorStub = sandbox.stub(console, 'error');
200+
201+
fsStub.withArgs('/root/.digitalocean_password').returns(false);
202+
fsStub.withArgs('/var/www/.ghost-cli').returns(false);
203+
204+
try {
205+
checkRootUser('update');
206+
throw new Error('should not be thrown');
207+
} catch (e) {
208+
expect(e.message).to.not.equal('should not be thrown');
209+
expect(cwdStub.calledOnce).to.be.true;
210+
expect(fsStub.calledWithExactly('/var/www/.ghost-cli')).to.be.true;
211+
expect(fsStatStub.calledOnce).to.be.false;
212+
expect(osStub.calledOnce).to.be.true;
213+
expect(processStub.calledOnce).to.be.true;
214+
expect(errorStub.calledOnce).to.be.true;
215+
expect(exitStub.calledOnce).to.be.true;
216+
expect(errorStub.args[0][0]).to.match(/Can't run command as 'root' user/);
217+
}
218+
});
219+
220+
it('throws error command run with root outside of valid ghost installation, but doesn\'t exit on `restart`', function () {
221+
const osStub = sandbox.stub(os, 'platform').returns('linux');
222+
const cwdStub = sandbox.stub(process, 'cwd').returns('/var/www/');
223+
const fsStub = sandbox.stub(fs, 'existsSync');
224+
const fsStatStub = sandbox.stub(fs, 'statSync').returns({uid: 501});
225+
const processStub = sandbox.stub(process, 'getuid').returns(0);
226+
const exitStub = sandbox.stub(process, 'exit').throws();
227+
const errorStub = sandbox.stub(console, 'error');
228+
229+
fsStub.withArgs('/root/.digitalocean_password').returns(false);
230+
fsStub.withArgs('/var/www/.ghost-cli').returns(false);
231+
232+
checkRootUser('restart');
233+
expect(cwdStub.calledOnce).to.be.true;
234+
expect(fsStub.calledWithExactly('/var/www/.ghost-cli')).to.be.true;
235+
expect(fsStatStub.calledOnce).to.be.false;
236+
expect(osStub.calledOnce).to.be.true;
237+
expect(processStub.calledOnce).to.be.true;
238+
expect(errorStub.calledOnce).to.be.true;
62239
expect(exitStub.calledOnce).to.be.false;
240+
expect(errorStub.args[0][0]).to.match(/Can't run command as 'root' user/);
63241
});
64242
});

0 commit comments

Comments
 (0)