Skip to content

Commit

Permalink
Merge pull request #338 from factorial-io/custom-prop-names
Browse files Browse the repository at this point in the history
Custom prop names
  • Loading branch information
stmh authored Oct 9, 2024
2 parents e0ceade + a10bf68 commit 509226d
Show file tree
Hide file tree
Showing 3 changed files with 193 additions and 26 deletions.
16 changes: 16 additions & 0 deletions docs/passwords.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ secrets:
env: DB_PASSWORD
onePasswordId: 1234418718212s121
onePasswordVaultId: 768131213124
propName: credential # default propName is password
````


Expand Down Expand Up @@ -107,6 +108,21 @@ secrets:

Then just run your command as usual, phab will try to resolve the secret from 1password connect and use it.

### Different propnames than password

1Password allows to store more than a password, e.g. usernames, hostnames, etc. Per default phab will return `password`, but you can customize the returned propname:

```
secrets:
mysql-username:
question: Please provide the Mysql username for the cluster
onePasswordId: 1234418718212s121
onePasswordVaultId: 768131213124
tokenId: client-a
propName: username
```


## FTP-passwords

Previous versions of phabalicious supported a different mechanism to store ftp credentials in a local file. The local file is still supported, but the automatic retrieval is deprecated, please use the approach outlined above.
Expand Down
42 changes: 23 additions & 19 deletions src/Utilities/PasswordManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,8 @@ private function getSecretImpl(ConfigurationService $configuration_service, $sec
$secret_data['onePasswordVaultId'],
$secret_data['onePasswordId'],
$secret_data['tokenId'] ?? 'default',
$secret
$secret,
$secret_data['propName'] ?? 'password'
)) {
return $pw;
} else {
Expand All @@ -229,7 +230,10 @@ private function getSecretImpl(ConfigurationService $configuration_service, $sec
"Trying to get secret `%s` from 1password cli",
$secret
));
$pw = $this->getSecretFrom1PasswordCli($secret_data['onePasswordId']);
$pw = $this->getSecretFrom1PasswordCli(
$secret_data['onePasswordId'],
$secret_data['propName'] ?? 'password'
);
if ($pw) {
return $pw;
}
Expand Down Expand Up @@ -314,7 +318,7 @@ private function exec1PasswordCli($cmd_v1, $cmd_v2)
return new CommandResult($result_code, $output);
}

private function getSecretFrom1PasswordCli($item_id)
private function getSecretFrom1PasswordCli($item_id, $prop_name)
{
$result = $this->exec1PasswordCli(
sprintf("get item %s", $item_id),
Expand All @@ -323,7 +327,7 @@ private function getSecretFrom1PasswordCli($item_id)

if ($result && $result->succeeded()) {
$payload = implode("\n", $result->getOutput());
return $this->extractSecretFrom1PasswordPayload($payload, $this->get1PasswordCliVersion());
return $this->extractSecretFrom1PasswordPayload($payload, $this->get1PasswordCliVersion(), $prop_name);
}
$result->throwException("1Password returned an error, are you logged in?");
}
Expand Down Expand Up @@ -372,12 +376,12 @@ private function get1PasswordConnectResponse($token_id, $url)
return false;
}

