Skip to content

Commit 77785d2

Browse files
committed
feat: added secret referencing support
1 parent 72e72cc commit 77785d2

File tree

1 file changed

+101
-2
lines changed

1 file changed

+101
-2
lines changed

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)