Skip to content

Commit 0de0b14

Browse files
authored
Adds 1password as a secret provider (#181)
1 parent 31291fc commit 0de0b14

13 files changed

Lines changed: 906 additions & 0 deletions

File tree

docs/pages/variables.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ Here is an example JSON demonstrating the structure of the `variableProviders` p
9090

9191
| Type | Descriptions | Docs |
9292
| :------------- | ------------------------------------------------------ | ----------------------------------- |
93+
| onepassword | Variables stored in a 1Password vault | [Docs↗](./variables/1password) |
9394
| azure-keyvault | Variables stored in an Azure KeyVault | [Docs↗](./variables/azure-keyvault) |
9495
| git | fetch variables stored in a separate git repository | [Docs↗](./variables/git) |
9596
| local | reference local variables stored in a `json` file | [Docs↗](./variables/local) |

docs/pages/variables/1password.mdx

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import { Callout } from "nextra/components";
2+
3+
# 1Password
4+
5+
Use this provider to store and resolve variables from a [1Password](https://1password.com) vault using service account tokens.
6+
7+
<Callout type="info">
8+
Variable paths use `item.field` format, split on the first dot. Item names in
9+
1Password should not contain dots.
10+
</Callout>
11+
12+
## Prerequisites
13+
14+
The [1Password CLI (`op`)](https://developer.1password.com/docs/cli/get-started/) must be installed and available in your PATH.
15+
16+
## Configuration
17+
18+
| Field | Value |
19+
| :------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
20+
| vault | The name of the 1Password vault to use |
21+
| serviceAccountToken | Optional. Environment variable reference (e.g. `$OP_SERVICE_ACCOUNT_TOKEN`) or literal token. If omitted, Confix falls back to the signed-in `op` CLI session |
22+
| account | Optional. The 1Password account ID or sign-in address. Useful when multiple accounts are signed in to the `op` CLI |
23+
24+
**Sample**
25+
26+
```json copy filename=".confixrc"
27+
{
28+
"project": {
29+
"variableProviders": [
30+
{
31+
"name": "op",
32+
"type": "onepassword",
33+
"vault": "Infrastructure"
34+
}
35+
]
36+
}
37+
}
38+
```
39+
40+
**Environment Override**
41+
42+
```json copy filename=".confixrc"
43+
{
44+
"project": {
45+
"variableProviders": [
46+
{
47+
"name": "op",
48+
"type": "onepassword",
49+
"vault": "Development",
50+
"serviceAccountToken": "$OP_SERVICE_ACCOUNT_TOKEN",
51+
"environmentOverride": {
52+
"production": {
53+
"vault": "Production",
54+
"serviceAccountToken": "$OP_PROD_TOKEN"
55+
}
56+
}
57+
}
58+
]
59+
}
60+
}
61+
```
62+
63+
## Authentication
64+
65+
Confix supports two authentication modes:
66+
67+
1. **Service account token** (recommended for CI/automation). Set `OP_SERVICE_ACCOUNT_TOKEN` in your environment, or configure a custom env var reference via `serviceAccountToken`. The token is passed to the `op` CLI via the child process environment - it never appears in config files or process arguments. See [service account tokens](https://developer.1password.com/docs/service-accounts/get-started/).
68+
2. **Existing `op` CLI session** (convenient for local development). If `serviceAccountToken` is omitted and `OP_SERVICE_ACCOUNT_TOKEN` is not set, Confix uses your already-signed-in `op` CLI session. Enable the [1Password desktop app integration with the CLI](https://developer.1password.com/docs/cli/app-integration/) so the CLI can authenticate through the app, and Confix will work without any extra configuration.
69+
70+
The service account requires the following permissions on the configured vault:
71+
72+
| Action | Required Permission |
73+
| :------------------ | ------------------------------ |
74+
| confix restore | Read items in vault |
75+
| confix build | Read items in vault |
76+
| confix variable set | Read and write items in vault |
77+
| confix variable get | Read items in vault |
78+
79+
## Variable Path Format
80+
81+
Paths follow the format `item[.field]`, where `item` is either the 1Password item **title** or its unique **ID**, and `field` is the field label. The path is split on the first dot. If `field` is omitted, it defaults to `password`. For example, with a provider named `op` and vault `Development`:
82+
83+
- `$op:database.password` resolves to `op://Development/database/password` (by title)
84+
- `$op:database` resolves to the same thing (field defaults to `password`)
85+
- `$op:api.secret-key` resolves to `op://Development/api/secret-key` (by title)
86+
- `$op:xwjvejqflgo6oj4f4gtqcchany.password` resolves the same item by its ID
87+
88+
### Title vs. ID
89+
90+
Both forms work interchangeably - use whichever fits your workflow:
91+
92+
| Form | When to use |
93+
| :----- | ------------------------------------------------------------------------------------------------------------------- |
94+
| Title | Exploration, prototyping, or when the item name is stable. Readable and discoverable in the vault UI. |
95+
| ID | Committed configuration. Stable across item renames and unambiguous when multiple items share a similar title. |
96+
97+
<Callout type="warning">
98+
Titles must be unique (case-insensitive) within the vault when referenced by title. If two items share a case-insensitive title, `op` cannot disambiguate by title - use the item ID instead.
99+
</Callout>
100+
101+
<Callout type="warning">
102+
Variable paths only support word characters (letters, digits, underscores) and dots. If your item title contains spaces, hyphens, or other punctuation, reference it by ID. You can find an item's ID via `op item list --vault <vault>` or from the item's URL in the 1Password app.
103+
</Callout>
104+
105+
`confix variable list` emits one entry per item (both its title and ID) for each vault. Append `.field` in your configuration to target a specific field; omit it to use the default `password` field.
106+
107+
## Troubleshooting
108+
109+
**`op` CLI not installed**
110+
Confix requires the 1Password CLI to interact with vaults. Install it from the [1Password CLI documentation](https://developer.1password.com/docs/cli/get-started/).
111+
112+
**Not signed in**
113+
Either enable the [1Password desktop app integration with the CLI](https://developer.1password.com/docs/cli/app-integration/) so the `op` CLI can authenticate through the desktop app, or export a service account token:
114+
```bash
115+
export OP_SERVICE_ACCOUNT_TOKEN="your-token-here"
116+
```
117+
118+
**Vault not found**
119+
Verify the vault name matches exactly and that the service account has been granted access to that vault.
120+
121+
**Authentication failed**
122+
Check that the service account token is valid and has not expired. Tokens can be managed in the 1Password admin console.

src/Confix.Tool/src/Confix.Library/Configuration/JsonContext.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ namespace Confix.Extensions;
1212
[JsonSourceGenerationOptions(WriteIndented = true)]
1313
[JsonSerializable(typeof(LocalVariableProviderConfiguration))]
1414
[JsonSerializable(typeof(AzureKeyVaultProviderConfiguration))]
15+
[JsonSerializable(typeof(OnePasswordProviderConfiguration))]
1516
[JsonSerializable(typeof(SecretVariableProviderConfiguration))]
1617
[JsonSerializable(typeof(GitVariableProviderConfiguration))]
1718
[JsonSerializable(typeof(GitComponentProviderConfiguration))]

src/Confix.Tool/src/Confix.Library/Middlewares/Variables/VariableCommandBuilderExtensions.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ private static CommandLineBuilder AddDefaultVariableProviders(this CommandLineBu
2828
builder.AddVariableProvider(config => new AzureKeyVaultProvider(config));
2929
builder.AddVariableProvider((sp, config)
3030
=> new GitVariableProvider(sp.GetRequiredService<IGitService>(), config));
31+
builder.AddVariableProvider(config => new OnePasswordProvider(config));
3132

3233
return builder;
3334
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
namespace Confix.Variables;
2+
3+
public interface IOnePasswordCli
4+
{
5+
Task<string> ReadAsync(
6+
string vault,
7+
string item,
8+
string field,
9+
CancellationToken cancellationToken);
10+
11+
Task<IReadOnlyList<OnePasswordItemSummary>> ListItemsAsync(
12+
string vault,
13+
CancellationToken cancellationToken);
14+
15+
Task EditItemFieldAsync(
16+
string vault,
17+
string item,
18+
string field,
19+
string value,
20+
CancellationToken cancellationToken);
21+
22+
Task CreateItemAsync(
23+
string vault,
24+
string item,
25+
string field,
26+
string value,
27+
CancellationToken cancellationToken);
28+
}
29+
30+
public record OnePasswordItemSummary(string Id, string Title);
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
using System.Diagnostics;
2+
using System.Text.Json;
3+
using System.Text.Json.Nodes;
4+
using Confix.Tool;
5+
6+
namespace Confix.Variables;
7+
8+
public sealed class OnePasswordCli : IOnePasswordCli
9+
{
10+
private const string DefaultServiceAccountTokenEnvVar = "OP_SERVICE_ACCOUNT_TOKEN";
11+
private const string AccountEnvVar = "OP_ACCOUNT";
12+
13+
private readonly string? _serviceAccountToken;
14+
private readonly string? _account;
15+
private string? _resolvedToken;
16+
private bool _tokenResolved;
17+
18+
public OnePasswordCli(string? serviceAccountToken, string? account = null)
19+
{
20+
_serviceAccountToken = serviceAccountToken;
21+
_account = account;
22+
}
23+
24+
public async Task<string> ReadAsync(
25+
string vault,
26+
string item,
27+
string field,
28+
CancellationToken cancellationToken
29+
)
30+
{
31+
var result = await ExecuteAsync(
32+
new[] { "read", $"op://{vault}/{item}/{field}" },
33+
cancellationToken
34+
);
35+
36+
return result.Trim();
37+
}
38+
39+
public async Task<IReadOnlyList<OnePasswordItemSummary>> ListItemsAsync(
40+
string vault,
41+
CancellationToken cancellationToken
42+
)
43+
{
44+
var json = await ExecuteAsync(
45+
new[] { "item", "list", "--vault", vault, "--format", "json", "--no-color" },
46+
cancellationToken
47+
);
48+
49+
var items = JsonSerializer.Deserialize<JsonArray>(json) ?? new JsonArray();
50+
var result = new List<OnePasswordItemSummary>();
51+
foreach (var item in items)
52+
{
53+
if (item is null)
54+
{
55+
continue;
56+
}
57+
58+
var id = item["id"]?.GetValue<string>() ?? string.Empty;
59+
var title = item["title"]?.GetValue<string>() ?? string.Empty;
60+
result.Add(new OnePasswordItemSummary(id, title));
61+
}
62+
63+
return result;
64+
}
65+
66+
public async Task EditItemFieldAsync(
67+
string vault,
68+
string item,
69+
string field,
70+
string value,
71+
CancellationToken cancellationToken
72+
)
73+
{
74+
await ExecuteAsync(
75+
new[] { "item", "edit", item, "--vault", vault, $"{field}={value}" },
76+
cancellationToken
77+
);
78+
}
79+
80+
public async Task CreateItemAsync(
81+
string vault,
82+
string item,
83+
string field,
84+
string value,
85+
CancellationToken cancellationToken
86+
)
87+
{
88+
await ExecuteAsync(
89+
new[]
90+
{
91+
"item",
92+
"create",
93+
"--vault",
94+
vault,
95+
"--title",
96+
item,
97+
"--category",
98+
"login",
99+
$"{field}={value}",
100+
},
101+
cancellationToken
102+
);
103+
}
104+
105+
private async Task<string> ExecuteAsync(string[] args, CancellationToken cancellationToken)
106+
{
107+
var token = ResolveToken();
108+
109+
var startInfo = new ProcessStartInfo
110+
{
111+
FileName = "op",
112+
RedirectStandardOutput = true,
113+
RedirectStandardError = true,
114+
UseShellExecute = false,
115+
CreateNoWindow = true,
116+
};
117+
118+
foreach (var arg in args)
119+
{
120+
startInfo.ArgumentList.Add(arg);
121+
}
122+
123+
if (token is not null)
124+
{
125+
startInfo.Environment[DefaultServiceAccountTokenEnvVar] = token;
126+
}
127+
128+
if (!string.IsNullOrWhiteSpace(_account))
129+
{
130+
startInfo.Environment[AccountEnvVar] = _account;
131+
}
132+
133+
using var process =
134+
Process.Start(startInfo)
135+
?? throw new InvalidOperationException("Failed to start op process.");
136+
137+
try
138+
{
139+
var stdoutTask = process.StandardOutput.ReadToEndAsync(cancellationToken);
140+
var stderrTask = process.StandardError.ReadToEndAsync(cancellationToken);
141+
142+
await process.WaitForExitAsync(cancellationToken);
143+
144+
var stdout = await stdoutTask;
145+
var stderr = await stderrTask;
146+
147+
if (process.ExitCode != 0)
148+
{
149+
throw new OnePasswordCliException(process.ExitCode, stderr.Trim());
150+
}
151+
152+
return stdout;
153+
}
154+
catch (OperationCanceledException)
155+
{
156+
if (!process.HasExited)
157+
{
158+
process.Kill(entireProcessTree: true);
159+
}
160+
throw;
161+
}
162+
}
163+
164+
private string? ResolveToken()
165+
{
166+
if (_tokenResolved)
167+
{
168+
return _resolvedToken;
169+
}
170+
171+
_resolvedToken = ResolveTokenCore();
172+
_tokenResolved = true;
173+
return _resolvedToken;
174+
}
175+
176+
private string? ResolveTokenCore()
177+
{
178+
if (_serviceAccountToken is null)
179+
{
180+
return Environment.GetEnvironmentVariable(DefaultServiceAccountTokenEnvVar);
181+
}
182+
183+
if (_serviceAccountToken.StartsWith('$'))
184+
{
185+
var envVarName = _serviceAccountToken[1..];
186+
return Environment.GetEnvironmentVariable(envVarName)
187+
?? throw new ExitException($"Environment variable '{envVarName}' is not set.")
188+
{
189+
Help =
190+
"Set the environment variable, provide the token directly in the provider configuration, or remove the serviceAccountToken setting to use your existing 'op' CLI session.",
191+
};
192+
}
193+
194+
return _serviceAccountToken;
195+
}
196+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
namespace Confix.Variables;
2+
3+
public sealed class OnePasswordCliException : Exception
4+
{
5+
public int ExitCode { get; }
6+
7+
public OnePasswordCliException(int exitCode, string message)
8+
: base(message)
9+
{
10+
ExitCode = exitCode;
11+
}
12+
}

0 commit comments

Comments
 (0)