Below are two extension classes which allow our users to lease and execute commands on drydock machines. This is highly useful for scripting, and looks basically like:
- `arc lease --blueprint 14` (assuming you are authorized to use the blueprint already)
- `cat myscript.sh | ssh phgit@phabricator.uberspaceships.com bash --lease 99`
We do this to allow external systems to interface with drydock machines, especially in the case of extremely long running commands.
We only let people lease stuff from blueprints which create brand new virtual machines on each request, so they are never reusing a machine from a previous user.
The only obvious security "problem" I see so far is that the virtual machine is authenticated against phabricator as a bot user when it's created (it must be in order to clone the repository) , but we already have this problem in harbormaster anyways.
I'm more interested in discovering if there are potentially other ways that my methodology could have unintended consequences.
This is still an experimental service, but people are liking it quite a bit.
```lang=php
<?php
final class CIDrydockBashSSHWorkflow extends PhabricatorSSHWorkflow {
protected function didConstruct() {
$this->setName('bash');
$this->setArguments(
array(
array(
'name' => 'lease',
'param' => 'id',
),
));
}
public function execute(PhutilArgumentParser $args) {
$user = $this->getUser();
$lease = (new DrydockLeaseQuery())
->setViewer($user)
->withIDs([$args->getArg('lease')])
->executeOne();
if ($lease->getOwnerPHID() !== $user->getPHID()) {
throw new Exception('You do not own this lease.');
}
if (!$lease->isActive()) {
throw new Exception('Lease is not active.');
}
$interface = $lease->getInterface(DrydockCommandInterface::INTERFACE_TYPE);
$future = $interface->getExecFuture('bash');
$err = $this->newPassthruCommand()
->setIOChannel($this->getIOChannel())
->setCommandChannelFromExecFuture($future)
->execute();
return $err;
}
}
```
```lang=php
<?php
final class ICLeaseWorkflow extends ICDrydockWorkflow {
public function getWorkflowName() {
return 'lease';
}
public function getCommandSynopses() {
return phutil_console_format(<<<EOTEXT
**lease** [__options__]
EOTEXT
);
}
public function getCommandHelp() {
return phutil_console_format(<<<EOTEXT
Lease resources from drydock.
EOTEXT
);
}
public function getArguments() {
return [
'blueprint' => [
'help' => pht('ID of the blueprint to use when acquiring a lease.'),
'param' => 'id',
],
'*' => 'attributes',
];
}
public function requiresAuthentication() {
return true;
}
public function run() {
if (!$this->getPassedArguments()) {
$this->listBlueprints();
$this->listLeases();
return 0;
}
$attributes = $this->getArgument('attributes');
if (count($attributes) != 1) {
throw new ArcanistUsageException('You must provide lease attributes.');
}
$attributes = phutil_json_decode(Filesystem::readFile(head($attributes)));
if (!$blueprint_id = $this->getArgument('blueprint')) {
throw new ArcanistUsageException('You must provide a blueprint ID.');
}
$blueprint = $this->searchMethodForID(
'drydock.blueprint.search',
$blueprint_id);
try {
$lease = $this->getConduit()->callMethodSynchronous('drydock.lease.create', [
'blueprintPHID' => $blueprint['phid'],
'attributes' => $attributes,
]);
} catch (ConduitClientException $e) {
if ($e->getErrorCode() === 'ERR_NOT_AUTHORIZED') {
if (phutil_console_confirm(pht(
'You are not authorized to use this blueprint, request authorization?'))) {
$this->requestAuthorization($blueprint['phid']);
return 1;
}
}
}
return 0;
}
private function requestAuthorization($blueprint_phid) {
$console = PhutilConsole::getConsole();
$authorization = $this->getConduit()->callMethodSynchronous('drydock.authorization.request', [
'blueprintPHID' => $blueprint_phid,
]);
echo phutil_console_format(
"%s\n **%s** __%s__\n\n",
pht('Authorization request issued.'),
pht('Request URI:'),
rtrim($this->getConfigFromAnySource('phabricator.uri'), '/').'/'.
'drydock/authorization/'.$authorization['id'].'/');
}
private function listBlueprints() {
$console = PhutilConsole::getConsole();
$console->writeOut("<bg:blue> %s </bg>\n", pht('BLUEPRINTS'));
$blueprints = $this->getConduit()->callMethodSynchronous('drydock.blueprint.search', [
'constraints' => [
'isDisabled' => false,
],
'attachments' => [
'authorizations' => true,
],
]);
if (!$blueprints = idx($blueprints, 'data')) {
$console->writeOut("\t%s\n", pht('No blueprints found.'));
}
$table = (new PhutilConsoleTable())
->addColumn('id', ['title' => 'ID'])
->addColumn('phid', ['title' => 'PHID'])
->addColumn('name', ['title' => 'Name'])
->addColumn('type', ['title' => 'Type'])
->addColumn('authorized', ['title' => 'Authorized'])
->setBorders(true);
foreach ($blueprints as $blueprint) {
$is_authorized = in_array(
$this->getUserPHID(),
idxv($blueprint, ['attachments', 'authorizations']));
$table->addRow([
'id' => idx($blueprint, 'id'),
'phid' => idx($blueprint, 'phid'),
'name' => idxv($blueprint, ['fields', 'name']),
'type' => idxv($blueprint, ['fields', 'type']),
'authorized' => $is_authorized ? pht('Yes') : pht('No'),
]);
}
$table->draw();
}
}
```