Skip to content

Commit d8f26cd

Browse files
authored
feat[ibmsm]: Secret group name resolution and simpler key interpolation (#609)
* feat[ibmsm]: Secret group name resolution and simpler key interpolation Signed-off-by: Jarek Gawor <[email protected]> * add docs Signed-off-by: Jarek Gawor <[email protected]> * address comments Signed-off-by: Jarek Gawor <[email protected]> --------- Signed-off-by: Jarek Gawor <[email protected]>
1 parent 42a43f0 commit d8f26cd

File tree

3 files changed

+393
-25
lines changed

3 files changed

+393
-25
lines changed

docs/backends.md

+38-5
Original file line numberDiff line numberDiff line change
@@ -170,14 +170,28 @@ data:
170170
**Note**: Only Vault KV-V2 backends support versioning. Versions specified with a KV-V1 Vault will be ignored and the latest version will be retrieved.
171171
172172
### IBM Cloud Secrets Manager
173-
For IBM Cloud Secret Manager we only support using IAM authentication at this time.
174173
175-
We support all types of secrets that can be retrieved from IBM Cloud Secret Manager. Please note:
174+
The path for IBM Cloud Secret Manager secrets can be specified in two ways:
175+
1. `ibmcloud/<SECRET_TYPE>/secrets/groups/<GROUP>#<SECRET_NAME>`, or
176+
2. `ibmcloud/<SECRET_TYPE>/secrets/groups/<GROUP>/<SECRET_NAME>#<SECRET_KEY>`
176177

177-
- Secrets that are JSON data (i.e, non `arbitrary` secrets or an `arbitrary` secret with JSON `payload`) can have the select keys (i.e, the `username` in a `username_password` type secret) interpolated with the [jsonPath](./howitworks.md#jsonPath) modifier. Not all keys are available for extraction with `jsonPath`. Refer to the [IBM Cloud Secret Manager API docs](https://cloud.ibm.com/apidocs/secrets-manager#get-secret) for more details
178+
Where:
179+
* `<SECRET_TYPE>` can be one of the following: `arbitrary`, `iam_credentials`, `imported_cert`, `kv`, `private_cert`, `public_cert`, or `username_password`.
180+
* `<GROUP>` can be a secret group ID or name.
181+
* `<SECRET_NAME>` is the name of the secret.
182+
* `<SECRET_KEY>` is the key name within the secret. Specifically, the following keys are available for extraction:
183+
* `api_key` for the `iam_credentials` secret type
184+
* `username` and `password` for the `username_password` secret type
185+
* `certificate`, `private_key`, `intermediate` for the `imported_cert` or `public_cert` secret types
186+
* `certificate`, `private_key`, `issuing_ca`, `ca_chain` for the `private_cert` secret type
187+
* any key of the `kv` secret type
188+
`<SECRET_KEY>` is not supported for the `arbitrary` secret type.
178189

179-
##### IAM Authentication
180-
For IAM Authentication, these are the required parameters:
190+
When using the first path syntax, secrets that are JSON data (i.e, non `arbitrary` secrets or an `arbitrary` secret with JSON `payload`) can have select keys (listed under `<SECRET_KEY>` above) interpolated with the [jsonPath](./howitworks.md#jsonPath) modifier. With the second path syntax, the interpolation with the `jsonPath` modifier is not necessary.
191+
192+
##### Authentication
193+
194+
IAM authentication is only supported at this time. The following parameters are required for IAM authentication:
181195
```
182196
AVP_IBM_INSTANCE_URL or VAULT_ADDR: Your IBM Cloud Secret Manager Endpoint
183197
AVP_TYPE: ibmsecretsmanager
@@ -233,6 +247,25 @@ stringData:
233247
<my-cert-secret | jsonPath {.private_key}>
234248
```
235249

250+
###### Non-arbitrary secrets (alternative path syntax)
251+
252+
```yaml
253+
kind: Secret
254+
apiVersion: v1
255+
metadata:
256+
name: ibm-example
257+
annotations:
258+
avp.kubernetes.io/path: "ibmcloud/imported_cert/secrets/groups/myGroup/my-cert-secret"
259+
type: Opaque
260+
stringData:
261+
PUBLIC_CRT: |
262+
<certificate>
263+
PRIVATE_KEY: |
264+
<private_key>
265+
USERNAME: <path:ibmcloud/username_password/secrets/groups/myGroup/basic-auth#username>
266+
PASSWORD: <path:ibmcloud/username_password/secrets/groups/myGroup/basic-auth#password>
267+
```
268+
236269
### AWS Secrets Manager
237270

238271
##### AWS Authentication

pkg/backends/ibmsecretsmanager.go

+109-13
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ import (
1111
"github.com/argoproj-labs/argocd-vault-plugin/pkg/utils"
1212
)
1313

14-
var IBMPath, _ = regexp.Compile(`ibmcloud/(?P<type>.+)/secrets/groups/(?P<groupId>.+)`)
14+
var IBMPath, _ = regexp.Compile(`ibmcloud/(?P<type>.+)/secrets/groups/(?P<groupId>[^/\n]+)(/(?P<secretName>.+))?`)
15+
var GroupId, _ = regexp.Compile(`[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}`)
1516

1617
// IBMSecretMetadata wraps the SecretMetadataIntf provided by the SDK
1718
// It provides a generic method for accessing the metadata regardless of secret type
@@ -282,6 +283,7 @@ type IBMSecretsManagerClient interface {
282283
ListSecrets(listAllSecretsOptions *ibmsm.ListSecretsOptions) (result *ibmsm.SecretMetadataPaginatedCollection, response *core.DetailedResponse, err error)
283284
GetSecret(getSecretOptions *ibmsm.GetSecretOptions) (result ibmsm.SecretIntf, response *core.DetailedResponse, err error)
284285
GetSecretVersion(getSecretOptions *ibmsm.GetSecretVersionOptions) (result ibmsm.SecretVersionIntf, response *core.DetailedResponse, err error)
286+
ListSecretGroups(listSecretGroupsOptions *ibmsm.ListSecretGroupsOptions) (result *ibmsm.SecretGroupCollection, response *core.DetailedResponse, err error)
285287
}
286288

287289
// Used as the key into the several caches for IBM SM API calls
@@ -314,6 +316,8 @@ type IBMSecretsManager struct {
314316
// Keeps track of whether GetSecrets has been called for a given group and secret type
315317
// Only read/written to by the main goroutine, no synchronized access needed
316318
retrievedAllSecrets map[cacheKey]bool
319+
320+
secretGroups map[string]string
317321
}
318322

319323
// NewIBMSecretsManagerBackend initializes a new IBM Secret Manager backend
@@ -323,17 +327,18 @@ func NewIBMSecretsManagerBackend(client IBMSecretsManagerClient) *IBMSecretsMana
323327
listAllSecretsCache: make(map[cacheKey]map[string]*IBMSecretMetadata),
324328
getSecretsCache: make(map[cacheKey]map[string]interface{}),
325329
retrievedAllSecrets: make(map[cacheKey]bool),
330+
secretGroups: make(map[string]string),
326331
}
327332
return ibmSecretsManager
328333
}
329334

330335
// parsePath returns the groupId, secretType represented by the path
331-
func parsePath(path string) (string, string, error) {
336+
func parsePath(path string) (string, string, string, error) {
332337
matches := IBMPath.FindStringSubmatch(path)
333338
if len(matches) == 0 {
334-
return "", "", fmt.Errorf("Path is not in the correct format (ibmcloud/$TYPE/secrets/groups/$GROUP_ID) for IBM Secrets Manager: %s", path)
339+
return "", "", "", fmt.Errorf("Path is not in the correct format (ibmcloud/$TYPE/secrets/groups/$GROUP_ID) for IBM Secrets Manager: %s", path)
335340
}
336-
return matches[IBMPath.SubexpIndex("type")], matches[IBMPath.SubexpIndex("groupId")], nil
341+
return matches[IBMPath.SubexpIndex("type")], matches[IBMPath.SubexpIndex("groupId")], matches[IBMPath.SubexpIndex("secretName")], nil
337342
}
338343

339344
func (i *IBMSecretsManager) readSecretFromCache(groupId, secretType, secretName string) interface{} {
@@ -536,18 +541,64 @@ func storeSecret(secrets *map[string]interface{}, result map[string]interface{})
536541
return nil
537542
}
538543

544+
func (i *IBMSecretsManager) resolveGroup(group string) (string, error) {
545+
// no need to resolve default or groupIds
546+
if group == "default" || GroupId.MatchString(group) {
547+
return group, nil
548+
}
549+
550+
// list groups
551+
if len(i.secretGroups) == 0 {
552+
opts := &ibmsm.ListSecretGroupsOptions{}
553+
secretGroupCollection, _, err := i.Client.ListSecretGroups(opts)
554+
if err != nil {
555+
return "", fmt.Errorf("Could not list secret groups: %s", err)
556+
}
557+
for _, group := range secretGroupCollection.SecretGroups {
558+
i.secretGroups[*group.Name] = *group.ID
559+
}
560+
}
561+
562+
// look up group id for group name
563+
groupId := i.secretGroups[group]
564+
if groupId == "" {
565+
return "", fmt.Errorf("No such secret group %s", group)
566+
} else {
567+
return groupId, nil
568+
}
569+
}
570+
539571
// GetSecrets returns the data for all secrets of a specific type of a group in IBM Secrets Manager
540572
func (i *IBMSecretsManager) GetSecrets(path string, version string, annotations map[string]string) (map[string]interface{}, error) {
541-
secretType, groupId, err := parsePath(path)
573+
secretType, group, secretName, err := parsePath(path)
574+
if err != nil {
575+
return nil, fmt.Errorf("Path is not in the correct format (ibmcloud/$TYPE/secrets/groups/$GROUP) for IBM Secrets Manager: %s", path)
576+
}
577+
if secretType == "arbitrary" && secretName != "" {
578+
return nil, fmt.Errorf("The 'ibmcloud/$TYPE/secrets/groups/$GROUP/$SECRET' path format is not supported for arbitrary secrets: %s", path)
579+
}
580+
581+
groupId, err := i.resolveGroup(group)
542582
if err != nil {
543-
return nil, fmt.Errorf("Path is not in the correct format (ibmcloud/$TYPE/secrets/groups/$GROUP_ID) for IBM Secrets Manager: %s", path)
583+
return nil, err
544584
}
585+
545586
ckey := cacheKey{groupId, secretType}
546587

547588
// Bypass the cache when explicit version is requested
548589
// Otherwise, use it if applicable
549590
if version == "" && i.retrievedAllSecrets[ckey] {
550-
return i.getSecretsCache[ckey], nil
591+
secrets := i.getSecretsCache[ckey]
592+
if secretName != "" {
593+
secretData, ok := secrets[secretName].(map[string]interface{})
594+
if ok {
595+
return secretData, nil
596+
} else {
597+
return nil, nil
598+
}
599+
} else {
600+
return secrets, nil
601+
}
551602
}
552603

553604
// So we query the group to enumerate the secret ids, and retrieve each one to return a complete map of them
@@ -598,22 +649,57 @@ func (i *IBMSecretsManager) GetSecrets(path string, version string, annotations
598649

599650
i.retrievedAllSecrets[ckey] = true
600651

601-
return secrets, nil
652+
if secretName != "" {
653+
secretData, ok := secrets[secretName].(map[string]interface{})
654+
if ok {
655+
return secretData, nil
656+
} else {
657+
return nil, nil
658+
}
659+
} else {
660+
return secrets, nil
661+
}
602662
}
603663

604664
// GetIndividualSecret will get the specific secret (placeholder) from the SM backend
605665
// This requires listing the secrets of the group to obtain the id, and then using that to grab the one secret's payload
606-
func (i *IBMSecretsManager) GetIndividualSecret(kvpath, secretName, version string, annotations map[string]string) (interface{}, error) {
607-
secretType, groupId, err := parsePath(kvpath)
666+
func (i *IBMSecretsManager) GetIndividualSecret(kvpath, secretRef, version string, annotations map[string]string) (interface{}, error) {
667+
secretType, group, secretName, err := parsePath(kvpath)
608668
if err != nil {
609-
return nil, fmt.Errorf("Path is not in the correct format (ibmcloud/$TYPE/secrets/groups/$GROUP_ID) for IBM Secrets Manager: %s", kvpath)
669+
return nil, fmt.Errorf("Path is not in the correct format (ibmcloud/$TYPE/secrets/groups/$GROUP) for IBM Secrets Manager: %s", kvpath)
670+
}
671+
if secretType == "arbitrary" && secretName != "" {
672+
return nil, fmt.Errorf("The 'ibmcloud/$TYPE/secrets/groups/$GROUP/$SECRET' path format is not supported for arbitrary secrets: %s", kvpath)
610673
}
674+
675+
groupId, err := i.resolveGroup(group)
676+
if err != nil {
677+
return nil, err
678+
}
679+
680+
var secretKey string
681+
if secretName == "" {
682+
secretName = secretRef
683+
} else {
684+
secretKey = secretRef
685+
}
686+
611687
ckey := cacheKey{groupId, secretType}
612688

613689
// Bypass the cache when explicit version is requested
614690
// If we have already retrieved all the secrets for the requested secret's group and type, we have a cache hit
615691
if version == "" && i.retrievedAllSecrets[ckey] {
616-
return i.getSecretsCache[ckey][secretName], nil
692+
secretData := i.getSecretsCache[ckey][secretName]
693+
if secretKey != "" {
694+
secretValue, ok := secretData.(map[string]interface{})
695+
if ok {
696+
return secretValue[secretKey], nil
697+
} else {
698+
return nil, nil
699+
}
700+
} else {
701+
return secretData, nil
702+
}
617703
}
618704

619705
// Grab the *ibmsm.SecretMetadata corresponding to the secret
@@ -644,5 +730,15 @@ func (i *IBMSecretsManager) GetIndividualSecret(kvpath, secretName, version stri
644730
return nil, err
645731
}
646732

647-
return secrets[secretName], nil
733+
secretData := secrets[secretName]
734+
if secretKey != "" {
735+
secretValue, ok := secretData.(map[string]interface{})
736+
if ok {
737+
return secretValue[secretKey], nil
738+
} else {
739+
return nil, nil
740+
}
741+
} else {
742+
return secretData, nil
743+
}
648744
}

0 commit comments

Comments
 (0)