Skip to content

Commit

Permalink
feat: Add support for custom propnames to retrieve secrets from other…
Browse files Browse the repository at this point in the history
… props than `password`
  • Loading branch information
stmh committed Sep 19, 2024
1 parent 337b320 commit 7e9b4be
Show file tree
Hide file tree
Showing 2 changed files with 175 additions and 28 deletions.
41 changes: 21 additions & 20 deletions src/Utilities/PasswordManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ private function getSecretImpl(ConfigurationService $configuration_service, $sec
$exceptions = [];

try {

// Check onepassword connect ...
if (!empty($secret_data['onePasswordVaultId']) && !empty($secret_data['onePasswordId'])) {
$configuration_service->getLogger()->debug(sprintf(
Expand All @@ -208,7 +209,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 +231,7 @@ 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 +316,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 +325,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 +374,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 +392,49 @@ 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
162 changes: 154 additions & 8 deletions tests/PasswordManagerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,16 @@
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;

class PasswordManagerTest extends PhabTestCase
{

protected $context;
protected TaskContext $context;

public function setup(): void
{
Expand Down Expand Up @@ -52,6 +46,158 @@ 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 7e9b4be

Please sign in to comment.