private function getSecretFrom1PasswordConnect($vault_id, $item_id, $token_id, $secret_name)
private function getSecretFrom1PasswordConnect($vault_id, $item_id, $token_id, $secret_name, $prop_name)
{
try {
$response = $this->get1PasswordConnectResponse($token_id, "/v1/vaults/$vault_id/items/$item_id");
if ($response) {
return $this->extractSecretFrom1PasswordPayload((string) $response->getBody(), false);
return $this->extractSecretFrom1PasswordPayload((string) $response->getBody(), false, $prop_name);
}
} catch (\Exception $exception) {
throw new \RuntimeException(
Expand All @@ -390,50 +394,50 @@ private function getSecretFrom1PasswordConnect($vault_id, $item_id, $token_id, $
return false;
}

private function extractFieldsHelper($fields)
private function extractFieldsHelper($fields, $prop_name)
{

foreach ($fields as $field) {
if (!empty($field->designation) && $field->designation === 'password') {
if (!empty($field->id) && $field->id === $prop_name) {
/** @phpstan-ignore-next-line */
return $field->value;
}
if (!empty($field->purpose) && $field->purpose === 'PASSWORD') {
// Support for field in sections.
if (!empty($field->n) && $field->n === $prop_name) {
/** @phpstan-ignore-next-line */
return $field->value;
return $field->v;
}
if (!empty($field->id) && $field->id === 'password') {
if (!empty($field->designation) && $field->designation === 'password') {
/** @phpstan-ignore-next-line */
return $field->value;
}
// Support for field in sections.
if (!empty($field->n) && $field->n === 'password') {
if (!empty($field->purpose) && $field->purpose === 'PASSWORD') {
/** @phpstan-ignore-next-line */
return $field->v;
return $field->value;
}
}
return false;
}

public function extractSecretFrom1PasswordPayload($payload, $cli_version)
public function extractSecretFrom1PasswordPayload($payload, $cli_version, $prop_name)
{
$json = json_decode($payload);
if ($json) {
if ($cli_version === 1) {
$json = $json->details;
}
if (!empty($json->password)) {
return $json->password;
if (!empty($json->{$prop_name})) {
return $json->{$prop_name};
}
if (!empty($json->sections)) {
foreach ($json->sections as $section) {
if (isset($section->fields) && $result = $this->extractFieldsHelper($section->fields)) {
if (isset($section->fields) && $result = $this->extractFieldsHelper($section->fields, $prop_name)) {
return $result;
}
}
}
if (!empty($json->fields)) {
return $this->extractFieldsHelper($json->fields);
return $this->extractFieldsHelper($json->fields, $prop_name);
}
}

Expand Down
161 changes: 154 additions & 7 deletions tests/PasswordManagerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,8 @@
namespace Phabalicious\Tests;

use Phabalicious\Command\BaseCommand;
use Phabalicious\Configuration\Storage\Node;
use Phabalicious\Configuration\Storage\Store;
use Phabalicious\Exception\ArgumentParsingException;
use Phabalicious\Method\TaskContext;
use Phabalicious\Utilities\PasswordManager;
use Phabalicious\Utilities\Utilities;
use PHPUnit\Framework\TestCase;
use Prophecy\Argument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
Expand Down Expand Up @@ -52,6 +46,159 @@ public function test1PasswordPayload()
$mng = new PasswordManager();
$mng->setContext($this->context);

$this->assertEquals("my-very-special-secret", $mng->extractSecretFrom1PasswordPayload($payload, 1));
$this->assertEquals("my-very-special-secret", $mng->extractSecretFrom1PasswordPayload($payload, 1, 'password'));
}

public function test1PasswordCustomPropName()
{
$payload = <<<JSON
{
"additionalInformation": "bearer",
"category": "API_CREDENTIAL",
"createdAt": "2024-09-09T09:19:04Z",
"fields": [
{
"id": "notesPlain",
"label": "notesPlain",
"purpose": "NOTES",
"type": "STRING"
},
{
"id": "username",
"label": "Benutzername",
"type": "STRING",
"value": "MM_ACCESS_TOKEN"
},
{
"id": "credential",
"label": "Anmeldedaten",
"type": "CONCEALED",
"value": "zackbummpeng"
},
{
"id": "type",
"label": "Typ",
"type": "MENU",
"value": "bearer"
},
{
"id": "filename",
"label": "Dateiname",
"type": "STRING"
},
{
"id": "validFrom",
"label": "Gültig ab",
"type": "DATE"
},
{
"id": "expires",
"label": "Gültig bis",
"type": "DATE"
},
{
"id": "hostname",
"label": "Host-Name",
"type": "STRING",
"value": "a.simple.domain.name"
}
],
"id": "lajdlahdldjh",
"lastEditedBy": "jjhkjhk",
"title": "Mattermost DEV: API Admin Access Token",
"updatedAt": "2024-09-09T09:19:41Z",
"vault": {
"id": "lakjdladkj",
"name": "Some Vault"
},
"version": 2
}
JSON;
$mng = new PasswordManager();
$mng->setContext($this->context);

$this->assertEquals("zackbummpeng", $mng->extractSecretFrom1PasswordPayload($payload, 0, 'credential'));
$this->assertEquals("MM_ACCESS_TOKEN", $mng->extractSecretFrom1PasswordPayload($payload, 0, 'username'));
}

public function test1PasswordCliCustomPropName()
{
$payload = <<<JSON
{
"id": "SOME_UUID_WHATEVER",
"title": "Mattermost DEV: API Admin Access Token",
"version": 2,
"vault": {
"id": "bvjl7wmmyqdw37vkt7ldoixovm",
"name": "FooBar Name"
},
"category": "API_CREDENTIAL",
"last_edited_by": "GIQ64YLYRZECLEBAEJ6GF25G74",
"created_at": "2024-09-09T09:19:04Z",
"updated_at": "2024-09-09T09:19:41Z",
"additional_information": "bearer",
"fields": [
{
"id": "notesPlain",
"type": "STRING",
"purpose": "NOTES",
"label": "notesPlain",
"reference": "op://FooBar Name/SOME_UUID_WHATEVER/notesPlain"
},
{
"id": "username",
"type": "STRING",
"label": "Benutzername",
"value": "MM_ACCESS_TOKEN",
"reference": "op://FooBar Name/SOME_UUID_WHATEVER/Benutzername"
},
{
"id": "credential",
"type": "CONCEALED",
"label": "Anmeldedaten",
"value": "zackbummpeng",
"reference": "op://FooBar Name/SOME_UUID_WHATEVER/Anmeldedaten"
},
{
"id": "type",
"type": "MENU",
"label": "Typ",
"value": "bearer",
"reference": "op://FooBar Name/SOME_UUID_WHATEVER/Typ"
},
{
"id": "filename",
"type": "STRING",
"label": "Dateiname",
"reference": "op://FooBar Name/SOME_UUID_WHATEVER/Dateiname"
},
{
"id": "validFrom",
"type": "DATE",
"label": "Gültig ab",
"reference": "op://FooBar Name/SOME_UUID_WHATEVER/validFrom"
},
{
"id": "expires",
"type": "DATE",
"label": "Gültig bis",
"reference": "op://FooBar Name/SOME_UUID_WHATEVER/expires"
},
{
"id": "hostname",
"type": "STRING",
"label": "Host-Name",
"value": "a.simple.domain.name",
"reference": "op://FooBar Name/SOME_UUID_WHATEVER/Host-Name"
}
]
}
JSON;

$mng = new PasswordManager();
$mng->setContext($this->context);

$this->assertEquals("zackbummpeng", $mng->extractSecretFrom1PasswordPayload($payload, 2, 'credential'));
$this->assertEquals("MM_ACCESS_TOKEN", $mng->extractSecretFrom1PasswordPayload($payload, 2, 'username'));
}
}

0 comments on commit 509226d

Please sign in to comment.