Skip to content

Commit 9e79919

Browse files
authored
Merge pull request #7 from phasehq/feat--secret-referencing
Feat secret referencing
2 parents f467c35 + 0b2e35b commit 9e79919

File tree

4 files changed

+184
-10
lines changed

4 files changed

+184
-10
lines changed

README.md

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,73 @@
22

33
The Phase Secrets SDK provides a Go package for managing secrets in your application environments using the Phase service. This SDK let's you create, retrieve, update, and delete secrets, with end-to-end encryption with just a few lines of code.
44

5+
## Features:
6+
7+
- End-to-end encrypting secret CRUD
8+
- Cross and local env secret referencing
9+
- Built in handling of rate limiting
10+
11+
### Secret referencing syntax:
12+
13+
| Reference syntax | Environment | Path | Secret Key Being Referenced | Description |
14+
| --------------------------------- | ---------------- | --------------------------------- | --------------------------- | ------------------------------------------------------------------ |
15+
| `${KEY}` | same environment | `/ | KEY | Local reference in the same environment and path root (/). |
16+
| `${staging.DEBUG}` | `dev` | `/` (root of staging environment) | DEBUG | Cross-environment reference to a secret at the root (/). |
17+
| `${prod./frontend/SECRET_KEY}` | `prod` | `/frontend/` | SECRET_KEY | Cross-environment reference to a secret in a specific path. |
18+
| `${/backend/payments/STRIPE_KEY}` | same environment | `/backend/payments/` | STRIPE_KEY | Local reference with a specified path within the same environment. |
19+
520
## Installation
621

7-
To start using the Phase SDK in your Go project, install it using `go get`:
22+
This SDK uses the `sodium` package to perform cryptographic operations, on most system you will need to install the `libsodium` library as a system dependency. Here's how you can install `libsodium` or its development packages on different platforms, including macOS, Ubuntu, Debian, Arch Linux, Alpine Linux, and Windows.
23+
24+
### macOS
25+
26+
```sh
27+
brew install libsodium
28+
```
29+
30+
## Fedora
31+
32+
```sh
33+
sudo dnf install libsodium-devel
34+
```
35+
36+
### Ubuntu and Debian
37+
38+
```sh
39+
sudo apt-get update && sudo apt-get install libsodium-dev
40+
```
41+
42+
### Arch Linux
43+
44+
```sh
45+
sudo pacman -Syu libsodium
46+
```
47+
48+
### Alpine Linux
49+
50+
```sh
51+
sudo apk add libsodium-dev
52+
```
53+
54+
### Windows
55+
56+
On Windows, the process is a bit different due to the variety of development environments. However, you can download pre-built binaries from the official [libsodium GitHub releases page](https://github.com/jedisct1/libsodium/releases). Choose the appropriate version for your system architecture (e.g., Win32 or Win64), download it, and follow the instructions included to integrate `libsodium` with your development environment. For development with Visual Studio, you'll typically include the header files and link against the `libsodium.lib` or `libsodium.dll` file.
57+
58+
If you're using a package manager like `vcpkg` or `chocolatey`, you can also find `libsodium` packages available for installation:
59+
60+
- Using `vcpkg`:
61+
```sh
62+
vcpkg install libsodium
63+
```
64+
- Using `chocolatey`:
65+
```sh
66+
choco install libsodium
67+
```
68+
69+
Remember, after installing the library, you might need to configure your project or environment variables to locate the `libsodium` libraries correctly, especially on Windows.
70+
71+
Next, start using the Phase SDK in your Go project, install it using `go get`:
872

973
```bash
1074
go get github.com/phasehq/golang-sdk/phase
@@ -116,6 +180,6 @@ if err != nil {
116180
}
117181
```
118182

119-
For more information and advanced usage, refer to the official Phase documentation.
183+
For more information and advanced usage, refer to the [Phase Docs](https://docs.phase.dev/sdks/go).
120184

121185
---

phase/misc/const.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,12 @@ var (
2020
// Compiled regex patterns
2121
PssUserPattern = regexp.MustCompile(`^pss_user:v(\d+):([a-fA-F0-9]{64}):([a-fA-F0-9]{64}):([a-fA-F0-9]{64}):([a-fA-F0-9]{64})$`)
2222
PssServicePattern = regexp.MustCompile(`^pss_service:v(\d+):([a-fA-F0-9]{64}):([a-fA-F0-9]{64}):([a-fA-F0-9]{64}):([a-fA-F0-9]{64})$`)
23-
CrossEnvPattern = regexp.MustCompile(`\$\{(.+?)\.(.+?)\}`)
24-
LocalRefPattern = regexp.MustCompile(`\$\{([^.]+?)\}`)
23+
24+
//CrossEnvPattern = regexp.MustCompile(`\$\{(.+?)\.(.+?)\}`)
25+
// LocalRefPattern = regexp.MustCompile(`\$\{([^.]+?)\}`)
26+
27+
// Regex to identify secret references
28+
SecretRefRegex = regexp.MustCompile(`\$\{([^}]+)\}`)
2529
)
2630

2731

phase/misc/misc.go

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,24 @@ func PhaseGetContext(userData AppKeyResponse, appName, envName string) (string,
1919
return "", "", "", fmt.Errorf("matching context not found")
2020
}
2121

22+
// FindEnvironmentKey searches for an environment key with case-insensitive and partial matching.
2223
func FindEnvironmentKey(userData AppKeyResponse, envName, appName string) (*EnvironmentKey, error) {
24+
// Convert envName and appName to lowercase for case-insensitive comparison
25+
lcEnvName := strings.ToLower(envName)
26+
lcAppName := strings.ToLower(appName)
27+
2328
for _, app := range userData.Apps {
24-
if appName == "" || app.Name == appName {
29+
// Support partial and case-insensitive matching for appName
30+
if appName == "" || strings.Contains(strings.ToLower(app.Name), lcAppName) {
2531
for _, envKey := range app.EnvironmentKeys {
26-
if envKey.Environment.Name == envName {
27-
return &envKey, nil // Note the address-of operator (&) before envKey
32+
// Support partial and case-insensitive matching for envName
33+
if strings.Contains(strings.ToLower(envKey.Environment.Name), lcEnvName) {
34+
return &envKey, nil
2835
}
2936
}
3037
}
3138
}
32-
return nil, fmt.Errorf("environment key not found")
39+
return nil, fmt.Errorf("environment key not found for app '%s' and environment '%s'", appName, envName)
3340
}
3441

3542
// normalizeTag replaces underscores with spaces and converts the string to lower case.

phase/phase.go

Lines changed: 101 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"encoding/json"
55
"fmt"
66
"log"
7+
"strings"
78

89
"github.com/phasehq/golang-sdk/phase/crypto"
910
"github.com/phasehq/golang-sdk/phase/misc"
@@ -85,6 +86,86 @@ func Init(serviceToken, host string, debug bool) *Phase {
8586
}
8687
}
8788

89+
func (p *Phase) resolveSecretReference(ref, currentEnvName string) (string, error) {
90+
var envName, path, keyName string
91+
92+
// Check if the reference starts with an environment name followed by a dot
93+
if strings.Contains(ref, ".") {
94+
// Split on the first dot to differentiate environment from path/key
95+
parts := strings.SplitN(ref, ".", 2)
96+
envName = parts[0]
97+
98+
// Further split the second part to separate the path and the key
99+
// The last segment after the last "/" is the key, the rest is the path
100+
lastSlashIndex := strings.LastIndex(parts[1], "/")
101+
if lastSlashIndex != -1 { // Path is specified
102+
path = parts[1][:lastSlashIndex] // Include the slash in the path
103+
keyName = parts[1][lastSlashIndex+1:]
104+
} else { // No path specified, use root
105+
path = "/"
106+
keyName = parts[1]
107+
}
108+
} else { // Local reference without an environment prefix
109+
envName = currentEnvName
110+
lastSlashIndex := strings.LastIndex(ref, "/")
111+
if lastSlashIndex != -1 { // Path is specified
112+
path = ref[:lastSlashIndex] // Include the slash in the path
113+
keyName = ref[lastSlashIndex+1:]
114+
} else { // No path specified, use root
115+
path = "/"
116+
keyName = ref
117+
}
118+
}
119+
120+
// Validate the extracted parts
121+
if keyName == "" {
122+
return "", fmt.Errorf("invalid secret reference format: %s", ref)
123+
}
124+
125+
// Fetch and decrypt the referenced secret
126+
opts := GetSecretOptions{
127+
EnvName: envName,
128+
AppName: "", // AppName is available globally
129+
KeyToFind: keyName,
130+
SecretPath: path,
131+
}
132+
resolvedSecret, err := p.Get(opts)
133+
if err != nil {
134+
return "", fmt.Errorf("failed to resolve secret reference %s: %v", ref, err)
135+
}
136+
137+
// Return the decrypted value of the referenced secret
138+
decryptedValue, ok := (*resolvedSecret)["value"].(string)
139+
if !ok {
140+
return "", fmt.Errorf("decrypted value of the secret reference %s is not a string", ref)
141+
}
142+
143+
return decryptedValue, nil
144+
}
145+
146+
// resolveSecretValue resolves all secret references in a given value string.
147+
func (p *Phase) resolveSecretValue(value string, currentEnvName string) (string, error) {
148+
refs := misc.SecretRefRegex.FindAllString(value, -1)
149+
resolvedValue := value
150+
151+
for _, ref := range refs {
152+
// Extract just the reference part without the surrounding ${}
153+
refMatch := misc.SecretRefRegex.FindStringSubmatch(ref)
154+
if len(refMatch) > 1 {
155+
// Pass the current environment name if needed for resolution
156+
resolvedSecretValue, err := p.resolveSecretReference(refMatch[1], currentEnvName)
157+
if err != nil {
158+
return "", err
159+
}
160+
// Directly use the string value returned by resolveSecretReference
161+
resolvedValue = strings.Replace(resolvedValue, ref, resolvedSecretValue, -1)
162+
}
163+
}
164+
165+
return resolvedValue, nil
166+
}
167+
168+
// Get fetches and decrypts a secret, resolving any secret references within its value.
88169
func (p *Phase) Get(opts GetSecretOptions) (*map[string]interface{}, error) {
89170
// Fetch user data
90171
resp, err := network.FetchPhaseUser(p.AppToken, p.Host)
@@ -160,6 +241,15 @@ func (p *Phase) Get(opts GetSecretOptions) (*map[string]interface{}, error) {
160241
return nil, err
161242
}
162243

244+
// Resolve any secret references within the decryptedValue before creating the result map
245+
resolvedValue, err := p.resolveSecretValue(decryptedValue, opts.EnvName)
246+
if err != nil {
247+
if p.Debug {
248+
log.Printf("Failed to resolve secret value: %v", err)
249+
}
250+
return nil, err
251+
}
252+
163253
// Verify tag match if a tag is provided
164254
var stringTags []string
165255
if opts.Tag != "" {
@@ -180,7 +270,7 @@ func (p *Phase) Get(opts GetSecretOptions) (*map[string]interface{}, error) {
180270

181271
result := &map[string]interface{}{
182272
"key": decryptedKey,
183-
"value": decryptedValue,
273+
"value": resolvedValue, // Use resolvedValue here
184274
"comment": decryptedComment,
185275
"tags": stringTags,
186276
"path": secretPath,
@@ -259,6 +349,15 @@ func (p *Phase) GetAll(opts GetAllSecretsOptions) ([]map[string]interface{}, err
259349
continue
260350
}
261351

352+
// Resolve any secret references within the decryptedValue
353+
resolvedValue, err := p.resolveSecretValue(decryptedValue, opts.EnvName)
354+
if err != nil {
355+
if p.Debug {
356+
log.Printf("Failed to resolve secret value: %v\n", err)
357+
}
358+
continue
359+
}
360+
262361
// Prepare tags for inclusion in result
263362
var stringTags []string
264363
if secretTags, ok := secret["tags"].([]interface{}); ok {
@@ -280,7 +379,7 @@ func (p *Phase) GetAll(opts GetAllSecretsOptions) ([]map[string]interface{}, err
280379
// Append decrypted secret with path to result list
281380
result := map[string]interface{}{
282381
"key": decryptedKey,
283-
"value": decryptedValue,
382+
"value": resolvedValue, // Use resolvedValue here
284383
"comment": decryptedComment,
285384
"tags": stringTags,
286385
"path": path,

0 commit comments

Comments
 (0)