Skip to content

Commit 56e2f47

Browse files
[CODE-12] Improve sudo handling (#23)
* [CODE-12] Added ability to for plugins to request elevated sudo privileges * [CODE-12] Added secure mode. In secure mode, plugins are launched as detached (not shared with the parent process) and without tty and thus can't request / share sudo. Sudo will be asked every time it's requested. * [CODE-12] Fixed codify-schema dependency * [CODE-71] Improved sudo for default reporter. Made sudo command show asterisks and made sudo show the progress display before resuming a long running command
1 parent bcba094 commit 56e2f47

19 files changed

Lines changed: 287 additions & 100 deletions

README.md

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,11 @@ USAGE
3333
* [`codify plugins`](#codify-plugins)
3434
* [`codify plugins:install PLUGIN...`](#codify-pluginsinstall-plugin)
3535
* [`codify plugins:inspect PLUGIN...`](#codify-pluginsinspect-plugin)
36-
* [`codify plugins:install PLUGIN...`](#codify-pluginsinstall-plugin-1)
36+
* [`codify plugins:install PLUGIN...`](#codify-pluginsinstall-plugin)
3737
* [`codify plugins:link PLUGIN`](#codify-pluginslink-plugin)
3838
* [`codify plugins:uninstall PLUGIN...`](#codify-pluginsuninstall-plugin)
39-
* [`codify plugins:uninstall PLUGIN...`](#codify-pluginsuninstall-plugin-1)
40-
* [`codify plugins:uninstall PLUGIN...`](#codify-pluginsuninstall-plugin-2)
39+
* [`codify plugins:uninstall PLUGIN...`](#codify-pluginsuninstall-plugin)
40+
* [`codify plugins:uninstall PLUGIN...`](#codify-pluginsuninstall-plugin)
4141
* [`codify plugins update`](#codify-plugins-update)
4242
* [`codify uninstall`](#codify-uninstall)
4343

@@ -47,7 +47,7 @@ describe the command here
4747

4848
```
4949
USAGE
50-
$ codify apply [FILE] [--json] [-o plain|default|debug|json] [--debug] [-p <value>]
50+
$ codify apply [FILE] [--json] [--debug] [-o plain|default|debug|json] [-s] [-p <value>]
5151
5252
ARGUMENTS
5353
FILE file to read
@@ -56,6 +56,7 @@ FLAGS
5656
-o, --output=<option> [default: default]
5757
<options: plain|default|debug|json>
5858
-p, --path=<value> path to project
59+
-s, --secure
5960
--debug
6061
6162
GLOBAL FLAGS
@@ -97,7 +98,7 @@ describe the command here
9798

9899
```
99100
USAGE
100-
$ codify plan [FILE] [--json] [-o plain|default|debug|json] [--debug] [-f] [-n <value>] [-p <value>]
101+
$ codify plan [FILE] [--json] [--debug] [-o plain|default|debug|json] [-s] [-f] [-n <value>] [-p <value>]
101102
102103
ARGUMENTS
103104
FILE file to read
@@ -108,6 +109,7 @@ FLAGS
108109
-o, --output=<option> [default: default]
109110
<options: plain|default|debug|json>
110111
-p, --path=<value> path to project
112+
-s, --secure
111113
--debug
112114
113115
GLOBAL FLAGS
@@ -143,7 +145,7 @@ EXAMPLES
143145
$ codify plugins
144146
```
145147

146-
_See code: [@oclif/plugin-plugins](https://github.com/oclif/plugin-plugins/blob/v3.9.4/src/commands/plugins/index.ts)_
148+
_See code: [@oclif/plugin-plugins](https://github.com/oclif/plugin-plugins/blob/v3.10.1/src/commands/plugins/index.ts)_
147149

148150
## `codify plugins:install PLUGIN...`
149151

@@ -208,7 +210,8 @@ EXAMPLES
208210
$ codify plugins:inspect myplugin
209211
```
210212

211-
_See code: [@oclif/plugin-plugins](https://github.com/oclif/plugin-plugins/blob/v3.9.4/src/commands/plugins/inspect.ts)_
213+
_See
214+
code: [@oclif/plugin-plugins](https://github.com/oclif/plugin-plugins/blob/v3.10.1/src/commands/plugins/inspect.ts)_
212215

213216
## `codify plugins:install PLUGIN...`
214217

@@ -248,7 +251,8 @@ EXAMPLES
248251
$ codify plugins:install someuser/someplugin
249252
```
250253

251-
_See code: [@oclif/plugin-plugins](https://github.com/oclif/plugin-plugins/blob/v3.9.4/src/commands/plugins/install.ts)_
254+
_See
255+
code: [@oclif/plugin-plugins](https://github.com/oclif/plugin-plugins/blob/v3.10.1/src/commands/plugins/install.ts)_
252256

253257
## `codify plugins:link PLUGIN`
254258

@@ -278,7 +282,7 @@ EXAMPLES
278282
$ codify plugins:link myplugin
279283
```
280284

281-
_See code: [@oclif/plugin-plugins](https://github.com/oclif/plugin-plugins/blob/v3.9.4/src/commands/plugins/link.ts)_
285+
_See code: [@oclif/plugin-plugins](https://github.com/oclif/plugin-plugins/blob/v3.10.1/src/commands/plugins/link.ts)_
282286

283287
## `codify plugins:uninstall PLUGIN...`
284288

@@ -327,7 +331,7 @@ ALIASES
327331
```
328332

329333
_See
330-
code: [@oclif/plugin-plugins](https://github.com/oclif/plugin-plugins/blob/v3.9.4/src/commands/plugins/uninstall.ts)_
334+
code: [@oclif/plugin-plugins](https://github.com/oclif/plugin-plugins/blob/v3.10.1/src/commands/plugins/uninstall.ts)_
331335

332336
## `codify plugins:uninstall PLUGIN...`
333337

@@ -368,19 +372,20 @@ DESCRIPTION
368372
Update installed plugins.
369373
```
370374

371-
_See code: [@oclif/plugin-plugins](https://github.com/oclif/plugin-plugins/blob/v3.9.4/src/commands/plugins/update.ts)_
375+
_See code: [@oclif/plugin-plugins](https://github.com/oclif/plugin-plugins/blob/v3.10.1/src/commands/plugins/update.ts)_
372376

373377
## `codify uninstall`
374378

375379
describe the command here
376380

377381
```
378382
USAGE
379-
$ codify uninstall [--json] [-o plain|default|debug|json] [--debug]
383+
$ codify uninstall [--json] [--debug] [-o plain|default|debug|json] [-s]
380384
381385
FLAGS
382386
-o, --output=<option> [default: default]
383387
<options: plain|default|debug|json>
388+
-s, --secure
384389
--debug
385390
386391
GLOBAL FLAGS

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
"ajv": "^8.12.0",
1212
"ajv-formats": "^3.0.1",
1313
"chalk": "^5.3.0",
14-
"codify-schemas": "1.0.37",
14+
"codify-schemas": "1.0.38",
1515
"debug": "^4.3.4",
1616
"ink": "^4.4.1",
1717
"parse-json": "^8.1.0",

src/commands/apply/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export default class Apply extends BaseCommand {
3131

3232
const resolvedPath = path.resolve(flags.path ?? '.');
3333

34-
const planResult = await PlanOrchestrator.run(resolvedPath);
34+
const planResult = await PlanOrchestrator.run(resolvedPath, flags.secure);
3535
this.reporter.displayPlan(planResult.plan);
3636

3737
// Short circuit and exit if every change is NOOP

src/commands/plan/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export default class Plan extends BaseCommand {
3333

3434
const resolvedPath = path.resolve(flags.path ?? '.');
3535

36-
const { plan } = await PlanOrchestrator.run(resolvedPath);
36+
const { plan } = await PlanOrchestrator.run(resolvedPath, flags.secure);
3737
this.reporter.displayPlan(plan);
3838

3939
process.exit(0);

src/commands/uninstall.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { UninstallOrchestrator } from '../orchestrators/uninstall.js';
21
import { BaseCommand } from '../common/base-command.js';
2+
import { UninstallOrchestrator } from '../orchestrators/uninstall.js';
33

44
export default class Uninstall extends BaseCommand {
55
static description = 'describe the command here'
@@ -8,12 +8,10 @@ export default class Uninstall extends BaseCommand {
88
'<%= config.bin %> <%= command.id %>',
99
]
1010

11-
static flags = {}
12-
1311
static strict = false;
1412

1513
public async run(): Promise<void> {
16-
const { raw } = await this.parse(Uninstall)
14+
const { flags, raw } = await this.parse(Uninstall)
1715

1816
const args = raw
1917
.filter((r) => r.type === 'arg')
@@ -23,7 +21,7 @@ export default class Uninstall extends BaseCommand {
2321
throw new Error('A resource id must be specified for uninstall. Ex: "codify uninstall homebrew"')
2422
}
2523

26-
await UninstallOrchestrator.run(args);
24+
await UninstallOrchestrator.run(args, flags.secure);
2725

2826
process.exit(0);
2927
}

src/common/base-command.ts

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Command, Flags } from '@oclif/core';
22
import { FlagOutput } from '@oclif/core/lib/interfaces/parser.js';
33
import chalk from 'chalk';
4+
import { SudoRequestData } from 'codify-schemas';
45
import createDebug from 'debug';
56

67
import { ctx, Event } from '../events/context.js';
@@ -15,7 +16,11 @@ export abstract class BaseCommand extends Command {
1516
char: 'o',
1617
default: 'default',
1718
options: ['plain', 'default', 'debug', 'json'],
18-
})()
19+
})(),
20+
'secure': Flags.boolean({
21+
char: 's',
22+
default: false,
23+
})
1924
}
2025

2126
protected reporter!: Reporter;
@@ -31,9 +36,19 @@ export abstract class BaseCommand extends Command {
3136
const reporterType = this.getReporterType(flags);
3237
this.reporter = ReporterFactory.create(reporterType)
3338

34-
ctx.on(Event.SUDO_REQUEST, async (pluginName: string, command: string) => {
35-
await this.reporter.promptSudo(pluginName, command);
36-
ctx.sudoRequestGranted(pluginName);
39+
if (flags.secure) {
40+
console.log(chalk.blue('Running Codify in secure mode. Sudo will be prompted every time'));
41+
}
42+
43+
ctx.on(Event.SUDO_REQUEST, async (pluginName: string, data: SudoRequestData) => {
44+
try {
45+
const result = await this.reporter.promptSudo(pluginName, data, flags.secure);
46+
ctx.sudoRequestGranted(pluginName, result);
47+
48+
// This listener is outside of the base-command callstack. We have to manually catch the error.
49+
} catch (error) {
50+
this.catch(error as Error);
51+
}
3752
});
3853
}
3954

src/common/orchestrator.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@ import { ctx, SubProcessName } from '../events/context.js';
33
import { DependencyMap, PluginCollection } from '../plugins/plugin-collection.js';
44

55
export const CommonOrchestrator = {
6-
async initializePlugins(project?: Project): Promise<{
6+
async initializePlugins(project?: Project, secureMode = false): Promise<{
77
dependencyMap: DependencyMap
88
pluginCollection: PluginCollection,
99
}> {
1010
ctx.subprocessStarted(SubProcessName.INITIALIZE_PLUGINS)
1111
const pluginCollection = new PluginCollection();
12-
const dependencyMap = await pluginCollection.initialize(project);
12+
const dependencyMap = await pluginCollection.initialize(project, secureMode);
1313
ctx.subprocessFinished(SubProcessName.INITIALIZE_PLUGINS)
1414

1515
return { dependencyMap, pluginCollection };

src/events/context.ts

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,34 @@
1+
import type { SudoRequestData, SudoRequestResponseData } from 'codify-schemas';
2+
13
import { EventEmitter } from 'node:events';
24

35
export enum Event {
4-
STDOUT = 'stdout',
5-
STDERR = 'stderr',
6-
PLUGIN_STDOUT = 'plugin_stdout',
7-
PLUGIN_STDERR = 'plugin_stderr',
86
DEBUG = 'debug',
97
OUTPUT = 'output',
10-
PROCESS_START = 'process_start',
8+
PLUGIN_STDERR = 'plugin_stderr',
9+
PLUGIN_STDOUT = 'plugin_stdout',
1110
PROCESS_FINISH = 'process_finish',
12-
SUB_PROCESS_START = 'sub_process_start',
11+
PROCESS_START = 'process_start',
12+
STDERR = 'stderr',
13+
STDOUT = 'stdout',
1314
SUB_PROCESS_FINISH = 'sub_process_finish',
15+
SUB_PROCESS_START = 'sub_process_start',
1416
SUDO_REQUEST = 'sudo_request',
1517
SUDO_REQUEST_GRANTED = 'sudo_request_granted',
1618
}
1719

1820
export enum ProcessName {
19-
PLAN = 'plan',
2021
APPLY = 'apply',
22+
PLAN = 'plan',
2123
UNINSTALL = 'uninstall',
2224
}
2325

2426
export enum SubProcessName {
25-
PARSE = 'parse',
27+
APPLYING_RESOURCE = 'apply_resource',
28+
GENERATE_PLAN = 'generate_plan',
2629
INITIALIZE_PLUGINS = 'initialize_plugins',
30+
PARSE = 'parse',
2731
VALIDATE = 'validate',
28-
GENERATE_PLAN = 'generate_plan',
29-
APPLYING_RESOURCE = 'apply_resource',
3032
}
3133

3234
export const ctx = new class {
@@ -79,12 +81,12 @@ export const ctx = new class {
7981
this.emitter.emit(Event.SUB_PROCESS_FINISH, name, additionalName);
8082
}
8183

82-
sudoRequested(pluginName: string, command: string) {
83-
this.emitter.emit(Event.SUDO_REQUEST, pluginName, command);
84+
sudoRequested(pluginName: string, data: SudoRequestData) {
85+
this.emitter.emit(Event.SUDO_REQUEST, pluginName, data);
8486
}
8587

86-
sudoRequestGranted(pluginName: string) {
87-
this.emitter.emit(Event.SUDO_REQUEST_GRANTED, pluginName);
88+
sudoRequestGranted(pluginName: string, data: SudoRequestResponseData) {
89+
this.emitter.emit(Event.SUDO_REQUEST_GRANTED, pluginName, data);
8890
}
8991

9092
async subprocess<T>(name: string, run: () => Promise<T>): Promise<T> {

src/orchestrators/plan.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export interface PlanOrchestratorResponse {
1414
}
1515

1616
export const PlanOrchestrator = {
17-
async run(path: string): Promise<PlanOrchestratorResponse> {
17+
async run(path: string, secureMode: boolean): Promise<PlanOrchestratorResponse> {
1818
ctx.processStarted(ProcessName.PLAN)
1919

2020
ctx.subprocessStarted(SubProcessName.PARSE);
@@ -24,7 +24,7 @@ export const PlanOrchestrator = {
2424
project.addXCodeToolsConfig();
2525
ctx.subprocessFinished(SubProcessName.PARSE);
2626

27-
const { dependencyMap, pluginCollection } = await CommonOrchestrator.initializePlugins(project);
27+
const { dependencyMap, pluginCollection } = await CommonOrchestrator.initializePlugins(project, secureMode);
2828
await createStartupShellScriptsIfNotExists();
2929

3030
ctx.subprocessStarted(SubProcessName.VALIDATE)

src/orchestrators/uninstall.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import { PlanResponseData, ResourceOperation } from 'codify-schemas';
22
import { randomUUID } from 'node:crypto';
33

4+
import { CommonOrchestrator } from '../common/orchestrator.js';
45
import { ctx, ProcessName } from '../events/context.js';
56
import { createStartupShellScriptsIfNotExists } from '../utils/file.js';
6-
import { CommonOrchestrator } from '../common/orchestrator.js';
77

88
export const UninstallOrchestrator = {
9-
async run(typeIds: string[]): Promise<void> {
9+
async run(typeIds: string[], secureMode: boolean): Promise<void> {
1010
if (typeIds.length === 0) {
1111
return;
1212
}
@@ -18,7 +18,7 @@ export const UninstallOrchestrator = {
1818
resourceType: type,
1919
} as PlanResponseData))
2020

21-
const { pluginCollection } = await CommonOrchestrator.initializePlugins();
21+
const { pluginCollection } = await CommonOrchestrator.initializePlugins(undefined, secureMode);
2222
await createStartupShellScriptsIfNotExists();
2323

2424
ctx.processStarted(ProcessName.UNINSTALL);

0 commit comments

Comments
 (0)