From 9b3f315683882a80376f276637a4f4ac608d9da8 Mon Sep 17 00:00:00 2001 From: Nimish Date: Mon, 26 Aug 2024 11:47:45 +0530 Subject: [PATCH 1/6] feat: parse AppID over AppName --- phase/misc/misc.go | 38 ++++++++++++++++++-------------------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/phase/misc/misc.go b/phase/misc/misc.go index 95e0ced..bfa2e1a 100644 --- a/phase/misc/misc.go +++ b/phase/misc/misc.go @@ -5,12 +5,12 @@ import ( "strings" ) -// phaseGetContext finds the matching application and environment, returning their IDs and the public key. -func PhaseGetContext(userData AppKeyResponse, appName, envName string) (string, string, string, error) { +// PhaseGetContext finds the matching application and environment, returning their IDs and the public key. +func PhaseGetContext(userData AppKeyResponse, opts GetContextOptions) (string, string, string, error) { for _, app := range userData.Apps { - if app.Name == appName { + if (opts.AppID != "" && app.ID == opts.AppID) || (opts.AppName != "" && app.Name == opts.AppName) { for _, envKey := range app.EnvironmentKeys { - if envKey.Environment.Name == envName { + if envKey.Environment.Name == opts.EnvName { return app.ID, envKey.Environment.ID, envKey.IdentityKey, nil } } @@ -20,23 +20,21 @@ func PhaseGetContext(userData AppKeyResponse, appName, envName string) (string, } // FindEnvironmentKey searches for an environment key with case-insensitive and partial matching. -func FindEnvironmentKey(userData AppKeyResponse, envName, appName string) (*EnvironmentKey, error) { - // Convert envName and appName to lowercase for case-insensitive comparison - lcEnvName := strings.ToLower(envName) - lcAppName := strings.ToLower(appName) +func FindEnvironmentKey(userData AppKeyResponse, opts FindEnvironmentKeyOptions) (*EnvironmentKey, error) { + lcEnvName := strings.ToLower(opts.EnvName) + lcAppName := strings.ToLower(opts.AppName) - for _, app := range userData.Apps { - // Support partial and case-insensitive matching for appName - if appName == "" || strings.Contains(strings.ToLower(app.Name), lcAppName) { - for _, envKey := range app.EnvironmentKeys { - // Support partial and case-insensitive matching for envName - if strings.Contains(strings.ToLower(envKey.Environment.Name), lcEnvName) { - return &envKey, nil - } - } - } - } - return nil, fmt.Errorf("environment key not found for app '%s' and environment '%s'", appName, envName) + for _, app := range userData.Apps { + if (opts.AppID != "" && app.ID == opts.AppID) || + (opts.AppName != "" && (opts.AppName == "" || strings.Contains(strings.ToLower(app.Name), lcAppName))) { + for _, envKey := range app.EnvironmentKeys { + if strings.Contains(strings.ToLower(envKey.Environment.Name), lcEnvName) { + return &envKey, nil + } + } + } + } + return nil, fmt.Errorf("environment key not found for app '%s' (ID: %s) and environment '%s'", opts.AppName, opts.AppID, opts.EnvName) } // normalizeTag replaces underscores with spaces and converts the string to lower case. From a91029f3e5dd5a89df2e65715ca083bda979778a Mon Sep 17 00:00:00 2001 From: Nimish Date: Mon, 26 Aug 2024 11:48:03 +0530 Subject: [PATCH 2/6] feat: updated types, added AppID --- phase/phase.go | 197 ++++++++++++++++++++++++++++--------------------- 1 file changed, 112 insertions(+), 85 deletions(-) diff --git a/phase/phase.go b/phase/phase.go index fcc16d1..1b6f7b9 100644 --- a/phase/phase.go +++ b/phase/phase.go @@ -24,40 +24,45 @@ type Phase struct { } type GetSecretOptions struct { - EnvName string - AppName string - KeyToFind string - Tag string - SecretPath string + EnvName string + AppName string + AppID string + KeyToFind string + Tag string + SecretPath string } type GetAllSecretsOptions struct { - EnvName string - AppName string - Tag string - SecretPath string + EnvName string + AppName string + AppID string + Tag string + SecretPath string } type CreateSecretsOptions struct { - KeyValuePairs []map[string]string - EnvName string - AppName string - SecretPath map[string]string + KeyValuePairs []map[string]string + EnvName string + AppName string + AppID string + SecretPath map[string]string } type SecretUpdateOptions struct { - EnvName string - AppName string - Key string - Value string - SecretPath string + EnvName string + AppName string + AppID string + Key string + Value string + SecretPath string } type DeleteSecretOptions struct { - EnvName string - AppName string - KeyToDelete string - SecretPath string + EnvName string + AppName string + AppID string + KeyToDelete string + SecretPath string } // Init initializes a new instance of Phase with the provided service token, host, and debug flag. @@ -167,31 +172,35 @@ func (p *Phase) resolveSecretValue(value string, currentEnvName string) (string, // Get fetches and decrypts a secret, resolving any secret references within its value. func (p *Phase) Get(opts GetSecretOptions) (*map[string]interface{}, error) { - // Fetch user data - resp, err := network.FetchPhaseUser(p.AppToken, p.Host) - if err != nil { - if p.Debug { - log.Printf("Failed to fetch user data: %v", err) - } - return nil, err - } - defer resp.Body.Close() - - var userData misc.AppKeyResponse - if err := json.NewDecoder(resp.Body).Decode(&userData); err != nil { - if p.Debug { - log.Printf("Failed to decode user data: %v", err) - } - return nil, err - } - - envKey, err := misc.FindEnvironmentKey(userData, opts.EnvName, opts.AppName) - if err != nil { - if p.Debug { - log.Printf("Failed to find environment key: %v", err) - } - return nil, err - } + // Fetch user data + resp, err := network.FetchPhaseUser(p.AppToken, p.Host) + if err != nil { + if p.Debug { + log.Printf("Failed to fetch user data: %v", err) + } + return nil, err + } + defer resp.Body.Close() + + var userData misc.AppKeyResponse + if err := json.NewDecoder(resp.Body).Decode(&userData); err != nil { + if p.Debug { + log.Printf("Failed to decode user data: %v", err) + } + return nil, err + } + + envKey, err := misc.FindEnvironmentKey(userData, misc.FindEnvironmentKeyOptions{ + EnvName: opts.EnvName, + AppName: opts.AppName, + AppID: opts.AppID, + }) + if err != nil { + if p.Debug { + log.Printf("Failed to find environment key: %v", err) + } + return nil, err + } decryptedSeed, err := crypto.DecryptWrappedKeyShare(envKey.WrappedSeed, p.Keyshare0, p.AppToken, p.Keyshare1UnwrapKey, p.PssUserPublicKey, p.Host) if err != nil { @@ -302,14 +311,17 @@ func (p *Phase) GetAll(opts GetAllSecretsOptions) ([]map[string]interface{}, err return nil, err } - // Identify the correct environment and application - envKey, err := misc.FindEnvironmentKey(userData, opts.EnvName, opts.AppName) - if err != nil { - if p.Debug { - log.Printf("Failed to find environment key: %v", err) - } - return nil, err - } + envKey, err := misc.FindEnvironmentKey(userData, misc.FindEnvironmentKeyOptions{ + EnvName: opts.EnvName, + AppName: opts.AppName, + AppID: opts.AppID, + }) + if err != nil { + if p.Debug { + log.Printf("Failed to find environment key: %v", err) + } + return nil, err + } // Decrypt the wrapped seed decryptedSeed, err := crypto.DecryptWrappedKeyShare(envKey.WrappedSeed, p.Keyshare0, p.AppToken, p.Keyshare1UnwrapKey, p.PssUserPublicKey, p.Host) @@ -415,22 +427,29 @@ func (p *Phase) Create(opts CreateSecretsOptions) error { return err } - _, envID, publicKey, err := misc.PhaseGetContext(userData, opts.AppName, opts.EnvName) - if err != nil { - if p.Debug { - log.Printf("Failed to get context: %v", err) - } - return err - } - - // Identify the correct environment and application - envKey, err := misc.FindEnvironmentKey(userData, opts.EnvName, opts.AppName) - if err != nil { - if p.Debug { - log.Printf("Failed to find environment key: %v", err) - } - return err - } + _, envID, publicKey, err := misc.PhaseGetContext(userData, misc.GetContextOptions{ + AppName: opts.AppName, + AppID: opts.AppID, + EnvName: opts.EnvName, + }) + if err != nil { + if p.Debug { + log.Printf("Failed to get context: %v", err) + } + return err + } + + envKey, err := misc.FindEnvironmentKey(userData, misc.FindEnvironmentKeyOptions{ + EnvName: opts.EnvName, + AppName: opts.AppName, + AppID: opts.AppID, + }) + if err != nil { + if p.Debug { + log.Printf("Failed to find environment key: %v", err) + } + return err + } decryptedSalt, err := crypto.DecryptWrappedKeyShare(envKey.WrappedSalt, p.Keyshare0, p.AppToken, p.Keyshare1UnwrapKey, p.PssUserPublicKey, p.Host) if err != nil { @@ -518,13 +537,17 @@ func (p *Phase) Update(opts SecretUpdateOptions) error { return err } - envKey, err := misc.FindEnvironmentKey(userData, opts.EnvName, opts.AppName) - if err != nil { - if p.Debug { - log.Printf("Failed to find environment key: %v", err) - } - return err - } + envKey, err := misc.FindEnvironmentKey(userData, misc.FindEnvironmentKeyOptions{ + EnvName: opts.EnvName, + AppName: opts.AppName, + AppID: opts.AppID, + }) + if err != nil { + if p.Debug { + log.Printf("Failed to find environment key: %v", err) + } + return err + } decryptedSalt, err := crypto.DecryptWrappedKeyShare(envKey.WrappedSalt, p.Keyshare0, p.AppToken, p.Keyshare1UnwrapKey, p.PssUserPublicKey, p.Host) if err != nil { @@ -625,13 +648,17 @@ func (p *Phase) Delete(opts DeleteSecretOptions) error { return err } - envKey, err := misc.FindEnvironmentKey(userData, opts.EnvName, opts.AppName) - if err != nil { - if p.Debug { - log.Printf("Failed to find environment key: %v", err) - } - return err - } + envKey, err := misc.FindEnvironmentKey(userData, misc.FindEnvironmentKeyOptions{ + EnvName: opts.EnvName, + AppName: opts.AppName, + AppID: opts.AppID, + }) + if err != nil { + if p.Debug { + log.Printf("Failed to find environment key: %v", err) + } + return err + } decryptedSalt, err := crypto.DecryptWrappedKeyShare(envKey.WrappedSalt, p.Keyshare0, p.AppToken, p.Keyshare1UnwrapKey, p.PssUserPublicKey, p.Host) if err != nil { From 6cbeb1d22426b9fc184025c40619186ef04c6cf9 Mon Sep 17 00:00:00 2001 From: Nimish Date: Mon, 26 Aug 2024 11:48:22 +0530 Subject: [PATCH 3/6] chore: added types, bumped version --- phase/misc/const.go | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/phase/misc/const.go b/phase/misc/const.go index 8e867a0..1c9f695 100644 --- a/phase/misc/const.go +++ b/phase/misc/const.go @@ -5,7 +5,7 @@ import ( ) const ( - Version = "1.0" + Version = "1.1" PhVersion = "v1" PhaseCloudAPIHost = "https://console.phase.dev" ) @@ -21,7 +21,7 @@ var ( PssUserPattern = regexp.MustCompile(`^pss_user:v(\d+):([a-fA-F0-9]{64}):([a-fA-F0-9]{64}):([a-fA-F0-9]{64}):([a-fA-F0-9]{64})$`) PssServicePattern = regexp.MustCompile(`^pss_service:v(\d+):([a-fA-F0-9]{64}):([a-fA-F0-9]{64}):([a-fA-F0-9]{64}):([a-fA-F0-9]{64})$`) - //CrossEnvPattern = regexp.MustCompile(`\$\{(.+?)\.(.+?)\}`) + // CrossEnvPattern = regexp.MustCompile(`\$\{(.+?)\.(.+?)\}`) // LocalRefPattern = regexp.MustCompile(`\$\{([^.]+?)\}`) // Regex to identify secret references @@ -58,3 +58,15 @@ type AppKeyResponse struct { WrappedKeyShare string `json:"wrapped_key_share"` Apps []App `json:"apps"` } + +type GetContextOptions struct { + AppName string + AppID string + EnvName string +} + +type FindEnvironmentKeyOptions struct { + EnvName string + AppName string + AppID string +} \ No newline at end of file From 471a7392d48144a6c68d47ebebb6a555cb44a179 Mon Sep 17 00:00:00 2001 From: Nimish Date: Mon, 26 Aug 2024 11:56:07 +0530 Subject: [PATCH 4/6] feat: updated README --- README.md | 87 ++++++++++++++++++++++++++++--------------------------- 1 file changed, 44 insertions(+), 43 deletions(-) diff --git a/README.md b/README.md index ffdb429..99fd92b 100644 --- a/README.md +++ b/README.md @@ -1,57 +1,55 @@ -# Phase Secrets Management SDK +# Phase Secrets Management SDK for Go -The Phase Secrets SDK provides a Go package for managing secrets in your application environments using the Phase service. This SDK let's you create, retrieve, update, and delete secrets, with end-to-end encryption with just a few lines of code. +The Phase Secrets SDK provides a Go package for managing secrets in your application environments using the Phase service. This SDK allows you to create, retrieve, update, and delete secrets with end-to-end encryption using just a few lines of code. -## Features: +## Features -- End-to-end encrypting secret CRUD -- Cross and local env secret referencing -- Built in handling of rate limiting +- End-to-end encrypted secret CRUD operations +- Cross-environment and local environment secret referencing +- Bulk secret operations -### Secret referencing syntax: +### Secret Referencing Syntax -| Reference syntax | Environment | Path | Secret Key Being Referenced | Description | -| --------------------------------- | ---------------- | --------------------------------- | --------------------------- | ------------------------------------------------------------------ | -| `${KEY}` | same environment | `/` | KEY | Local reference in the same environment and path root (/). | -| `${staging.DEBUG}` | `dev` | `/` (root of staging environment) | DEBUG | Cross-environment reference to a secret at the root (/). | -| `${prod./frontend/SECRET_KEY}` | `prod` | `/frontend/` | SECRET_KEY | Cross-environment reference to a secret in a specific path. | -| `${/backend/payments/STRIPE_KEY}` | same environment | `/backend/payments/` | STRIPE_KEY | Local reference with a specified path within the same environment. | +| Reference Syntax | Environment | Path | Secret Key | Description | +|-----------------------------------|------------------|-----------------------------------|------------------------|-------------------------------------------------------------| +| `${KEY}` | Same environment | `/` | KEY | Local reference in the same environment and root path (/). | +| `${staging.DEBUG}` | `staging` | `/` (root of staging environment) | DEBUG | Cross-environment reference to a secret at the root (/). | +| `${prod./frontend/SECRET_KEY}` | `prod` | `/frontend/` | SECRET_KEY | Cross-environment reference to a secret in a specific path. | +| `${/backend/payments/STRIPE_KEY}` | Same environment | `/backend/payments/` | STRIPE_KEY | Local reference with a specified path. | ## Installation This SDK uses the `sodium` package to perform cryptographic operations, on most system you will need to install the `libsodium` library as a system dependency. Here's how you can install `libsodium` or its development packages on different platforms, including macOS, Ubuntu, Debian, Arch Linux, Alpine Linux, and Windows. -### macOS +This SDK uses the `sodium` package for cryptographic operations. On most systems, you'll need to install the `libsodium` library as a system dependency. +#### macOS ```sh brew install libsodium ``` -## Fedora - +#### Fedora ```sh sudo dnf install libsodium-devel ``` -### Ubuntu and Debian - +#### Ubuntu and Debian ```sh sudo apt-get update && sudo apt-get install libsodium-dev ``` -### Arch Linux - +#### Arch Linux ```sh sudo pacman -Syu libsodium ``` -### Alpine Linux - +#### Alpine Linux ```sh sudo apk add libsodium-dev ``` -### Windows +#### Windows +For Windows, download pre-built binaries from the [libsodium GitHub releases page](https://github.com/jedisct1/libsodium/releases). Choose the appropriate version for your system architecture and follow the included instructions. On Windows, the process is a bit different due to the variety of development environments. However, you can download pre-built binaries from the official [libsodium GitHub releases page](https://github.com/jedisct1/libsodium/releases). Choose the appropriate version for your system architecture (e.g., Win32 or Win64), download it, and follow the instructions included to integrate `libsodium` with your development environment. For development with Visual Studio, you'll typically include the header files and link against the `libsodium.lib` or `libsodium.dll` file. @@ -66,15 +64,15 @@ If you're using a package manager like `vcpkg` or `chocolatey`, you can also fin choco install libsodium ``` -Remember, after installing the library, you might need to configure your project or environment variables to locate the `libsodium` libraries correctly, especially on Windows. +### Installing the SDK -Next, start using the Phase SDK in your Go project, install it using `go get`: +To start using the Phase SDK in your Go project, install it using `go get`: ```bash go get github.com/phasehq/golang-sdk/phase ``` -Make sure to import the SDK in your Go files: +Import the SDK in your Go files: ```go import "github.com/phasehq/golang-sdk/phase" @@ -82,7 +80,7 @@ import "github.com/phasehq/golang-sdk/phase" ## Configuration -Before you can interact with the Phase service, you need to initialize the SDK with your service token and the host information. +Initialize the SDK with your service token and host information: ```go package main @@ -94,8 +92,8 @@ import ( func main() { serviceToken := "pss_service:v1:....." - host := "https://console.phase.dev" // Change this for a self hosted instance of Phase - debug := false + host := "https://console.phase.dev" // Change this for a self-hosted instance of Phase + debug := false // For logging verbosity, disable in production phaseClient := phase.Init(serviceToken, host, debug) if phaseClient == nil { @@ -104,9 +102,11 @@ func main() { } ``` -## Creating a Secret +## Usage + +### Creating a Secret -To create new secrets, define key-value pairs, specify the environment and application name, and optionally set paths for each key. +Define key-value pairs, specify the environment and application (using either name or ID), and optionally set paths for each key: ```go opts := phase.CreateSecretsOptions{ @@ -114,7 +114,7 @@ opts := phase.CreateSecretsOptions{ {"API_KEY": "api_secret"}, }, EnvName: "Production", - AppName: "MyApp", + AppName: "MyApp", // Or use AppID: "app-id-here" SecretPath: map[string]string{"API_KEY": "/api/keys"}, // Optional, default path: / } @@ -124,14 +124,14 @@ if err != nil { } ``` -## Retrieving a Secret +### Retrieving a Secret -To retrieve a secret, provide the environment name, application name, key to find, and optionally a tag and path. +Provide the environment name, application (name or ID), key to find, and optionally a tag and path: ```go getOpts := phase.GetSecretOptions{ EnvName: "Production", - AppName: "MyApp", + AppName: "MyApp", // Or use AppID: "app-id-here" KeyToFind: "API_KEY", } @@ -143,14 +143,14 @@ if err != nil { } ``` -## Updating a Secret +### Updating a Secret -To update an existing secret, provide the new value along with the environment name, application name, key, and optionally the path. +Provide the new value along with the environment name, application (name or ID), key, and optionally the path: ```go updateOpts := phase.SecretUpdateOptions{ EnvName: "Production", - AppName: "MyApp", + AppName: "MyApp", // Or use AppID: "app-id-here" Key: "API_KEY", Value: "my_updated_api_secret", SecretPath: "/api/keys", // Optional, default path: / @@ -162,14 +162,14 @@ if err != nil { } ``` -## Deleting a Secret +### Deleting a Secret -To delete a secret, specify the environment name, application name, key to delete, and optionally the path. +Specify the environment name, application (name or ID), key to delete, and optionally the path: ```go deleteOpts := phase.DeleteSecretOptions{ EnvName: "Production", - AppName: "MyApp", + AppName: "MyApp", // Or use AppID: "app-id-here" KeyToDelete: "API_KEY", SecretPath: "/api/keys", // Optional, default path: / } @@ -180,6 +180,7 @@ if err != nil { } ``` -For more information and advanced usage, refer to the [Phase Docs](https://docs.phase.dev/sdks/go). +For more information on advanced usage, including detailed API references and best practices, please refer to the [Phase Docs](https://docs.phase.dev/sdks/go). + ---- +If you encounter any issues or have questions, please file an issue on the [GitHub repository](https://github.com/phasehq/golang-sdk) or contact our support team over [Slack](https://slack.phase.dev). From ee6e54feb40d61ddf09ad10e6bb19916cd08d32c Mon Sep 17 00:00:00 2001 From: Nimish Date: Mon, 26 Aug 2024 12:59:54 +0530 Subject: [PATCH 5/6] fix: version --- phase/misc/const.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/phase/misc/const.go b/phase/misc/const.go index 1c9f695..0abfea4 100644 --- a/phase/misc/const.go +++ b/phase/misc/const.go @@ -5,7 +5,7 @@ import ( ) const ( - Version = "1.1" + Version = "1.0.1" PhVersion = "v1" PhaseCloudAPIHost = "https://console.phase.dev" ) From 5a25197d90f3962e345f03304884368baa892915 Mon Sep 17 00:00:00 2001 From: Nimish Date: Mon, 26 Aug 2024 17:44:40 +0530 Subject: [PATCH 6/6] feat: add support for custom user agent for SDK wrappers --- phase/network/network.go | 40 +++++++++++++++++++++++++--------------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/phase/network/network.go b/phase/network/network.go index e1f6d48..60f8382 100644 --- a/phase/network/network.go +++ b/phase/network/network.go @@ -24,27 +24,37 @@ func ConstructHTTPHeaders(appToken string) http.Header { return headers } +var customUserAgent string + +func SetUserAgent(ua string) { + customUserAgent = ua +} + func GetUserAgent() string { - details := []string{} + if customUserAgent != "" { + return customUserAgent + } - cliVersion := "phase-golang-sdk/" + misc.Version - details = append(details, cliVersion) + details := []string{} - osType := runtime.GOOS - architecture := runtime.GOARCH - details = append(details, fmt.Sprintf("%s %s", osType, architecture)) + cliVersion := "phase-golang-sdk/" + misc.Version + details = append(details, cliVersion) - currentUser, err := user.Current() - if err == nil { - hostname, err := os.Hostname() - if err == nil { - userHostString := fmt.Sprintf("%s@%s", currentUser.Username, hostname) - details = append(details, userHostString) - } - } + osType := runtime.GOOS + architecture := runtime.GOARCH + details = append(details, fmt.Sprintf("%s %s", osType, architecture)) + + currentUser, err := user.Current() + if err == nil { + hostname, err := os.Hostname() + if err == nil { + userHostString := fmt.Sprintf("%s@%s", currentUser.Username, hostname) + details = append(details, userHostString) + } + } // Return only the concatenated string without "User-Agent:" prefix - return strings.Join(details, " ") + return strings.Join(details, " ") } func createHTTPClient() *http.Client {