Skip to content

Commit d838f40

Browse files
committed
feat: add OpenBaoSecretStore with token and AppRole auth
Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com>
1 parent 2b13973 commit d838f40

1 file changed

Lines changed: 110 additions & 0 deletions

File tree

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
<?php
2+
3+
// SPDX-FileCopyrightText: 2026 LibreCode coop and contributors
4+
// SPDX-License-Identifier: AGPL-3.0-or-later
5+
6+
declare(strict_types=1);
7+
8+
namespace LibreCodeCoop\NfsePHP\SecretStore;
9+
10+
use LibreCodeCoop\NfsePHP\Contracts\SecretStoreInterface;
11+
use LibreCodeCoop\NfsePHP\Exception\SecretStoreException;
12+
use Vault\Client;
13+
use Vault\AuthenticationStrategies\AppRoleAuthenticationStrategy;
14+
use Vault\AuthenticationStrategies\TokenAuthenticationStrategy;
15+
16+
/**
17+
* OpenBao / HashiCorp Vault KV v2 secret store.
18+
*
19+
* Supports two authentication methods:
20+
* - Token (for development/CI): pass $token
21+
* - AppRole (for production): pass $roleId + $secretId
22+
*
23+
* Secrets are stored under: {$mount}/data/{$path}
24+
*/
25+
class OpenBaoSecretStore implements SecretStoreInterface
26+
{
27+
private readonly Client $vault;
28+
private readonly string $mount;
29+
30+
public function __construct(
31+
private readonly string $addr,
32+
private readonly string $mount = 'nfse',
33+
private readonly ?string $token = null,
34+
private readonly ?string $roleId = null,
35+
private readonly ?string $secretId = null,
36+
private readonly ?string $namespace = null,
37+
) {
38+
if ($this->token === null && ($this->roleId === null || $this->secretId === null)) {
39+
throw new SecretStoreException('Either $token or ($roleId + $secretId) must be provided.');
40+
}
41+
42+
$this->vault = $this->buildClient();
43+
}
44+
45+
/**
46+
* @return array<string, string>
47+
*/
48+
public function get(string $path): array
49+
{
50+
try {
51+
$response = $this->vault->read($this->kvPath($path));
52+
/** @var array<string, string> $data */
53+
$data = $response->getData()['data'] ?? [];
54+
55+
return $data;
56+
} catch (\Throwable $e) {
57+
throw new SecretStoreException('Failed to read secret at path "' . $path . '": ' . $e->getMessage(), previous: $e);
58+
}
59+
}
60+
61+
/**
62+
* @param array<string, string> $data
63+
*/
64+
public function put(string $path, array $data): void
65+
{
66+
try {
67+
$this->vault->write($this->kvPath($path), ['data' => $data]);
68+
} catch (\Throwable $e) {
69+
throw new SecretStoreException('Failed to write secret at path "' . $path . '": ' . $e->getMessage(), previous: $e);
70+
}
71+
}
72+
73+
public function delete(string $path): void
74+
{
75+
try {
76+
$this->vault->revoke($this->kvPath($path));
77+
} catch (\Throwable $e) {
78+
throw new SecretStoreException('Failed to delete secret at path "' . $path . '": ' . $e->getMessage(), previous: $e);
79+
}
80+
}
81+
82+
// -------------------------------------------------------------------------
83+
84+
private function kvPath(string $path): string
85+
{
86+
// KV v2 API uses the "data" sub-path for reads/writes
87+
return $this->mount . '/data/' . ltrim($path, '/');
88+
}
89+
90+
private function buildClient(): Client
91+
{
92+
$client = new Client($this->addr);
93+
94+
if ($this->namespace !== null) {
95+
$client->setNamespace($this->namespace);
96+
}
97+
98+
if ($this->token !== null) {
99+
$client->setAuthenticationStrategy(new TokenAuthenticationStrategy($this->token));
100+
} else {
101+
$client->setAuthenticationStrategy(
102+
new AppRoleAuthenticationStrategy($this->roleId, $this->secretId)
103+
);
104+
}
105+
106+
$client->authenticate();
107+
108+
return $client;
109+
}
110+
}

0 commit comments

Comments
 (0)