From 28718899ec3cd10e6f5c054bcd2458eeb49f43b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Volkan=20O=CC=88zc=CC=A7elik?= Date: Mon, 26 Aug 2024 23:41:40 -0700 Subject: [PATCH] =?UTF-8?q?=F0=9F=8C=9F=20enhancement(VSecM):=20Isolate=20?= =?UTF-8?q?VSecM=20SDK?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Copied upstream dependencies into the SDK module; and added a separate go.mod file for it to allow SDK use a diferent version of Golang than the rest of the project. Signed-off-by: Volkan Özçelik --- app/keygen/cmd/main.go | 3 +- app/safe/internal/server/handle/route.go | 2 +- .../internal/initialization/connectivity.go | 5 +- .../{implementations.go => impl.go} | 9 +- app/sentinel/internal/initialization/run.go | 3 +- app/sentinel/internal/initialization/types.go | 1 + app/sentinel/internal/safe/action.go | 3 +- app/sentinel/internal/safe/get.go | 4 +- core/audit/journal/log_test.go | 474 ++++++++++-------- core/audit/journal/print_test.go | 105 ++++ core/constants/env/env_test.go | 27 + core/constants/url/url.go | 10 + core/constants/val/val_test.go | 99 ++++ core/entity/v1/data/convert.go | 4 +- core/entity/v1/data/crypto.go | 3 +- core/env/backoff.go | 3 +- core/env/init.go | 3 +- core/env/logging.go | 5 +- core/env/poll.go | 3 +- core/env/store.go | 2 +- core/spiffe/spiffe.go | 3 +- go.mod | 14 +- go.sum | 7 + lib/spiffe/spiffe.go | 3 +- sdk/LICENSE | 23 + sdk/README.md | 125 +++++ sdk/core/constants/crypto/algorithm.go | 13 + sdk/core/constants/env/env.go | 46 ++ sdk/core/constants/key/key.go | 13 + sdk/core/constants/symbol/symbol.go | 15 + sdk/core/constants/val/val.go | 16 + sdk/core/crypto/crypto.go | 26 + sdk/core/entity/v1/data/convert.go | 189 +++++++ sdk/core/entity/v1/data/secret.go | 60 +++ sdk/core/entity/v1/data/secret_stored.go | 158 ++++++ sdk/core/entity/v1/data/status.go | 56 +++ sdk/core/entity/v1/reqres/safe/safe.go | 122 +++++ sdk/core/env/endpoint.go | 27 + sdk/core/env/init.go | 39 ++ sdk/core/env/logging.go | 73 +++ sdk/core/env/poll.go | 38 ++ sdk/core/env/sidecar.go | 26 + sdk/core/env/spiffe.go | 44 ++ sdk/core/env/spiffeid.go | 52 ++ sdk/core/log/level/level.go | 45 ++ sdk/core/log/std/log.go | 25 + sdk/core/log/std/print.go | 39 ++ sdk/core/template/filter.go | 47 ++ sdk/core/template/template.go | 86 ++++ sdk/core/validation/validation.go | 173 +++++++ sdk/go.mod | 24 + sdk/go.sum | 40 ++ sdk/lib/backoff/retry.go | 147 ++++++ sdk/lib/crypto/crypto.go | 33 ++ sdk/lib/entity/json_time.go | 68 +++ sdk/sentry/fetch.go | 10 +- sdk/sentry/privates.go | 2 +- sdk/sentry/watch.go | 8 +- sdk/startup/watch.go | 6 +- 59 files changed, 2461 insertions(+), 248 deletions(-) rename app/sentinel/internal/initialization/{implementations.go => impl.go} (99%) create mode 100644 core/constants/val/val_test.go create mode 100644 sdk/LICENSE create mode 100644 sdk/README.md create mode 100644 sdk/core/constants/crypto/algorithm.go create mode 100644 sdk/core/constants/env/env.go create mode 100644 sdk/core/constants/key/key.go create mode 100644 sdk/core/constants/symbol/symbol.go create mode 100644 sdk/core/constants/val/val.go create mode 100644 sdk/core/crypto/crypto.go create mode 100644 sdk/core/entity/v1/data/convert.go create mode 100644 sdk/core/entity/v1/data/secret.go create mode 100644 sdk/core/entity/v1/data/secret_stored.go create mode 100644 sdk/core/entity/v1/data/status.go create mode 100644 sdk/core/entity/v1/reqres/safe/safe.go create mode 100644 sdk/core/env/endpoint.go create mode 100644 sdk/core/env/init.go create mode 100644 sdk/core/env/logging.go create mode 100644 sdk/core/env/poll.go create mode 100644 sdk/core/env/sidecar.go create mode 100644 sdk/core/env/spiffe.go create mode 100644 sdk/core/env/spiffeid.go create mode 100644 sdk/core/log/level/level.go create mode 100644 sdk/core/log/std/log.go create mode 100644 sdk/core/log/std/print.go create mode 100644 sdk/core/template/filter.go create mode 100644 sdk/core/template/template.go create mode 100644 sdk/core/validation/validation.go create mode 100644 sdk/go.mod create mode 100644 sdk/go.sum create mode 100644 sdk/lib/backoff/retry.go create mode 100644 sdk/lib/crypto/crypto.go create mode 100644 sdk/lib/entity/json_time.go diff --git a/app/keygen/cmd/main.go b/app/keygen/cmd/main.go index 4792e4ae..dbd7c3a8 100644 --- a/app/keygen/cmd/main.go +++ b/app/keygen/cmd/main.go @@ -11,12 +11,13 @@ package main import ( + "os" + "github.com/vmware-tanzu/secrets-manager/app/keygen/internal" e "github.com/vmware-tanzu/secrets-manager/core/constants/env" "github.com/vmware-tanzu/secrets-manager/core/crypto" "github.com/vmware-tanzu/secrets-manager/core/env" log "github.com/vmware-tanzu/secrets-manager/core/log/std" - "os" ) func main() { diff --git a/app/safe/internal/server/handle/route.go b/app/safe/internal/server/handle/route.go index 0871579e..0dffc40b 100644 --- a/app/safe/internal/server/handle/route.go +++ b/app/safe/internal/server/handle/route.go @@ -11,10 +11,10 @@ package handle import ( - routeFallback "github.com/vmware-tanzu/secrets-manager/app/safe/internal/server/route/fallback" "net/http" routeDelete "github.com/vmware-tanzu/secrets-manager/app/safe/internal/server/route/delete" + routeFallback "github.com/vmware-tanzu/secrets-manager/app/safe/internal/server/route/fallback" routeFetch "github.com/vmware-tanzu/secrets-manager/app/safe/internal/server/route/fetch" routeKeystone "github.com/vmware-tanzu/secrets-manager/app/safe/internal/server/route/keystone" routeList "github.com/vmware-tanzu/secrets-manager/app/safe/internal/server/route/list" diff --git a/app/sentinel/internal/initialization/connectivity.go b/app/sentinel/internal/initialization/connectivity.go index 23914415..0a62d4ac 100644 --- a/app/sentinel/internal/initialization/connectivity.go +++ b/app/sentinel/internal/initialization/connectivity.go @@ -13,10 +13,11 @@ package initialization import ( "context" "errors" - "github.com/vmware-tanzu/secrets-manager/core/constants/key" - "github.com/vmware-tanzu/secrets-manager/lib/backoff" "github.com/spiffe/go-spiffe/v2/workloadapi" + + "github.com/vmware-tanzu/secrets-manager/core/constants/key" + "github.com/vmware-tanzu/secrets-manager/lib/backoff" ) func (i *Initializer) ensureApiConnectivity(ctx context.Context, cid *string) { diff --git a/app/sentinel/internal/initialization/implementations.go b/app/sentinel/internal/initialization/impl.go similarity index 99% rename from app/sentinel/internal/initialization/implementations.go rename to app/sentinel/internal/initialization/impl.go index c81effd1..dd349203 100644 --- a/app/sentinel/internal/initialization/implementations.go +++ b/app/sentinel/internal/initialization/impl.go @@ -12,15 +12,16 @@ package initialization import ( "context" - "github.com/spiffe/go-spiffe/v2/workloadapi" - "github.com/vmware-tanzu/secrets-manager/app/sentinel/internal/safe" - "github.com/vmware-tanzu/secrets-manager/core/entity/v1/data" - "github.com/vmware-tanzu/secrets-manager/core/spiffe" "os" "time" + "github.com/spiffe/go-spiffe/v2/workloadapi" + + "github.com/vmware-tanzu/secrets-manager/app/sentinel/internal/safe" + "github.com/vmware-tanzu/secrets-manager/core/entity/v1/data" "github.com/vmware-tanzu/secrets-manager/core/env" "github.com/vmware-tanzu/secrets-manager/core/log/std" + "github.com/vmware-tanzu/secrets-manager/core/spiffe" ) type OSFileOpener struct{} diff --git a/app/sentinel/internal/initialization/run.go b/app/sentinel/internal/initialization/run.go index 6d5261a2..3f528e44 100644 --- a/app/sentinel/internal/initialization/run.go +++ b/app/sentinel/internal/initialization/run.go @@ -12,9 +12,10 @@ package initialization import ( "context" - "github.com/vmware-tanzu/secrets-manager/core/constants/key" "os" "time" + + "github.com/vmware-tanzu/secrets-manager/core/constants/key" ) // RunInitCommands reads and processes initialization commands from a file. diff --git a/app/sentinel/internal/initialization/types.go b/app/sentinel/internal/initialization/types.go index 7fc443b4..d8cf4420 100644 --- a/app/sentinel/internal/initialization/types.go +++ b/app/sentinel/internal/initialization/types.go @@ -16,6 +16,7 @@ import ( "time" "github.com/spiffe/go-spiffe/v2/workloadapi" + entity "github.com/vmware-tanzu/secrets-manager/core/entity/v1/data" ) diff --git a/app/sentinel/internal/safe/action.go b/app/sentinel/internal/safe/action.go index 32084849..d8531efb 100644 --- a/app/sentinel/internal/safe/action.go +++ b/app/sentinel/internal/safe/action.go @@ -12,9 +12,8 @@ package safe import ( "bytes" - "net/http" - "errors" + "net/http" ) func doDelete(cid *string, client *http.Client, p string, md []byte) error { diff --git a/app/sentinel/internal/safe/get.go b/app/sentinel/internal/safe/get.go index 1c247328..d946063c 100644 --- a/app/sentinel/internal/safe/get.go +++ b/app/sentinel/internal/safe/get.go @@ -21,12 +21,12 @@ import ( "github.com/spiffe/go-spiffe/v2/spiffeid" "github.com/spiffe/go-spiffe/v2/spiffetls/tlsconfig" "github.com/spiffe/go-spiffe/v2/workloadapi" - "github.com/vmware-tanzu/secrets-manager/core/constants/key" - "github.com/vmware-tanzu/secrets-manager/core/spiffe" + "github.com/vmware-tanzu/secrets-manager/core/constants/key" u "github.com/vmware-tanzu/secrets-manager/core/constants/url" "github.com/vmware-tanzu/secrets-manager/core/env" log "github.com/vmware-tanzu/secrets-manager/core/log/rpc" + "github.com/vmware-tanzu/secrets-manager/core/spiffe" "github.com/vmware-tanzu/secrets-manager/core/validation" ) diff --git a/core/audit/journal/log_test.go b/core/audit/journal/log_test.go index 5d7ae84d..d0024742 100644 --- a/core/audit/journal/log_test.go +++ b/core/audit/journal/log_test.go @@ -10,207 +10,273 @@ package journal -//import ( -// "testing" -// -// reqres "github.com/vmware-tanzu/secrets-manager/core/entity/v1/reqres/safe" -//) -// -//func Test_printAudit(t *testing.T) { -// type args struct { -// correlationId string -// entityName string -// method string -// url string -// spiffeid string -// message string -// } -// tests := []struct { -// name string -// args args -// }{ -// { -// name: "success_case", -// args: args{ -// correlationId: "1234", -// entityName: "abcd", -// method: "GET", -// url: "http://localhost:5000/", -// spiffeid: "abcd1234", -// message: "testing audit func", -// }, -// }, -// } -// for _, tt := range tests { -// t.Run(tt.name, func(t *testing.T) { -// printAudit(tt.args.correlationId, tt.args.entityName, tt.args.method, tt.args.url, tt.args.spiffeid, tt.args.message) -// }) -// } -//} -// -//func TestLog(t *testing.T) { -// type args struct { -// e Entry -// } -// tests := []struct { -// name string -// args args -// }{ -// { -// name: "nil_JournalEntry", -// args: args{ -// e: Entry{ -// Entity: nil, -// }, -// }, -// }, -// { -// name: "Entity_default", -// args: args{ -// e: Entry{ -// CorrelationId: "1234", -// Entity: "", -// Method: "test_method", -// Url: "test_url", -// SpiffeId: "test_spiffeid", -// Event: "test_event", -// }, -// }, -// }, -// { -// name: "Entity_type_SecretDeleteRequest", -// args: args{ -// e: Entry{ -// CorrelationId: "1234", -// Entity: reqres.SecretDeleteRequest{}, -// Method: "test_method", -// Url: "test_url", -// SpiffeId: "test_spiffeid", -// Event: "test_event", -// }, -// }, -// }, -// { -// name: "Entity_type_SecretDeleteResponse", -// args: args{ -// e: Entry{ -// CorrelationId: "1234", -// Entity: reqres.SecretDeleteResponse{}, -// Method: "test_method", -// Url: "test_url", -// SpiffeId: "test_spiffeid", -// Event: "test_event", -// }, -// }, -// }, -// { -// name: "Entity_type_SecretFetchRequest", -// args: args{ -// e: Entry{ -// CorrelationId: "1234", -// Entity: reqres.SecretFetchRequest{}, -// Method: "test_method", -// Url: "test_url", -// SpiffeId: "test_spiffeid", -// Event: "test_event", -// }, -// }, -// }, -// { -// name: "Entity_type_SecretFetchResponse", -// args: args{ -// e: Entry{ -// CorrelationId: "1234", -// Entity: reqres.SecretFetchResponse{}, -// Method: "test_method", -// Url: "test_url", -// SpiffeId: "test_spiffeid", -// Event: "test_event", -// }, -// }, -// }, -// { -// name: "Entity_type_SecretUpsertRequest", -// args: args{ -// e: Entry{ -// CorrelationId: "1234", -// Entity: reqres.SecretUpsertRequest{}, -// Method: "test_method", -// Url: "test_url", -// SpiffeId: "test_spiffeid", -// Event: "test_event", -// }, -// }, -// }, -// { -// name: "Entity_type_SecretUpsertResponse", -// args: args{ -// e: Entry{ -// CorrelationId: "1234", -// Entity: reqres.SecretUpsertResponse{}, -// Method: "test_method", -// Url: "test_url", -// SpiffeId: "test_spiffeid", -// Event: "test_event", -// }, -// }, -// }, -// { -// name: "Entity_type_SecretListRequest", -// args: args{ -// e: Entry{ -// CorrelationId: "1234", -// Entity: reqres.SecretListRequest{}, -// Method: "test_method", -// Url: "test_url", -// SpiffeId: "test_spiffeid", -// Event: "test_event", -// }, -// }, -// }, -// { -// name: "Entity_type_SecretListResponse", -// args: args{ -// e: Entry{ -// CorrelationId: "1234", -// Entity: reqres.SecretListResponse{}, -// Method: "test_method", -// Url: "test_url", -// SpiffeId: "test_spiffeid", -// Event: "test_event", -// }, -// }, -// }, -// { -// name: "Entity_type_SecretEncryptedListResponse", -// args: args{ -// e: Entry{ -// CorrelationId: "1234", -// Entity: reqres.SecretEncryptedListResponse{}, -// Method: "test_method", -// Url: "test_url", -// SpiffeId: "test_spiffeid", -// Event: "test_event", -// }, -// }, -// }, -// { -// name: "Entity_type_KeyInputRequest", -// args: args{ -// e: Entry{ -// CorrelationId: "1234", -// Entity: reqres.KeyInputRequest{}, -// Method: "test_method", -// Url: "test_url", -// SpiffeId: "test_spiffeid", -// Event: "test_event", -// }, -// }, -// }, -// } -// for _, tt := range tests { -// t.Run(tt.name, func(t *testing.T) { -// Log(tt.args.e) -// }) -// } -//} +import ( + "encoding/json" + "github.com/vmware-tanzu/secrets-manager/core/constants/audit" + "github.com/vmware-tanzu/secrets-manager/core/entity/v1/data" + "net/http" + "regexp" + "testing" + + reqres "github.com/vmware-tanzu/secrets-manager/core/entity/v1/reqres/safe" +) + +func TestLog(t *testing.T) { + // Compile the regular expression + re, err := regexp.Compile(regexPattern) + if err != nil { + t.Fatal(err) + } + + toJsonStr := func(s any) string { + bytes, err := json.Marshal(s) + if err != nil { + return "" + } + return string(bytes) + } + + type args struct { + e data.JournalEntry + } + tests := []struct { + name string + args args + }{ + { + name: "nil_JournalEntry", + args: args{ + e: data.JournalEntry{}, + }, + }, + { + name: "Entity_default", + args: args{ + e: data.JournalEntry{ + CorrelationId: "1234", + Payload: "", + Method: "test_method", + Url: "test_url", + SpiffeId: "test_spiffeid", + Event: "test_event", + }, + }, + }, + { + name: "Entity_type_SecretDeleteRequest", + args: args{ + e: data.JournalEntry{ + CorrelationId: "1234", + Payload: toJsonStr(reqres.SecretDeleteRequest{WorkloadIds: []string{"test_workloadid"}, Err: "test_err"}), + Method: "test_method", + Url: "test_url", + SpiffeId: "test_spiffeid", + Event: "test_event", + }, + }, + }, + { + name: "Entity_type_SecretDeleteResponse", + args: args{ + e: data.JournalEntry{ + CorrelationId: "1234", + Payload: toJsonStr(reqres.SecretDeleteResponse{Err: "test_err"}), + Method: "test_method", + Url: "test_url", + SpiffeId: "test_spiffeid", + Event: "test_event", + }, + }, + }, + { + name: "Entity_type_SecretFetchRequest", + args: args{ + e: data.JournalEntry{ + CorrelationId: "1234", + Payload: toJsonStr(reqres.SecretFetchRequest{Err: "test_err"}), + Method: "test_method", + Url: "test_url", + SpiffeId: "test_spiffeid", + Event: "test_event", + }, + }, + }, + { + name: "Entity_type_SecretFetchResponse", + args: args{ + e: data.JournalEntry{ + CorrelationId: "1234", + Payload: toJsonStr(reqres.SecretFetchResponse{Data: "test_data", Created: "test_created", Updated: "test_updated", Err: "test_err"}), + Method: "test_method", + Url: "test_url", + SpiffeId: "test_spiffeid", + Event: "test_event", + }, + }, + }, + { + name: "Entity_type_SecretUpsertRequest", + args: args{ + e: data.JournalEntry{ + CorrelationId: "1234", + Payload: toJsonStr(reqres.SecretUpsertRequest{ + WorkloadIds: []string{"test_workloadid"}, + Namespaces: []string{"test_namespace"}, + Value: "test_value", + Template: "test_template", + Format: data.SecretFormat("test_format"), + }), + }, + }, + }, + { + name: "Entity_type_SecretUpsertResponse", + args: args{ + e: data.JournalEntry{ + CorrelationId: "1234", + Payload: toJsonStr(reqres.SecretUpsertResponse{Err: "test_err"}), + Method: "test_method", + Url: "test_url", + SpiffeId: "test_spiffeid", + Event: "test_event", + }, + }, + }, + { + name: "Entity_type_SecretListRequest", + args: args{ + e: data.JournalEntry{ + CorrelationId: "1234", + Payload: toJsonStr(reqres.SecretListRequest{Err: "test_err"}), + Method: "test_method", + Url: "test_url", + SpiffeId: "test_spiffeid", + Event: "test_event", + }, + }, + }, + { + name: "Entity_type_SecretListResponse", + args: args{ + e: data.JournalEntry{ + CorrelationId: "1234", + Payload: toJsonStr(reqres.SecretListResponse{Err: "test_err"}), + Method: "test_method", + Url: "test_url", + SpiffeId: "test_spiffeid", + Event: "test_event", + }, + }, + }, + { + name: "Entity_type_SecretEncryptedListResponse", + args: args{ + e: data.JournalEntry{ + CorrelationId: "1234", + Payload: toJsonStr(reqres.SecretEncryptedListResponse{Err: "test_err"}), + Method: "test_method", + Url: "test_url", + SpiffeId: "test_spiffeid", + Event: "test_event", + }, + }, + }, + { + name: "Entity_type_KeyInputRequest", + args: args{ + e: data.JournalEntry{ + CorrelationId: "1234", + Payload: toJsonStr(reqres.KeyInputRequest{}), + Method: "test_method", + Url: "test_url", + SpiffeId: "test_spiffeid", + Event: "test_event", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + logLine := captureOutput(func() { + Log(tt.args.e) + }) + // Match the log line against the regular expression + matches := re.FindStringSubmatch(clean(logLine)) + if matches == nil { + t.Fatalf("printAudit() = %v; expected %v", logLine, regexPattern) + } + + // Extract components from the matched groups + _ = matches[1] // date + _ = matches[2] // time + correlationID := matches[3] + event := matches[4] + method := matches[5] + url := matches[6] + spiffeid := matches[7] + payload := matches[8] + + // Check if extracted values match expected arguments + if !(correlationID == tt.args.e.CorrelationId && + event == string(tt.args.e.Event) && + method == tt.args.e.Method && + url == tt.args.e.Url && + spiffeid == tt.args.e.SpiffeId && + payload == tt.args.e.Payload) { + t.Errorf("printAudit() = %v; expected %v", logLine, regexPattern) + } + }) + } +} + +func TestCreateDefaultEntry(t *testing.T) { + type args struct { + cid string + spiffeid string + r *http.Request + } + tests := []struct { + name string + args args + }{ + //{ // This test case is not applicable as the function is not expected to handle nil requests + // name: "nil_http_request", + // args: args{ + // cid: "1234", + // spiffeid: "abcd", + // r: nil, + // }, + //}, + { + name: "empty_values", + args: args{ + cid: "", + spiffeid: "", + r: &http.Request{}, + }, + }, + { + name: "valid_values", + args: args{ + cid: "1234", + spiffeid: "abcd", + r: &http.Request{ + Method: "GET", + RequestURI: "http://localhost:5000/", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + actual := CreateDefaultEntry(tt.args.cid, tt.args.spiffeid, tt.args.r) + if !(actual.CorrelationId == tt.args.cid && + actual.Method == tt.args.r.Method && + actual.Url == tt.args.r.RequestURI && + actual.SpiffeId == tt.args.spiffeid && + actual.Event == audit.Enter) { + t.Errorf("CreateDefaultEntry() = %v; expected %v", actual, tt.args) + } + }) + } +} diff --git a/core/audit/journal/print_test.go b/core/audit/journal/print_test.go index 845a692f..7441482b 100644 --- a/core/audit/journal/print_test.go +++ b/core/audit/journal/print_test.go @@ -9,3 +9,108 @@ */ package journal + +import ( + "github.com/vmware-tanzu/secrets-manager/core/constants/audit" + "log" + "os" + "regexp" + "strings" + "testing" +) + +// Regular expression to match the log line +const regexPattern = `^(\d{4}/\d{2}/\d{2}) (\d{2}:\d{2}:\d{2}) \[AUDIT\]\s*(\d+)?\s*(\w+)?\s*\{\{method:\[\[(.*?)\]\],url:\[\[(.*?)\]\],spiffeid:\[\[(.*?)\]\],payload:\[\[(.*?)\]\]\}\}$` // Updated regex to allow for optional correlation ID and entity name + +func Test_printAudit(t *testing.T) { + // Compile the regular expression + re, err := regexp.Compile(regexPattern) + if err != nil { + t.Fatal(err) + } + + type args struct { + correlationId string + entityName string + method string + url string + spiffeid string + message string + } + tests := []struct { + name string + args args + }{ + { + name: "success_case", + args: args{ + correlationId: "1234", + entityName: "abcd", + method: "GET", + url: "http://localhost:5000/", + spiffeid: "abcd1234", + message: "testing audit func", + }, + }, + { + name: "empty_values", + args: args{ + correlationId: "", + entityName: "", + method: "", + url: "", + spiffeid: "", + message: "", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + logLine := captureOutput(func() { + printAudit(tt.args.correlationId, audit.Event(tt.args.entityName), tt.args.method, tt.args.url, tt.args.spiffeid, tt.args.message) + }) + // Match the log line against the regular expression + matches := re.FindStringSubmatch(clean(logLine)) + if matches == nil { + t.Fatalf("printAudit() = %v; expected %v", logLine, regexPattern) + } + + // Extract components from the matched groups + _ = matches[1] // date + _ = matches[2] // time + correlationID := matches[3] + entityName := matches[4] + method := matches[5] + url := matches[6] + spiffeid := matches[7] + payload := matches[8] + + // Check if extracted values match expected arguments + if !(correlationID == tt.args.correlationId && + entityName == tt.args.entityName && + method == tt.args.method && + url == tt.args.url && + spiffeid == tt.args.spiffeid && + payload == tt.args.message) { + t.Errorf("printAudit() = %v; expected %v", logLine, regexPattern) + } + }) + } +} + +// clean prepares log output for comparison. +func clean(s string) string { + if len(s) > 0 && s[len(s)-1] == '\n' { // Remove trailing newline + s = s[:len(s)-1] + } + return strings.ReplaceAll(s, "\n", "~") // Replace newline with tilde for comparison purposes +} + +// captureOutput captures log output. It sets the log output to a buffer, runs the function, and returns the buffer contents. +func captureOutput(f func()) string { + var buf strings.Builder + log.SetOutput(&buf) + defer log.SetOutput(os.Stderr) + f() + return buf.String() +} diff --git a/core/constants/env/env_test.go b/core/constants/env/env_test.go index 8c30d4d9..0bb5bc1d 100644 --- a/core/constants/env/env_test.go +++ b/core/constants/env/env_test.go @@ -9,3 +9,30 @@ */ package env + +import "testing" + +func TestValue(t *testing.T) { + t.Run("success_case", func(t *testing.T) { + t.Setenv("TEST_ENV", "test") + got := Value("TEST_ENV") + if got != "test" { + t.Errorf("Value() = %v, want %v", got, "test") + } + }) + + t.Run("empty_case", func(t *testing.T) { + t.Setenv("TEST_ENV", "") + got := Value("TEST_ENV") + if got != "" { + t.Errorf("Value() = %v, want %v", got, "") + } + }) + + t.Run("not_set_case", func(t *testing.T) { + got := Value("TEST_ENV") + if got != "" { + t.Errorf("Value() = %v, want %v", got, "") + } + }) +} diff --git a/core/constants/url/url.go b/core/constants/url/url.go index 513774d3..f93898da 100644 --- a/core/constants/url/url.go +++ b/core/constants/url/url.go @@ -1,3 +1,13 @@ +/* +| Protect your secrets, protect your sensitive data. +: Explore VMware Secrets Manager docs at https://vsecm.com/ +/ keep your secrets... secret +>/ +<>/' Copyright 2023-present VMware Secrets Manager contributors. +>/' SPDX-License-Identifier: BSD-2-Clause +*/ + package url const SentinelKeystone = "/sentinel/v1/keystone" diff --git a/core/constants/val/val_test.go b/core/constants/val/val_test.go new file mode 100644 index 00000000..4c71138c --- /dev/null +++ b/core/constants/val/val_test.go @@ -0,0 +1,99 @@ +package val + +import "testing" + +func TestNever(t *testing.T) { + type args struct { + s string + } + tests := []struct { + name string + args args + want bool + }{ + { + name: "never", + args: args{ + s: "never", + }, + want: true, + }, + { + name: "Never", + args: args{ + s: "Never", + }, + want: true, + }, + { + name: "never with space", + args: args{ + s: " never ", + }, + want: true, + }, + { + name: "never with space and caps", + args: args{ + s: " NeVeR ", + }, + want: true, + }, + { + name: "not never", + args: args{ + s: "not never", + }, + want: false, + }, + { + name: "empty", + args: args{ + s: "", + }, + want: false, + }, + { + name: "space", + args: args{ + s: " ", + }, + want: false, + }, + { + name: "never with space", + args: args{ + s: " never", + }, + want: true, + }, + { + name: "never with space", + args: args{ + s: "never ", + }, + want: true, + }, + { + name: "never with space", + args: args{ + s: " never ", + }, + want: true, + }, + { + name: "never with space", + args: args{ + s: " NeVeR ", + }, + want: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := Never(tt.args.s); got != tt.want { + t.Errorf("Never() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/core/entity/v1/data/convert.go b/core/entity/v1/data/convert.go index 770928c6..6e293514 100644 --- a/core/entity/v1/data/convert.go +++ b/core/entity/v1/data/convert.go @@ -14,11 +14,11 @@ import ( "bytes" "encoding/json" "fmt" - "github.com/vmware-tanzu/secrets-manager/core/constants/key" - "github.com/vmware-tanzu/secrets-manager/core/constants/symbol" "strings" "text/template" + "github.com/vmware-tanzu/secrets-manager/core/constants/key" + "github.com/vmware-tanzu/secrets-manager/core/constants/symbol" tpl "github.com/vmware-tanzu/secrets-manager/core/template" ) diff --git a/core/entity/v1/data/crypto.go b/core/entity/v1/data/crypto.go index d5f3123d..16ef0397 100644 --- a/core/entity/v1/data/crypto.go +++ b/core/entity/v1/data/crypto.go @@ -11,8 +11,9 @@ package data import ( - "github.com/vmware-tanzu/secrets-manager/core/constants/symbol" "strings" + + "github.com/vmware-tanzu/secrets-manager/core/constants/symbol" ) type RootKeyCollection struct { diff --git a/core/env/backoff.go b/core/env/backoff.go index b99067fc..5fdae696 100644 --- a/core/env/backoff.go +++ b/core/env/backoff.go @@ -11,10 +11,11 @@ package env import ( - "github.com/vmware-tanzu/secrets-manager/core/constants/env" "strconv" "strings" "time" + + "github.com/vmware-tanzu/secrets-manager/core/constants/env" ) // Redefine some constants to avoid import cycle. diff --git a/core/env/init.go b/core/env/init.go index 855ea799..b9ddc40b 100644 --- a/core/env/init.go +++ b/core/env/init.go @@ -11,9 +11,10 @@ package env import ( - "github.com/vmware-tanzu/secrets-manager/core/constants/env" "strconv" "time" + + "github.com/vmware-tanzu/secrets-manager/core/constants/env" ) // PollIntervalForInitContainer returns the time interval between each poll in the diff --git a/core/env/logging.go b/core/env/logging.go index 66dbfadf..1719eaaf 100644 --- a/core/env/logging.go +++ b/core/env/logging.go @@ -11,10 +11,11 @@ package env import ( - "github.com/vmware-tanzu/secrets-manager/core/constants/env" - "github.com/vmware-tanzu/secrets-manager/core/constants/val" "strconv" "strings" + + "github.com/vmware-tanzu/secrets-manager/core/constants/env" + "github.com/vmware-tanzu/secrets-manager/core/constants/val" ) type Level int diff --git a/core/env/poll.go b/core/env/poll.go index 943eaa90..4c57b79a 100644 --- a/core/env/poll.go +++ b/core/env/poll.go @@ -11,9 +11,10 @@ package env import ( - "github.com/vmware-tanzu/secrets-manager/core/constants/env" "strconv" "time" + + "github.com/vmware-tanzu/secrets-manager/core/constants/env" ) // MaxPollIntervalForSidecar returns the maximum interval for polling by the diff --git a/core/env/store.go b/core/env/store.go index f3a5be20..efebdfe8 100644 --- a/core/env/store.go +++ b/core/env/store.go @@ -11,9 +11,9 @@ package env import ( - "github.com/vmware-tanzu/secrets-manager/core/constants/env" "os" + "github.com/vmware-tanzu/secrets-manager/core/constants/env" "github.com/vmware-tanzu/secrets-manager/core/entity/v1/data" ) diff --git a/core/spiffe/spiffe.go b/core/spiffe/spiffe.go index 7754d5a8..75d533bd 100644 --- a/core/spiffe/spiffe.go +++ b/core/spiffe/spiffe.go @@ -13,9 +13,10 @@ package spiffe import ( "context" "errors" + "github.com/spiffe/go-spiffe/v2/workloadapi" - "github.com/vmware-tanzu/secrets-manager/core/constants/key" + "github.com/vmware-tanzu/secrets-manager/core/constants/key" "github.com/vmware-tanzu/secrets-manager/core/env" log "github.com/vmware-tanzu/secrets-manager/core/log/std" "github.com/vmware-tanzu/secrets-manager/core/validation" diff --git a/go.mod b/go.mod index 569629d1..8cfbbe62 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/akamensky/argparse v1.4.0 github.com/spiffe/go-spiffe/v2 v2.2.0 github.com/stretchr/testify v1.9.0 - golang.org/x/text v0.15.0 + golang.org/x/text v0.17.0 google.golang.org/grpc v1.63.2 google.golang.org/protobuf v1.34.1 gopkg.in/yaml.v3 v3.0.1 @@ -40,14 +40,14 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/zeebo/errs v1.3.0 // indirect - golang.org/x/crypto v0.21.0 // indirect - golang.org/x/mod v0.15.0 // indirect - golang.org/x/net v0.23.0 // indirect + golang.org/x/crypto v0.26.0 // indirect + golang.org/x/mod v0.17.0 // indirect + golang.org/x/net v0.25.0 // indirect golang.org/x/oauth2 v0.17.0 // indirect - golang.org/x/sys v0.18.0 // indirect - golang.org/x/term v0.18.0 // indirect + golang.org/x/sys v0.23.0 // indirect + golang.org/x/term v0.23.0 // indirect golang.org/x/time v0.3.0 // indirect - golang.org/x/tools v0.18.0 // indirect + golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect google.golang.org/appengine v1.6.8 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de // indirect gopkg.in/inf.v0 v0.9.1 // indirect diff --git a/go.sum b/go.sum index 05caf17c..c1d79751 100644 --- a/go.sum +++ b/go.sum @@ -103,11 +103,13 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.15.0 h1:SernR4v+D55NyBH2QiEQrlBAnj1ECL6AGrA5+dPaMY8= golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -117,6 +119,7 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/oauth2 v0.17.0 h1:6m3ZPmLEFdVxKKWnKq4VqZ60gutO35zm+zrAHVmHyDQ= golang.org/x/oauth2 v0.17.0/go.mod h1:OzPDGQiuQMguemayvdylqddI7qcD9lnSDb+1FiwQ5HA= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -134,16 +137,19 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= +golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -154,6 +160,7 @@ golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.18.0 h1:k8NLag8AGHnn+PHbl7g43CtqZAwG60vZkLqgyZgIHgQ= golang.org/x/tools v0.18.0/go.mod h1:GL7B4CwcLLeo59yx/9UWWuNOW1n3VZ4f5axWfML7Lcg= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/lib/spiffe/spiffe.go b/lib/spiffe/spiffe.go index 9172257a..14f9a243 100644 --- a/lib/spiffe/spiffe.go +++ b/lib/spiffe/spiffe.go @@ -2,9 +2,10 @@ package spiffe import ( "errors" + "net/http" + "github.com/spiffe/go-spiffe/v2/spiffeid" "github.com/spiffe/go-spiffe/v2/svid/x509svid" - "net/http" ) // IdFromRequest extracts the SPIFFE ID from the TLS peer certificate of diff --git a/sdk/LICENSE b/sdk/LICENSE new file mode 100644 index 00000000..6dce9f18 --- /dev/null +++ b/sdk/LICENSE @@ -0,0 +1,23 @@ +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/sdk/README.md b/sdk/README.md new file mode 100644 index 00000000..e684ac88 --- /dev/null +++ b/sdk/README.md @@ -0,0 +1,125 @@ +## **VMware Secrets Manager** *for cloud-native apps* + +![VSecM Logo](https://github.com/vmware-tanzu/secrets-manager/assets/1041224/885c11ac-7269-4344-a376-0d0a0fb082a7) + +## VMWare Secrets Manager Go SDK + +VMware Secrets Manager Go SDK is a Go client library for accessing the +VMware Secrets Manager API. It is a part of the VMware Secrets Manager project. + +## Quick Start + +You can use the SDK to interact with the VMware Secrets Manager API. + +Here is a simple example to get started: + +```go +package main + +import ( + "fmt" + "time" + + "github.com/vmware-tanzu/secrets-manager/sdk/sentry" +) + +func main() { + for { + // Fetch the secret bound to this workload + // using VMware Secrets Manager Go SDK: + data, err := sentry.Fetch() + + if err != nil { + fmt.Println("Failed. Will retry...") + } else { + fmt.Println("secret: '", data, "'") + } + + time.Sleep(5 * time.Second) + } +} +``` + +If your application is configured to consume secrets from VMware Secrets Manager, +then the above code will fetch the secret bound to the workload every 5 seconds. + +Here is a sample `Deployment` manifest for the above code: + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: example + namespace: default + labels: + app.kubernetes.io/name: example +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: example + template: + metadata: + labels: + app.kubernetes.io/name: example + spec: + serviceAccountName: example + containers: + - name: main + image: vsecm/example-using-sdk-go:latest + volumeMounts: + - name: spire-agent-socket + mountPath: /spire-agent-socket + readOnly: true + env: + - name: SPIFFE_ENDPOINT_SOCKET + value: unix:///spire-agent-socket/spire-agent.sock + volumes: + - name: spire-agent-socket + csi: + driver: "csi.spiffe.io" + readOnly: true +``` + +## Documentation + +For more information about **VMware Secrets Manager** Go SDK, +see the [official documentation][ducks]. + +[ducks]: https://vsecm.com/documentation/usage/sdk/ + +## Project Structure + +This project uses a slimmed down versions of the parent project's codebase. + +* `./sdk/core/*` is a slimmed-down copy of the parent project's `./core/*`. +* `./sdk/lib/*` is a slimmed-down copy of the parent project's `./lib/*`. + +* `./sentry` and `/.startup` are the main entry points for the SDK. + +## Why Copy the Codebase? + +As the Go experts say: "*A little copying is better than a little dependency.*" + +The reason we copied the codebase is to keep the SDK self-contained and isolated. +This also makes it easier to migrate the SDK to its own repository if a need +arises. + +In addition, this approach allows us to serve the SDK with a lower Go version +and enable projects that have older go versions to still use the VSecM Go SDK. + +## Contributing + +Follow the main project's [contribution guidelines][contributing]. + +[contributing]: ../CONTRIBUTING.md + +## Code of Conduct + +Follow the main project's [code of conduct][coc]. + +[coc]: ../CODE_OF_CONDUCT.md + +## License + +[BSD 2-Clause License](LICENSE). diff --git a/sdk/core/constants/crypto/algorithm.go b/sdk/core/constants/crypto/algorithm.go new file mode 100644 index 00000000..7fbe0674 --- /dev/null +++ b/sdk/core/constants/crypto/algorithm.go @@ -0,0 +1,13 @@ +/* +| Protect your secrets, protect your sensitive data. +: Explore VMware Secrets Manager docs at https://vsecm.com/ +/ keep your secrets... secret +>/ +<>/' Copyright 2023-present VMware Secrets Manager contributors. +>/' SPDX-License-Identifier: BSD-2-Clause +*/ + +package crypto + +type Algorithm string diff --git a/sdk/core/constants/env/env.go b/sdk/core/constants/env/env.go new file mode 100644 index 00000000..be89dcad --- /dev/null +++ b/sdk/core/constants/env/env.go @@ -0,0 +1,46 @@ +/* +| Protect your secrets, protect your sensitive data. +: Explore VMware Secrets Manager docs at https://vsecm.com/ +/ keep your secrets... secret +>/ +<>/' Copyright 2023-present VMware Secrets Manager contributors. +>/' SPDX-License-Identifier: BSD-2-Clause +*/ + +package env + +import ( + "os" +) + +type VarName string + +const SpiffeEndpointSocket VarName = "SPIFFE_ENDPOINT_SOCKET" +const SpiffeTrustDomain VarName = "SPIFFE_TRUST_DOMAIN" +const VSecMInitContainerPollInterval VarName = "VSECM_INIT_CONTAINER_POLL_INTERVAL" +const VSecMLogLevel VarName = "VSECM_LOG_LEVEL" +const VSecMSafeEndpointUrl VarName = "VSECM_SAFE_ENDPOINT_URL" +const VSecMSidecarPollInterval VarName = "VSECM_SIDECAR_POLL_INTERVAL" +const VSecMSidecarSecretsPath VarName = "VSECM_SIDECAR_SECRETS_PATH" +const VSecMSpiffeIdPrefixSafe VarName = "VSECM_SPIFFEID_PREFIX_SAFE" +const VSecMSpiffeIdPrefixWorkload VarName = "VSECM_SPIFFEID_PREFIX_WORKLOAD" +const VSecMWorkloadNameRegExp VarName = "VSECM_WORKLOAD_NAME_REGEXP" + +type VarValue string + +const SpiffeEndpointSocketDefault VarValue = "unix:///spire-agent-socket/spire-agent.sock" +const SpiffeTrustDomainDefault VarValue = "vsecm.com" +const VSecMInitContainerPollIntervalDefault VarValue = "5000" +const VSecMSafeEndpointUrlDefault VarValue = "https://vsecm-safe.vsecm-system.svc.cluster.local:8443/" +const VSecMSidecarPollIntervalDefault VarValue = "20000" +const VSecMSidecarSecretsPathDefault VarValue = "/opt/vsecm/secrets.json" +const VSecMSpiffeIdPrefixSafeDefault VarValue = "^spiffe://vsecm.com/workload/vsecm-safe/ns/vsecm-system/sa/vsecm-safe/n/[^/]+$" +const VSecMSpiffeIdPrefixWorkloadDefault VarValue = "^spiffe://vsecm.com/workload/[^/]+/ns/[^/]+/sa/[^/]+/n/[^/]+$" +const VSecMNameRegExpForWorkloadDefault VarValue = "^spiffe://vsecm.com/workload/([^/]+)/ns/[^/]+/sa/[^/]+/n/[^/]+$" + +func Value(name VarName) string { + return os.Getenv(string(name)) +} + +type FieldName string diff --git a/sdk/core/constants/key/key.go b/sdk/core/constants/key/key.go new file mode 100644 index 00000000..2685e789 --- /dev/null +++ b/sdk/core/constants/key/key.go @@ -0,0 +1,13 @@ +/* +| Protect your secrets, protect your sensitive data. +: Explore VMware Secrets Manager docs at https://vsecm.com/ +/ keep your secrets... secret +>/ +<>/' Copyright 2023-present VMware Secrets Manager contributors. +>/' SPDX-License-Identifier: BSD-2-Clause +*/ + +package key + +const SecretDataValue = "VALUE" diff --git a/sdk/core/constants/symbol/symbol.go b/sdk/core/constants/symbol/symbol.go new file mode 100644 index 00000000..b96a9840 --- /dev/null +++ b/sdk/core/constants/symbol/symbol.go @@ -0,0 +1,15 @@ +/* +| Protect your secrets, protect your sensitive data. +: Explore VMware Secrets Manager docs at https://vsecm.com/ +/ keep your secrets... secret +>/ +<>/' Copyright 2023-present VMware Secrets Manager contributors. +>/' SPDX-License-Identifier: BSD-2-Clause +*/ + +package symbol + +const Separator = ":" +const ItemSeparator = "," +const CollectionDelimiter = "," diff --git a/sdk/core/constants/val/val.go b/sdk/core/constants/val/val.go new file mode 100644 index 00000000..d4d13b7e --- /dev/null +++ b/sdk/core/constants/val/val.go @@ -0,0 +1,16 @@ +/* +| Protect your secrets, protect your sensitive data. +: Explore VMware Secrets Manager docs at https://vsecm.com/ +/ keep your secrets... secret +>/ +<>/' Copyright 2023-present VMware Secrets Manager contributors. +>/' SPDX-License-Identifier: BSD-2-Clause +*/ + +package val + +// JsonEmpty is a constant string representing an empty value. +// This value is generated by the go templating engine +// when a key-value pair has no value. Don't change this value. +const JsonEmpty = "" diff --git a/sdk/core/crypto/crypto.go b/sdk/core/crypto/crypto.go new file mode 100644 index 00000000..d2b29149 --- /dev/null +++ b/sdk/core/crypto/crypto.go @@ -0,0 +1,26 @@ +/* +| Protect your secrets, protect your sensitive data. +: Explore VMware Secrets Manager docs at https://vsecm.com/ +/ keep your secrets... secret +>/ +<>/' Copyright 2023-present VMware Secrets Manager contributors. +>/' SPDX-License-Identifier: BSD-2-Clause +*/ + +package crypto + +import ( + "fmt" + + "github.com/vmware-tanzu/secrets-manager/sdk/lib/crypto" +) + +// Id generates a cryptographically-unique secure random string. +func Id() string { + id, err := crypto.RandomString(8) + if err != nil { + id = fmt.Sprintf("CRYPTO-ERR: %s", err.Error()) + } + return id +} diff --git a/sdk/core/entity/v1/data/convert.go b/sdk/core/entity/v1/data/convert.go new file mode 100644 index 00000000..4d11b4bd --- /dev/null +++ b/sdk/core/entity/v1/data/convert.go @@ -0,0 +1,189 @@ +/* +| Protect your secrets, protect your sensitive data. +: Explore VMware Secrets Manager docs at https://vsecm.com/ +/ keep your secrets... secret +>/ +<>/' Copyright 2023-present VMware Secrets Manager contributors. +>/' SPDX-License-Identifier: BSD-2-Clause +*/ + +package data + +import ( + "bytes" + "encoding/json" + "fmt" + "strings" + "text/template" + + "github.com/vmware-tanzu/secrets-manager/sdk/core/constants/key" + "github.com/vmware-tanzu/secrets-manager/sdk/core/constants/symbol" + tpl "github.com/vmware-tanzu/secrets-manager/sdk/core/template" +) + +// convertMapToStringBytes converts a map[string]string into a map[string][]byte, +// by converting each string value into a []byte, and returns the resulting map. +func convertMapToStringBytes(inputMap map[string]string) map[string][]byte { + data := make(map[string][]byte) + for k, v := range inputMap { + data[k] = []byte(v) + } + return data +} + +// handleTemplateFailure is used when applying a template to the secret's value +// fails. It attempts to unmarshal the 'value' string as JSON into the 'data' +// map. If the unmarshalling fails, it creates a new empty 'data' map and +// populates it with a single entry, "VALUE", containing the original 'value' as +// []byte. +func convertValueToMap(values []string) map[string][]byte { + var data map[string][]byte + + val := "" + if len(values) == 1 { + val = values[0] + } else { + val = strings.Join(values, symbol.CollectionDelimiter) + } + + err := json.Unmarshal([]byte(val), &data) + if err != nil { + data = map[string][]byte{} + data[key.SecretDataValue] = []byte(val) + } + + return data +} + +// handleNoTemplate is used when there is no template defined. +// It attempts to unmarshal the 'value' string as JSON. If successful, it +// returns a map with the JSON key-value pairs converted to []byte values; +// otherwise, it returns a map with a single entry, "VALUE", containing the +// original 'value' as []byte. +func convertValueNoTemplate(values []string) map[string][]byte { + var data map[string][]byte + var jsonData map[string]string + + val := "" + if len(values) == 1 { + val = values[0] + } else { + val = strings.Join(values, symbol.CollectionDelimiter) + } + + err := json.Unmarshal(([]byte)(val), &jsonData) + if err != nil { + //If error in unmarshalling, add the whole as a part of VALUE + data[key.SecretDataValue] = ([]byte)(val) + } else { + //Use the secret's value as a key-val pair + return convertMapToStringBytes(jsonData) + } + + return data +} + +// parseForK8sSecret parses the provided `SecretStored` and applies a template +// if one is defined. +// +// Args: +// +// secret: A SecretStored struct containing the secret data and metadata. +// +// Returns: +// +// A map of string keys to string values, containing the parsed secret data. +// +// If there is an error during parsing or applying the template, an error +// will be returned. +// +// Note that this function will consider only the first value in the `Values` +// collection. If there are multiple values, only the first value will be +// parsed and transformed. +func parseForK8sSecret(secret SecretStored) (map[string]string, error) { + // cannot move this to /core/template because of circular dependency. + + secretData := make(map[string]string) + + if len(secret.Values) == 0 { + return secretData, fmt.Errorf("no values found for secret %s", + secret.Name) + } + + jsonData := strings.TrimSpace(secret.Values[0]) + tmpStr := strings.TrimSpace(secret.Meta.Template) + + err := json.Unmarshal([]byte(jsonData), &secretData) + if err != nil { + return secretData, err + } + + if tmpStr == "" { + return secretData, err + } + + tmpl, err := template.New("secret").Parse(tmpStr) + if err != nil { + return secretData, err + } + + var t bytes.Buffer + err = tmpl.Execute(&t, secretData) + if err != nil { + return secretData, err + } + + output := make(map[string]string) + err = json.Unmarshal(t.Bytes(), &output) + if err != nil { + return output, err + } + + return output, nil +} + +func transform( + value string, tmpStr string, f SecretFormat, +) (string, error) { + jsonData := strings.TrimSpace(value) + + parsedString := "" + if tmpStr == "" { + parsedString = jsonData + } else { + parsedString = tpl.TryParse(tmpStr, jsonData) + } + + switch f { + case Json: + // If the parsed string is a valid JSON, return it as is. + // Otherwise, assume the parsing failed and return the original + // JSON string. + if tpl.ValidJSON(parsedString) { + return parsedString, nil + } else { + return jsonData, nil + } + case Yaml: + if tpl.ValidJSON(parsedString) { + yml, err := tpl.JsonToYaml(parsedString) + if err != nil { + return parsedString, err + } + return yml, nil + } else { + // Parsed string is not a valid JSON, so return it as is. + // It can be either a valid YAML already, or some random string. + // There is not much can be done at this point other than + // returning it. + return parsedString, nil + } + case Raw: + // If the format is Raw, return the parsed string as is. + return parsedString, nil + default: + // The program flow shall never enter here. + return parsedString, fmt.Errorf("unknown format: %s", f) + } +} diff --git a/sdk/core/entity/v1/data/secret.go b/sdk/core/entity/v1/data/secret.go new file mode 100644 index 00000000..9c347be7 --- /dev/null +++ b/sdk/core/entity/v1/data/secret.go @@ -0,0 +1,60 @@ +/* +| Protect your secrets, protect your sensitive data. +: Explore VMware Secrets Manager docs at https://vsecm.com/ +/ keep your secrets... secret +>/ +<>/' Copyright 2023-present VMware Secrets Manager contributors. +>/' SPDX-License-Identifier: BSD-2-Clause +*/ + +package data + +import "github.com/vmware-tanzu/secrets-manager/sdk/lib/entity" + +// SecretFormat represents the format of the secret. +type SecretFormat string + +var ( + Json SecretFormat = "json" + Yaml SecretFormat = "yaml" + Raw SecretFormat = "raw" +) + +// Secret represents the secret that is safe to view. +type Secret struct { + Name string `json:"name"` + Created entity.JsonTime `json:"created"` + Updated entity.JsonTime `json:"updated"` + NotBefore entity.JsonTime `json:"notBefore"` + ExpiresAfter entity.JsonTime `json:"expiresAfter"` +} + +// SecretEncrypted represents the secret with an encrypted value. +// It is still safe to view since the value of it is encrypted. +type SecretEncrypted struct { + Name string `json:"name"` + EncryptedValue []string `json:"value"` + Created entity.JsonTime `json:"created"` + Updated entity.JsonTime `json:"updated"` + NotBefore entity.JsonTime `json:"notBefore"` + ExpiresAfter entity.JsonTime `json:"expiresAfter"` +} + +// SecretMeta represents the metadata of the secret that is not +// directly relevant to the secret itself but provides additional +// context for VSecM Safe's internal operations. +type SecretMeta struct { + // Defaults to "default" + Namespaces []string `json:"namespaces"` + // Go template used to transform the secret. + // Sample secret: + // '{"username":"admin","password":"VSecMRocks"}' + // Sample template: + // '{"USER":"{{.username}}", "PASS":"{{.password}}"}" + Template string `json:"template"` + // Defaults to None + Format SecretFormat + // For tracking purposes + CorrelationId string `json:"correlationId"` +} diff --git a/sdk/core/entity/v1/data/secret_stored.go b/sdk/core/entity/v1/data/secret_stored.go new file mode 100644 index 00000000..c5060721 --- /dev/null +++ b/sdk/core/entity/v1/data/secret_stored.go @@ -0,0 +1,158 @@ +/* +| Protect your secrets, protect your sensitive data. +: Explore VMware Secrets Manager docs at https://vsecm.com/ +/ keep your secrets... secret +>/ +<>/' Copyright 2023-present VMware Secrets Manager contributors. +>/' SPDX-License-Identifier: BSD-2-Clause +*/ + +package data + +import ( + "encoding/json" + "fmt" + "time" +) + +// SecretStored represents a secret stored in VSecM Safe. +type SecretStored struct { + // Name of the secret. + Name string + // Raw values. A secret can have multiple values. Sentinel returns + // a single value if there is a single value in this array. Sentinel + // will return an array of values if there are multiple values in the array. + Values []string `json:"values"` + // Transformed values. This value is the value that workloads see. + // + // Apply transformation (if needed) and then store the value in + // one of the supported formats. If the format is json, ensure that + // a valid JSON is stored here. If the format is yaml, ensure that + // a valid YAML is stored here. If the format is none, then just + // apply transformation (if needed) and do not do any validity check. + ValueTransformed string `json:"valuesTransformed"` + // Additional information that helps format and store the secret. + Meta SecretMeta + // Timestamps + Created time.Time + Updated time.Time + // Invalid before this time. + NotBefore time.Time `json:"notBefore"` + // Invalid after this time. + ExpiresAfter time.Time `json:"expiresAfter"` +} + +// ToMapForK8s returns a map that can be used to create a Kubernetes secret. +// +// 1. If there is no template, attempt to unmarshal the secret's value +// into a map. If that fails, store the secret's value under the "VALUE" key. +// 2. If there is a template, attempt to parse it. If parsing is successful, +// create a new map with the parsed data. If parsing fails, follow the same +// logic as in case 1, attempting to unmarshal the secret's value into a map, +// and if that fails, storing the secret's value under the "VALUE" key. +func (secret SecretStored) ToMapForK8s() map[string][]byte { + data := make(map[string][]byte) + + // If there are no values, return an empty map. + if len(secret.Values) == 0 { + return data + } + + // If there is no template, use the secret's value as is. + if secret.Meta.Template == "" { + return convertValueNoTemplate(secret.Values) + } + + // Otherwise, apply the template. + newData, err := parseForK8sSecret(secret) + if err == nil { + return convertMapToStringBytes(newData) + } + + // If the template fails, use the secret's value as is. + return convertValueToMap(secret.Values) +} + +// ToMap converts the SecretStored struct to a map[string]any. +// The resulting map contains the following key-value pairs: +// +// "Name": the Name field of the SecretStored struct +// "Values": the Values field of the SecretStored struct +// "Created": the Created field of the SecretStored struct +// "Updated": the Updated field of the SecretStored struct +func (secret SecretStored) ToMap() map[string]any { + return map[string]any{ + "Name": secret.Name, + "Values": secret.Values, + "Created": secret.Created, + "Updated": secret.Updated, + } +} + +// Parse takes a data.SecretStored type as input and returns the parsed +// string or an error. +// +// It parses all the `.Values` of the secret, and for each value tries to apply +// a template transformation. +// +// Here is how the template transformation is applied: +// +// 1. Compute parsedString: +// If the Meta.Template field is empty, then parsedString is the original +// value. Otherwise, parsedString is the result of applying the template +// transformation to the original value. +// +// 2. Compute the output string: +// - If the Meta.Format field is Json, then the output string is parsedString +// if parsedString is a valid JSON, otherwise it's the original value. +// - If the Meta.Format field is Yaml, then the output string is the result of +// transforming parsedString into Yaml if parsedString is a valid JSON, +// otherwise it's parsedString. +// - If the Meta.Format field is Raw, then the output string is simply the +// parsedString, without any specific format checks or transformations. +func (secret SecretStored) Parse() (string, error) { + if len(secret.Values) == 0 { + return "", fmt.Errorf("no values found for secret %s", secret.Name) + } + + parseFailed := false + var results []string + for _, v := range secret.Values { + transformed, err := transform(v, + secret.Meta.Template, secret.Meta.Format) + if err != nil { + parseFailed = true + continue + } + if transformed == "" { + continue + } + results = append(results, transformed) + } + + if results == nil { + return "", fmt.Errorf("failed to parse secret %s", secret.Name) + } + + if len(results) == 1 { + // Can happen if there are N values, but only 1 was successfully parsed. + if parseFailed { + return results[0], + fmt.Errorf("failed to parse secret %s", secret.Name) + } + + return results[0], nil + } + + marshaled, err := json.Marshal(results) + if err != nil { + return "", err + } + if parseFailed { + return string(marshaled), + fmt.Errorf("failed to parse secret %s", secret.Name) + } + + return string(marshaled), nil +} diff --git a/sdk/core/entity/v1/data/status.go b/sdk/core/entity/v1/data/status.go new file mode 100644 index 00000000..911b2169 --- /dev/null +++ b/sdk/core/entity/v1/data/status.go @@ -0,0 +1,56 @@ +/* +| Protect your secrets, protect your sensitive data. +: Explore VMware Secrets Manager docs at https://vsecm.com/ +/ keep your secrets... secret +>/ +<>/' Copyright 2023-present VMware Secrets Manager contributors. +>/' SPDX-License-Identifier: BSD-2-Clause +*/ + +package data + +import "sync" + +// InitStatus is the initialization status of VSecM Sentinel +// and other VSecM components. +type InitStatus string + +var ( + Pending InitStatus = "pending" + Ready InitStatus = "ready" +) + +// Status is a struct representing the current state of the secret manager, +// including the lengths and capacities of the secret queues and the total +// number of secrets stored. +type Status struct { + SecretQueueLen int + SecretQueueCap int + K8sQueueLen int + K8sQueueCap int + NumSecrets int + Lock sync.RWMutex +} + +// Increment is a method for the Status struct that increments the NumSecrets +// field by 1 if the provided secret name is not found in the in-memory store. +func (s *Status) Increment(name string, loader func(name any) (any, bool)) { + s.Lock.Lock() + defer s.Lock.Unlock() + _, ok := loader(name) + if !ok { + s.NumSecrets++ + } +} + +// Decrement is a method for the Status struct that decrements the NumSecrets +// field by 1 if the provided secret name is found in the in-memory store. +func (s *Status) Decrement(name string, loader func(name any) (any, bool)) { + s.Lock.Lock() + defer s.Lock.Unlock() + _, ok := loader(name) + if ok { + s.NumSecrets-- + } +} diff --git a/sdk/core/entity/v1/reqres/safe/safe.go b/sdk/core/entity/v1/reqres/safe/safe.go new file mode 100644 index 00000000..4faffca7 --- /dev/null +++ b/sdk/core/entity/v1/reqres/safe/safe.go @@ -0,0 +1,122 @@ +/* +| Protect your secrets, protect your sensitive data. +: Explore VMware Secrets Manager docs at https://vsecm.com/ +/ keep your secrets... secret +>/ +<>/' Copyright 2023-present VMware Secrets Manager contributors. +>/' SPDX-License-Identifier: BSD-2-Clause +*/ + +package safe + +import ( + "github.com/vmware-tanzu/secrets-manager/sdk/core/constants/crypto" + "github.com/vmware-tanzu/secrets-manager/sdk/core/entity/v1/data" +) + +// SecretUpsertRequest is the request to upsert a secret. +type SecretUpsertRequest struct { + WorkloadIds []string `json:"workloads"` + Namespaces []string `json:"namespaces"` + Value string `json:"value"` + Template string `json:"template"` + Format data.SecretFormat `json:"format"` + Encrypt bool `json:"encrypt"` + AppendValue bool `json:"appendValue"` + NotBefore string `json:"notBefore"` + Expires string `json:"expires"` + + Err string `json:"err,omitempty"` +} + +// SecretUpsertResponse is the response to upsert a secret. +type SecretUpsertResponse struct { + Err string `json:"err,omitempty"` +} + +// KeyInputRequest is the request to provide new root encryption keys +// to VSecM Safe. +type KeyInputRequest struct { + AgeSecretKey string `json:"ageSecretKey"` + AgePublicKey string `json:"agePublicKey"` + AesCipherKey string `json:"aesCipherKey"` + Err string `json:"err,omitempty"` +} + +// SentinelInitCompleteRequest is the request to notify that VSecM Sentinel +// has completed initialization. +type SentinelInitCompleteRequest struct { + Err string `json:"err,omitempty"` +} + +// SentinelInitCompleteResponse is the response to SentinelInitCompleteRequest. +type SentinelInitCompleteResponse struct { + Err string `json:"err,omitempty"` +} + +// SecretFetchRequest is the request to fetch a secret. +type SecretFetchRequest struct { + Err string `json:"err,omitempty"` +} + +// SecretFetchResponse is the response to a SecretFetchRequest. +type SecretFetchResponse struct { + Data string `json:"data"` + Created string `json:"created"` + Updated string `json:"updated"` + Err string `json:"err,omitempty"` +} + +// SecretDeleteRequest is the request to delete a secret. +type SecretDeleteRequest struct { + WorkloadIds []string `json:"workloads"` + Err string `json:"err,omitempty"` +} + +// SecretDeleteResponse is the response to a SecretDeleteRequest. +type SecretDeleteResponse struct { + Err string `json:"err,omitempty"` +} + +// SecretListRequest is the request to list secrets. +// The response will not contain the secret values. +type SecretListRequest struct { + Err string `json:"err,omitempty"` +} + +// SecretListResponse is the response to a SecretListRequest. +type SecretListResponse struct { + Secrets []data.Secret `json:"secrets"` + Err string `json:"err,omitempty"` +} + +// SecretEncryptedListResponse is the response that lists secrets +// The secret values will be encrypted. +type SecretEncryptedListResponse struct { + Secrets []data.SecretEncrypted `json:"secrets"` + Algorithm crypto.Algorithm `json:"algorithm"` + Err string `json:"err,omitempty"` +} + +// KeystoneStatusRequest is the request to check the status of +// VSecM Keystone. +type KeystoneStatusRequest struct { + Err string `json:"err,omitempty"` +} + +// KeystoneStatusResponse is the response to a KeystoneStatusRequest. +type KeystoneStatusResponse struct { + Status data.InitStatus `json:"status"` + Err string `json:"err,omitempty"` +} + +// GenericRequest is the request for generic operations. +type GenericRequest struct { + Err string `json:"err,omitempty"` +} + +// GenericResponse is the response for generic operations. +type GenericResponse struct { + Err string `json:"err,omitempty"` +} diff --git a/sdk/core/env/endpoint.go b/sdk/core/env/endpoint.go new file mode 100644 index 00000000..a7d53f20 --- /dev/null +++ b/sdk/core/env/endpoint.go @@ -0,0 +1,27 @@ +/* +| Protect your secrets, protect your sensitive data. +: Explore VMware Secrets Manager docs at https://vsecm.com/ +/ keep your secrets... secret +>/ +<>/' Copyright 2023-present VMware Secrets Manager contributors. +>/' SPDX-License-Identifier: BSD-2-Clause +*/ + +package env + +import ( + "github.com/vmware-tanzu/secrets-manager/sdk/core/constants/env" +) + +// EndpointUrlForSafe returns the URL for the VSecM Safe endpoint +// used in the VMware Secrets Manager system. +// The URL is obtained from the environment variable VSECM_SAFE_ENDPOINT_URL. +// If the variable is not set, the default URL is used. +func EndpointUrlForSafe() string { + u := env.Value(env.VSecMSafeEndpointUrl) + if u == "" { + u = string(env.VSecMSafeEndpointUrlDefault) + } + return u +} diff --git a/sdk/core/env/init.go b/sdk/core/env/init.go new file mode 100644 index 00000000..eb76e56f --- /dev/null +++ b/sdk/core/env/init.go @@ -0,0 +1,39 @@ +/* +| Protect your secrets, protect your sensitive data. +: Explore VMware Secrets Manager docs at https://vsecm.com/ +/ keep your secrets... secret +>/ +<>/' Copyright 2023-present VMware Secrets Manager contributors. +>/' SPDX-License-Identifier: BSD-2-Clause +*/ + +package env + +import ( + "strconv" + "time" + + "github.com/vmware-tanzu/secrets-manager/sdk/core/constants/env" +) + +// PollIntervalForInitContainer returns the time interval between each poll in the +// Watch function. The interval is specified in milliseconds as the +// VSECM_INIT_CONTAINER_POLL_INTERVAL environment variable. If the environment +// variable is not set or is not a valid integer value, the function returns the +// default interval of 5000 milliseconds. +func PollIntervalForInitContainer() time.Duration { + p := env.Value(env.VSecMInitContainerPollInterval) + d, _ := strconv.Atoi(string(env.VSecMInitContainerPollIntervalDefault)) + if p == "" { + p = string(env.VSecMInitContainerPollIntervalDefault) + } + + i, err := strconv.ParseInt(p, 10, 32) + if err != nil { + i = int64(d) + return time.Duration(i) * time.Millisecond + } + + return time.Duration(i) * time.Millisecond +} diff --git a/sdk/core/env/logging.go b/sdk/core/env/logging.go new file mode 100644 index 00000000..3d698d64 --- /dev/null +++ b/sdk/core/env/logging.go @@ -0,0 +1,73 @@ +/* +| Protect your secrets, protect your sensitive data. +: Explore VMware Secrets Manager docs at https://vsecm.com/ +/ keep your secrets... secret +>/ +<>/' Copyright 2023-present VMware Secrets Manager contributors. +>/' SPDX-License-Identifier: BSD-2-Clause +*/ + +package env + +import ( + "github.com/vmware-tanzu/secrets-manager/sdk/core/constants/env" + "strconv" +) + +type Level int + +// Redefine log levels to avoid import cycle. +const ( + Off Level = iota + Fatal + Error + Warn + Info + Audit + Debug + Trace +) + +var level = struct { + Off Level + Fatal Level + Error Level + Warn Level + Info Level + Audit Level + Debug Level + Trace Level +}{ + Off: Off, + Fatal: Fatal, + Error: Error, + Warn: Warn, + Info: Info, + Audit: Audit, + Debug: Debug, + Trace: Trace, +} + +// LogLevel returns the value set by VSECM_LOG_LEVEL environment +// variable, or a default level. +// +// VSECM_LOG_LEVEL determines the verbosity of the logs. +// 0: logs are off, 7: highest verbosity (TRACE). +func LogLevel() int { + p := env.Value(env.VSecMLogLevel) + if p == "" { + return int(level.Warn) + } + + l, _ := strconv.Atoi(p) + if l == int(level.Off) { + return int(level.Warn) + } + + if l < int(level.Off) || l > int(level.Trace) { + return int(level.Warn) + } + + return l +} diff --git a/sdk/core/env/poll.go b/sdk/core/env/poll.go new file mode 100644 index 00000000..c67043c5 --- /dev/null +++ b/sdk/core/env/poll.go @@ -0,0 +1,38 @@ +/* +| Protect your secrets, protect your sensitive data. +: Explore VMware Secrets Manager docs at https://vsecm.com/ +/ keep your secrets... secret +>/ +<>/' Copyright 2023-present VMware Secrets Manager contributors. +>/' SPDX-License-Identifier: BSD-2-Clause +*/ + +package env + +import ( + "strconv" + "time" + + "github.com/vmware-tanzu/secrets-manager/sdk/core/constants/env" +) + +// PollIntervalForSidecar returns the polling interval for sentry in time.Duration +// The interval is determined by the VSECM_SIDECAR_POLL_INTERVAL environment +// variable, with a default value of 20000 milliseconds if the variable is not +// set or if there is an error in parsing the value. +func PollIntervalForSidecar() time.Duration { + p := env.Value(env.VSecMSidecarPollInterval) + d, _ := strconv.Atoi(string(env.VSecMSidecarPollIntervalDefault)) + if p == "" { + p = string(env.VSecMSidecarPollIntervalDefault) + } + + i, err := strconv.ParseInt(p, 10, 32) + if err != nil { + i = int64(d) + return time.Duration(i) * time.Millisecond + } + + return time.Duration(i) * time.Millisecond +} diff --git a/sdk/core/env/sidecar.go b/sdk/core/env/sidecar.go new file mode 100644 index 00000000..ddd16521 --- /dev/null +++ b/sdk/core/env/sidecar.go @@ -0,0 +1,26 @@ +/* +| Protect your secrets, protect your sensitive data. +: Explore VMware Secrets Manager docs at https://vsecm.com/ +/ keep your secrets... secret +>/ +<>/' Copyright 2023-present VMware Secrets Manager contributors. +>/' SPDX-License-Identifier: BSD-2-Clause +*/ + +package env + +import ( + "github.com/vmware-tanzu/secrets-manager/sdk/core/constants/env" +) + +// SecretsPathForSidecar returns the path to the secrets file used by the sidecar. +// The path is determined by the VSECM_SIDECAR_SECRETS_PATH environment variable, +// with a default value of "/opt/vsecm/secrets.json" if the variable is not set. +func SecretsPathForSidecar() string { + p := env.Value(env.VSecMSidecarSecretsPath) + if p == "" { + p = string(env.VSecMSidecarSecretsPathDefault) + } + return p +} diff --git a/sdk/core/env/spiffe.go b/sdk/core/env/spiffe.go new file mode 100644 index 00000000..7a489eb2 --- /dev/null +++ b/sdk/core/env/spiffe.go @@ -0,0 +1,44 @@ +/* +| Protect your secrets, protect your sensitive data. +: Explore VMware Secrets Manager docs at https://vsecm.com/ +/ keep your secrets... secret +>/ +<>/' Copyright 2023-present VMware Secrets Manager contributors. +>/' SPDX-License-Identifier: BSD-2-Clause +*/ + +package env + +import ( + "github.com/vmware-tanzu/secrets-manager/sdk/core/constants/env" +) + +// SpiffeSocketUrl returns the URL for the SPIFFE endpoint socket used in the +// VMware Secrets Manager system. The URL is obtained from the environment variable +// SPIFFE_ENDPOINT_SOCKET. If the variable is not set, the default URL is used. +func SpiffeSocketUrl() string { + p := env.Value(env.SpiffeEndpointSocket) + if p == "" { + p = string(env.SpiffeEndpointSocketDefault) + } + return p +} + +// SpiffeTrustDomain retrieves the SPIFFE trust domain from environment +// variables. +// +// This function looks for the trust domain using the environment variable +// defined by `constants.SpiffeTrustDomain`. If the environment variable is not +// set or is an empty string, it defaults to the value specified by +// `constants.SpiffeTrustDomainDefault`. +// +// Returns: +// - A string representing the SPIFFE trust domain. +func SpiffeTrustDomain() string { + p := env.Value(env.SpiffeTrustDomain) + if p == "" { + p = string(env.SpiffeTrustDomainDefault) + } + return p +} diff --git a/sdk/core/env/spiffeid.go b/sdk/core/env/spiffeid.go new file mode 100644 index 00000000..60a64d44 --- /dev/null +++ b/sdk/core/env/spiffeid.go @@ -0,0 +1,52 @@ +/* +| Protect your secrets, protect your sensitive data. +: Explore VMware Secrets Manager docs at https://vsecm.com/ +/ keep your secrets... secret +>/ +<>/' Copyright 2023-present VMware Secrets Manager contributors. +>/' SPDX-License-Identifier: BSD-2-Clause +*/ + +package env + +import ( + "github.com/vmware-tanzu/secrets-manager/sdk/core/constants/env" +) + +// SpiffeIdPrefixForSafe returns the prefix for the Safe SPIFFE ID. +// The prefix is obtained from the environment variable +// VSECM_SPIFFEID_PREFIX_SAFE. If the variable is not set, the default prefix is +// used. +func SpiffeIdPrefixForSafe() string { + p := env.Value(env.VSecMSpiffeIdPrefixSafe) + if p == "" { + p = string(env.VSecMSpiffeIdPrefixSafeDefault) + } + return p +} + +// SpiffeIdPrefixForWorkload returns the prefix for the Workload's SPIFFE ID. +// The prefix is obtained from the environment variable +// VSECM_SPIFFEID_PREFIX_WORKLOAD. +// If the variable is not set, the default prefix is used. +func SpiffeIdPrefixForWorkload() string { + p := env.Value(env.VSecMSpiffeIdPrefixWorkload) + if p == "" { + p = string(env.VSecMSpiffeIdPrefixWorkloadDefault) + } + return p +} + +// NameRegExpForWorkload returns the regular expression pattern for extracting +// the workload name from the SPIFFE ID. +// The prefix is obtained from the environment variable +// VSECM_NAME_REGEXP_FOR_WORKLOAD. +// If the variable is not set, the default pattern is used. +func NameRegExpForWorkload() string { + p := env.Value(env.VSecMWorkloadNameRegExp) + if p == "" { + p = string(env.VSecMNameRegExpForWorkloadDefault) + } + return p +} diff --git a/sdk/core/log/level/level.go b/sdk/core/log/level/level.go new file mode 100644 index 00000000..20456c42 --- /dev/null +++ b/sdk/core/log/level/level.go @@ -0,0 +1,45 @@ +/* +| Protect your secrets, protect your sensitive data. +: Explore VMware Secrets Manager docs at https://vsecm.com/ +/ keep your secrets... secret +>/ +<>/' Copyright 2023-present VMware Secrets Manager contributors. +>/' SPDX-License-Identifier: BSD-2-Clause +*/ + +package level + +import ( + "sync" + + "github.com/vmware-tanzu/secrets-manager/sdk/core/env" +) + +// Level represents log levels. +type Level int + +// Define log levels as constants. +const ( + Off Level = iota + Fatal + Error + Warn + Info + Audit + Debug + Trace +) + +var mux sync.RWMutex // Protects access to currentLevel. + +// Initialize currentLevel with the value from the environment. +var currentLevel = Level(env.LogLevel()) + +// Get retrieves the current global log level. +func Get() Level { + mux.RLock() + defer mux.RUnlock() + + return currentLevel +} diff --git a/sdk/core/log/std/log.go b/sdk/core/log/std/log.go new file mode 100644 index 00000000..8cf32322 --- /dev/null +++ b/sdk/core/log/std/log.go @@ -0,0 +1,25 @@ +/* +| Protect your secrets, protect your sensitive data. +: Explore VMware Secrets Manager docs at https://vsecm.com/ +/ keep your secrets... secret +>/ +<>/' Copyright 2023-present VMware Secrets Manager contributors. +>/' SPDX-License-Identifier: BSD-2-Clause +*/ + +// Package std provides a simple and flexible logging library with various +// log levels. +package std + +import "github.com/vmware-tanzu/secrets-manager/sdk/core/log/level" + +// InfoLn logs an info level message. +func InfoLn(correlationID *string, v ...any) { + logMessage(level.Info, "[INFO]", correlationID, v...) +} + +// TraceLn logs a trace level message. +func TraceLn(correlationID *string, v ...any) { + logMessage(level.Trace, "[TRACE]", correlationID, v...) +} diff --git a/sdk/core/log/std/print.go b/sdk/core/log/std/print.go new file mode 100644 index 00000000..94674d4f --- /dev/null +++ b/sdk/core/log/std/print.go @@ -0,0 +1,39 @@ +/* +| Protect your secrets, protect your sensitive data. +: Explore VMware Secrets Manager docs at https://vsecm.com/ +/ keep your secrets... secret +>/ +<>/' Copyright 2023-present VMware Secrets Manager contributors. +>/' SPDX-License-Identifier: BSD-2-Clause +*/ + +package std + +import ( + "github.com/vmware-tanzu/secrets-manager/sdk/core/log/level" + "log" +) + +// logMessage logs a message with the specified level, correlation ID, and +// message arguments. It checks the current log level to decide if the message +// should be logged. +func logMessage(l level.Level, prefix string, correlationID *string, v ...any) { + if l != level.Audit && level.Get() < l { + return + } + + args := make([]any, 0, len(v)+2) + args = append(args, prefix) + if correlationID != nil { + args = append(args, *correlationID) + } + args = append(args, v...) + + if l == level.Fatal { + log.Fatalln(args...) + return + } + + log.Println(args...) +} diff --git a/sdk/core/template/filter.go b/sdk/core/template/filter.go new file mode 100644 index 00000000..51d02e35 --- /dev/null +++ b/sdk/core/template/filter.go @@ -0,0 +1,47 @@ +/* +| Protect your secrets, protect your sensitive data. +: Explore VMware Secrets Manager docs at https://vsecm.com/ +/ keep your secrets... secret +>/ +<>/' Copyright 2023-present VMware Secrets Manager contributors. +>/' SPDX-License-Identifier: BSD-2-Clause +*/ + +package template + +import ( + "strings" + + "github.com/vmware-tanzu/secrets-manager/sdk/core/constants/symbol" + "github.com/vmware-tanzu/secrets-manager/sdk/core/constants/val" +) + +// removeKeyValueWithNoValue takes an input string containing key-value pairs +// and filters out pairs where the value is "". It splits the input +// string into key-value pairs, iterates through them, and retains only the +// pairs with values that are not equal to "". +// The function then joins the filtered pairs back into a string and returns the +// resulting string. This function effectively removes key-value pairs with +// "" from the input string. Helpful for data cleaning and filtering +// when you want to omit certain key/value pairs from a template. +func removeKeyValueWithNoValue(input string) string { + // Split the input string into key-value pairs + pairs := strings.Split(input, symbol.ItemSeparator) + + // Initialize a slice to store the filtered pairs + var filteredPairs []string + + for _, pair := range pairs { + keyValue := strings.SplitN(pair, symbol.Separator, 2) + if len(keyValue) == 2 && keyValue[1] != val.JsonEmpty { + // Add the pair to the filtered pairs if the value is not + // "" + filteredPairs = append(filteredPairs, pair) + } + } + + // Join the filtered pairs back into a string + result := strings.Join(filteredPairs, ",") + return result +} diff --git a/sdk/core/template/template.go b/sdk/core/template/template.go new file mode 100644 index 00000000..ec7de398 --- /dev/null +++ b/sdk/core/template/template.go @@ -0,0 +1,86 @@ +/* +| Protect your secrets, protect your sensitive data. +: Explore VMware Secrets Manager docs at https://vsecm.com/ +/ keep your secrets... secret +>/ +<>/' Copyright 2023-present VMware Secrets Manager contributors. +>/' SPDX-License-Identifier: BSD-2-Clause +*/ + +package template + +import ( + "bytes" + "encoding/json" + "text/template" + + "gopkg.in/yaml.v3" +) + +// ValidJSON checks if the provided string is a valid JSON object. +// +// The function takes a string as input and attempts to unmarshal it +// into a map[string]any using the JSON package. If the unmarshalling +// is successful, it returns true, indicating that the string is a valid JSON +// object. Otherwise, it returns false. +func ValidJSON(s string) bool { + var js map[string]any + return json.Unmarshal([]byte(s), &js) == nil +} + +// JsonToYaml converts a JSON string into a YAML string. +// +// The function takes a JSON string as input and attempts to unmarshal it +// into an empty interface. If the unmarshalling is successful, it marshals +// the data back into a YAML string using the YAML package. +// +// On success, the function returns the YAML string and a nil error. +// If there is any error during the conversion, it returns an empty string +// and the corresponding error. +func JsonToYaml(js string) (string, error) { + var jsonObj any + err := json.Unmarshal([]byte(js), &jsonObj) + if err != nil { + return "", err + } + yamlBytes, err := yaml.Marshal(jsonObj) + if err != nil { + return "", err + } + return string(yamlBytes), nil +} + +// TryParse attempts to parse and execute a template with the given JSON string. +// +// The function takes two string inputs - a template string (tmpStr) and a JSON +// string. It attempts to parse the template string using the "text/template" +// package. If there is any error during parsing, the function returns the +// original JSON string. +// +// If the template is parsed successfully, the function attempts to execute the +// template using the provided JSON string as input data. If there is any error +// during execution, the function returns the original JSON string. +// +// On successful execution, the function returns the resulting string from the +// executed template. +func TryParse(tmpStr, jason string) string { + tmpl, err := template.New("secret").Parse(tmpStr) + if err != nil { + return jason + } + + var result map[string]any + err = json.Unmarshal([]byte(jason), &result) + if err != nil { + return jason + } + + var tpl bytes.Buffer + err = tmpl.Execute(&tpl, result) + if err != nil { + return jason + } + + return removeKeyValueWithNoValue(tpl.String()) +} diff --git a/sdk/core/validation/validation.go b/sdk/core/validation/validation.go new file mode 100644 index 00000000..f8ff7c0b --- /dev/null +++ b/sdk/core/validation/validation.go @@ -0,0 +1,173 @@ +/* +| Protect your secrets, protect your sensitive data. +: Explore VMware Secrets Manager docs at https://vsecm.com/ +/ keep your secrets... secret +>/ +<>/' Copyright 2023-present VMware Secrets Manager contributors. +>/' SPDX-License-Identifier: BSD-2-Clause +*/ + +package validation + +import ( + "regexp" + "strings" + + e "github.com/vmware-tanzu/secrets-manager/sdk/core/constants/env" + "github.com/vmware-tanzu/secrets-manager/sdk/core/env" +) + +// Any SPIFFE ID regular expression matcher shall start with the +// `^spiffe://$trustDomain` prefix for extra security. +// +// This variable shall be treated as constant and should not be modified. +var spiffeRegexPrefixStart = "^spiffe://" + env.SpiffeTrustDomain() + "/" +var spiffeIdPrefixStart = "spiffe://" + env.SpiffeTrustDomain() + "/" + +// IsWorkload checks if a given SPIFFE ID belongs to a workload. +// +// A SPIFFE ID (SPIFFE IDentifier) is a URI that uniquely identifies a workload +// in a secure, interoperable way. This function verifies if the provided +// SPIFFE ID meets the criteria to be classified as a workload ID based on +// certain environmental settings. +// +// The function performs the following checks: +// 1. If the `spiffeid` starts with a "^", it assumed that it is a regular +// expression pattern, it compiles the expression and checks if the SPIFFE +// ID matches it. +// 2. Otherwise, it checks if the SPIFFE ID starts with the proper prefix. +// +// Parameters: +// +// spiffeid (string): The SPIFFE ID to be checked. +// +// Returns: +// +// bool: `true` if the SPIFFE ID belongs to a workload, `false` otherwise. +func IsWorkload(spiffeid string) bool { + prefix := env.SpiffeIdPrefixForWorkload() + + if strings.HasPrefix(prefix, spiffeRegexPrefixStart) { + re, err := regexp.Compile(prefix) + if err != nil { + panic( + "Failed to compile the regular expression pattern " + + "for SPIFFE ID." + + " Check the " + string(e.VSecMSpiffeIdPrefixWorkload) + + " environment variable. " + + " val: " + env.SpiffeIdPrefixForWorkload() + + " trust: " + env.SpiffeTrustDomain(), + ) + return false + } + + nrw := env.NameRegExpForWorkload() + wre, err := regexp.Compile(nrw) + if err != nil { + panic( + "Failed to compile the regular expression pattern " + + "for SPIFFE ID." + + " Check the " + string(e.VSecMWorkloadNameRegExp) + + " environment variable." + + " val: " + env.NameRegExpForWorkload() + + " trust: " + env.SpiffeTrustDomain(), + ) + return false + } + + match := wre.FindStringSubmatch(spiffeid) + if len(match) == 0 { + return false + } + + return re.MatchString(spiffeid) + } + + if !strings.HasPrefix(spiffeid, spiffeIdPrefixStart) { + return false + } + + nrw := env.NameRegExpForWorkload() + if !strings.HasPrefix(nrw, spiffeRegexPrefixStart) { + + // Insecure configuration detected. + // Panic to prevent further issues: + panic( + "Invalid regular expression pattern for SPIFFE ID." + + " Expected: ^spiffe:///..." + + " Check the " + string(e.VSecMWorkloadNameRegExp) + + " environment variable." + + " val: " + env.NameRegExpForWorkload() + + " trust: " + env.SpiffeTrustDomain(), + ) + return false + } + + wre, err := regexp.Compile(nrw) + if err != nil { + panic( + "Failed to compile the regular expression pattern " + + "for SPIFFE ID." + + " Check the " + string(e.VSecMWorkloadNameRegExp) + + " environment variable." + + " val: " + env.NameRegExpForWorkload() + + " trust: " + env.SpiffeTrustDomain(), + ) + return false + } + + match := wre.FindStringSubmatch(spiffeid) + if len(match) == 0 { + return false + } + + return strings.HasPrefix(spiffeid, prefix) +} + +// IsSafe checks if a given SPIFFE ID belongs to VSecM Safe. +// +// A SPIFFE ID (SPIFFE IDentifier) is a URI that uniquely identifies a workload +// in a secure, interoperable way. This function verifies if the provided +// SPIFFE ID meets the criteria to be classified as a workload ID based on +// certain environmental settings. +// +// The function performs the following checks: +// 1. If the `spiffeid` starts with a "^", it assumed that it is a regular +// expression pattern, it compiles the expression and checks if the SPIFFE +// ID matches it. +// 2. Otherwise, it checks if the SPIFFE ID starts with the proper prefix. +// +// Parameters: +// +// spiffeid (string): The SPIFFE ID to be checked. +// +// Returns: +// +// bool: `true` if the SPIFFE ID belongs to VSecM Safe, `false` otherwise. +func IsSafe(spiffeid string) bool { + if !IsWorkload(spiffeid) { + return false + } + + prefix := env.SpiffeIdPrefixForSafe() + + if strings.HasPrefix(prefix, spiffeRegexPrefixStart) { + re, err := regexp.Compile(prefix) + if err != nil { + panic( + "Failed to compile the regular expression pattern " + + "for Sentinel SPIFFE ID." + + " Check the " + string(e.VSecMSpiffeIdPrefixSafe) + + " environment variable." + + " val: " + env.SpiffeIdPrefixForSafe() + + " trust: " + env.SpiffeTrustDomain(), + ) + return false + } + + return re.MatchString(spiffeid) + } + + return strings.HasPrefix(spiffeid, prefix) +} diff --git a/sdk/go.mod b/sdk/go.mod new file mode 100644 index 00000000..8abc43fe --- /dev/null +++ b/sdk/go.mod @@ -0,0 +1,24 @@ +module github.com/vmware-tanzu/secrets-manager/sdk + +go 1.21 + +toolchain go1.22.3 + +require ( + github.com/spiffe/go-spiffe/v2 v2.3.0 + golang.org/x/text v0.17.0 + gopkg.in/yaml.v3 v3.0.1 +) + +require ( + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/go-jose/go-jose/v4 v4.0.2 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/zeebo/errs v1.3.0 // indirect + golang.org/x/crypto v0.26.0 // indirect + golang.org/x/net v0.23.0 // indirect + golang.org/x/sys v0.23.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 // indirect + google.golang.org/grpc v1.64.0 // indirect + google.golang.org/protobuf v1.34.2 // indirect +) diff --git a/sdk/go.sum b/sdk/go.sum new file mode 100644 index 00000000..b0febf99 --- /dev/null +++ b/sdk/go.sum @@ -0,0 +1,40 @@ +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-jose/go-jose/v4 v4.0.2 h1:R3l3kkBds16bO7ZFAEEcofK0MkrAJt3jlJznWZG0nvk= +github.com/go-jose/go-jose/v4 v4.0.2/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/spiffe/go-spiffe/v2 v2.3.0 h1:g2jYNb/PDMB8I7mBGL2Zuq/Ur6hUhoroxGQFyD6tTj8= +github.com/spiffe/go-spiffe/v2 v2.3.0/go.mod h1:Oxsaio7DBgSNqhAO9i/9tLClaVlfRok7zvJnTV8ZyIY= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/zeebo/errs v1.3.0 h1:hmiaKqgYZzcVgRL1Vkc1Mn2914BbzB0IBxs+ebeutGs= +github.com/zeebo/errs v1.3.0/go.mod h1:sgbWHsvVuTPHcqJJGQ1WhI5KbWlHYz+2+2C/LSEtCw4= +golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= +golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= +golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= +golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= +golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= +golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237 h1:NnYq6UN9ReLM9/Y01KWNOWyI5xQ9kbIms5GGJVwS/Yc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240318140521-94a12d6c2237/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= +google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY= +google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/sdk/lib/backoff/retry.go b/sdk/lib/backoff/retry.go new file mode 100644 index 00000000..226528df --- /dev/null +++ b/sdk/lib/backoff/retry.go @@ -0,0 +1,147 @@ +/* +| Protect your secrets, protect your sensitive data. +: Explore VMware Secrets Manager docs at https://vsecm.com/ +/ keep your secrets... secret +>/ +<>/' Copyright 2023-present VMware Secrets Manager contributors. +>/' SPDX-License-Identifier: BSD-2-Clause +*/ + +package backoff + +import ( + "math" + "math/rand" + "time" + + "github.com/vmware-tanzu/secrets-manager/sdk/core/crypto" + log "github.com/vmware-tanzu/secrets-manager/sdk/core/log/std" +) + +// Strategy is a configuration for the backoff strategy to use when retrying +// operations. +type Strategy struct { + // Maximum number of retries before giving up (inclusive) + // Default is 10 + MaxRetries int64 // Maximum number of retries before giving up (inclusive) + + // Initial delay between retries (in milliseconds). + Delay time.Duration + + // Whether to use exponential backoff or not (if false, constant delay + // (plus a random jitter) is used) + // Default is false + Exponential bool + // Maximum duration to wait between retries (in milliseconds) + // Default is 10 seconds + MaxWait time.Duration +} + +// Retry implements a retry mechanism for a function that can fail +// (return an error). +// It accepts a scope for logging or identification purposes, a function that +// it will attempt to execute, and a strategy defining the behavior of the retry +// logic. +// +// The retry strategy allows for setting maximum retries, initial delay, whether +// to use exponential backoff, and a maximum duration for the delay. If +// exponential backoff is enabled, the delay between retries increases +// exponentially with each attempt, combined with a small randomization to +// prevent synchronization issues (thundering herd problem). +// If the function succeeds (returns nil), Retry will terminate early. +// If all retries are exhausted, the last error is returned. +// +// Params: +// +// scope string - A descriptive name or identifier for the context of the retry +// operation. +// f func() error - The function to execute and retry if it fails. +// s Strategy - Struct defining the retry parameters including maximum retries, +// delay strategy, and max delay. +// +// Returns: +// +// error - The last error returned by the function after all retries, or nil +// if the function eventually succeeds. +// +// Example of usage: +// +// err := Retry("database_connection", connectToDatabase, Strategy{ +// MaxRetries: 5, +// Delay: 100, +// Exponential: true, +// MaxWait: 10 * time.Second, +// }) +// if err != nil { +// fmt.Println("Failed to connect to database after retries:", err) +// } +func Retry(scope string, f func() error, s Strategy) error { + cid := crypto.Id() + + s = withDefaults(s) + var err error + + log.TraceLn(&cid, "Retry: starting retry loop") + + for i := 0; i <= int(s.MaxRetries); i++ { + err = f() + + log.TraceLn(&cid, "Retry: executed the function") + + if err == nil { + log.TraceLn(&cid, "Retry: success") + return nil + } + + var multiplier float64 = 1 + + // if exponential backoff is enabled then delay increases exponentially: + if s.Exponential { + multiplier = math.Pow(2, float64(i)) + } + + sDelayMs := s.Delay.Milliseconds() + if sDelayMs == 0 { + sDelayMs = 10 + } + + delayMs := multiplier * float64(sDelayMs) + delay := time.Duration(delayMs) * time.Millisecond + + // Some randomness to avoid the thundering herd problem. + jitter := rand.Intn(int(sDelayMs)) + delay += time.Duration(jitter) * time.Millisecond + if delay > s.MaxWait { + delay = s.MaxWait + } + + log.TraceLn(&cid, "Retry: will sleep:", delay) + + time.Sleep(delay) + + log.TraceLn(&cid, + "Retrying after", delay, "ms for the scope", + scope, "-- attempt", i+1, "of", s.MaxRetries+1, + ) + } + + return err +} + +type Mode string + +// withDefaults sets default values for the strategy if they are not set. +func withDefaults(s Strategy) Strategy { + if s.MaxRetries == 0 { + s.MaxRetries = 5 + } + if s.Delay == 0 { + s.Delay = 1000 + } + if s.Exponential && s.MaxWait == 0 { + s.MaxWait = 10 * time.Second + } + + return s +} diff --git a/sdk/lib/crypto/crypto.go b/sdk/lib/crypto/crypto.go new file mode 100644 index 00000000..cef0082e --- /dev/null +++ b/sdk/lib/crypto/crypto.go @@ -0,0 +1,33 @@ +/* +| Protect your secrets, protect your sensitive data. +: Explore VMware Secrets Manager docs at https://vsecm.com/ +/ keep your secrets... secret +>/ +<>/' Copyright 2023-present VMware Secrets Manager contributors. +>/' SPDX-License-Identifier: BSD-2-Clause +*/ + +package crypto + +import "crypto/rand" + +const letters = "abcdefghijklmnopqrstuvwxyz" + + "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + +var reader = rand.Read + +// RandomString generates a cryptographically-unique secure random string. +func RandomString(n int) (string, error) { + bytes := make([]byte, n) + + if _, err := reader(bytes); err != nil { + return "", err + } + + for i, b := range bytes { + bytes[i] = letters[b%byte(len(letters))] + } + + return string(bytes), nil +} diff --git a/sdk/lib/entity/json_time.go b/sdk/lib/entity/json_time.go new file mode 100644 index 00000000..191f9476 --- /dev/null +++ b/sdk/lib/entity/json_time.go @@ -0,0 +1,68 @@ +/* +| Protect your secrets, protect your sensitive data. +: Explore VMware Secrets Manager docs at https://vsecm.com/ +/ keep your secrets... secret +>/ +<>/' Copyright 2023-present VMware Secrets Manager contributors. +>/' SPDX-License-Identifier: BSD-2-Clause +*/ + +package entity + +import ( + "fmt" + "strings" + "time" +) + +// JsonTime wraps the standard time.Time type to provide JSON serialization and +// deserialization in RFC3339 format. This type ensures that the JSON +// representation of dates and times in Go applications follows a standard and +// easily interchangeable format. +type JsonTime time.Time + +// MarshalJSON converts the JsonTime value to a JSON-formatted string in +// RFC3339 format. This method ensures JsonTime can be directly marshaled into +// a JSON string. +// +// Returns: +// - A byte slice containing the JSON-formatted date and time string. +// - An error if the formatting fails, though in practice this method should +// not error out since the time formatting used (RFC3339) is a valid and +// supported format. +func (t *JsonTime) MarshalJSON() ([]byte, error) { + stamp := fmt.Sprintf("\"%s\"", time.Time(*t).Format(time.RFC3339)) + return []byte(stamp), nil +} + +// String returns the JsonTime as a string formatted according to RFC3339. +// This method provides a standard way to convert a JsonTime object to a +// human-readable string. +func (t *JsonTime) String() string { + return time.Time(*t).Format(time.RFC3339) +} + +// UnmarshalJSON parses a JSON-formatted string in RFC3339 format and sets +// the JsonTime accordingly. This method enables JsonTime to directly receive +// and parse time information from JSON data. +// +// Parameters: +// - data: a byte slice containing the JSON string to be parsed. +// +// Returns: +// - An error if the string is not in valid RFC3339 format or if the parsing +// fails. +func (t *JsonTime) UnmarshalJSON(data []byte) error { + str := string(data) + str = strings.Trim(str, "\"") + + parsedTime, err := time.Parse(time.RFC3339, str) + if err != nil { + return err + } + + *t = JsonTime(parsedTime) + + return nil +} diff --git a/sdk/sentry/fetch.go b/sdk/sentry/fetch.go index dd062d03..73a4582a 100644 --- a/sdk/sentry/fetch.go +++ b/sdk/sentry/fetch.go @@ -22,11 +22,11 @@ import ( "github.com/spiffe/go-spiffe/v2/spiffetls/tlsconfig" "github.com/spiffe/go-spiffe/v2/workloadapi" - reqres "github.com/vmware-tanzu/secrets-manager/core/entity/v1/reqres/safe" - "github.com/vmware-tanzu/secrets-manager/core/env" - log "github.com/vmware-tanzu/secrets-manager/core/log/std" - "github.com/vmware-tanzu/secrets-manager/core/validation" - c "github.com/vmware-tanzu/secrets-manager/lib/crypto" + reqres "github.com/vmware-tanzu/secrets-manager/sdk/core/entity/v1/reqres/safe" + "github.com/vmware-tanzu/secrets-manager/sdk/core/env" + log "github.com/vmware-tanzu/secrets-manager/sdk/core/log/std" + "github.com/vmware-tanzu/secrets-manager/sdk/core/validation" + c "github.com/vmware-tanzu/secrets-manager/sdk/lib/crypto" ) // ErrSecretNotFound is returned when the secret is not found. diff --git a/sdk/sentry/privates.go b/sdk/sentry/privates.go index 1e57122f..263ce666 100644 --- a/sdk/sentry/privates.go +++ b/sdk/sentry/privates.go @@ -15,7 +15,7 @@ import ( "errors" "os" - "github.com/vmware-tanzu/secrets-manager/core/env" + "github.com/vmware-tanzu/secrets-manager/sdk/core/env" ) func saveData(data string) error { diff --git a/sdk/sentry/watch.go b/sdk/sentry/watch.go index 31346514..c9ae43db 100644 --- a/sdk/sentry/watch.go +++ b/sdk/sentry/watch.go @@ -11,10 +11,10 @@ package sentry import ( - "github.com/vmware-tanzu/secrets-manager/core/env" - log "github.com/vmware-tanzu/secrets-manager/core/log/std" - "github.com/vmware-tanzu/secrets-manager/lib/backoff" - "github.com/vmware-tanzu/secrets-manager/lib/crypto" + "github.com/vmware-tanzu/secrets-manager/sdk/core/env" + log "github.com/vmware-tanzu/secrets-manager/sdk/core/log/std" + "github.com/vmware-tanzu/secrets-manager/sdk/lib/backoff" + "github.com/vmware-tanzu/secrets-manager/sdk/lib/crypto" ) // Watch synchronizes the internal state of the sidecar by talking to diff --git a/sdk/startup/watch.go b/sdk/startup/watch.go index 7c0effdd..01592516 100644 --- a/sdk/startup/watch.go +++ b/sdk/startup/watch.go @@ -14,9 +14,9 @@ import ( "os" "time" - "github.com/vmware-tanzu/secrets-manager/core/env" - log "github.com/vmware-tanzu/secrets-manager/core/log/std" - "github.com/vmware-tanzu/secrets-manager/lib/crypto" + "github.com/vmware-tanzu/secrets-manager/sdk/core/env" + log "github.com/vmware-tanzu/secrets-manager/sdk/core/log/std" + "github.com/vmware-tanzu/secrets-manager/sdk/lib/crypto" ) // Watch continuously polls the associated secret of the workload to exist.