Repository: amp-labs/connectors
Pull request #65 (2024-02-10 02:58:09)

[ENG-522] Create credential getter. 
This is Part 1 for consolidation of credential getting mechanism. 

Created Credential getting struct and methods that support multi-source value getting ( currently from env var , and json file). 

Part 2 will do clean-up of current code base test examples to use these methods and consolidate and reduce redundant code. 

jlimatampersand: Current dependencies on/for this PR:
* `main`
  * **PR #65** <a href="https://app.graphite.dev/github/pr/amp-labs/connectors/65?utm_source=stack-comment-icon" target="_blank"><img src="https://static.graphite.dev/graphite-32x32-black.png" alt="Graphite" width="10px" height="10px"/></a>  👈

This [stack of pull requests](https://stacking.dev/?utm_source=stack-comment) is managed by [Graphite](https://app.graphite.dev/github/pr/amp-labs/connectors/65?utm_source=stack-comment).
codesee-maps[bot]: <details open>
<summary><b>👇 <a href="https://app.codesee.io/r/reviews?pr=65&src=https%3A%2F%2Fgithub.com%2Famp-labs%2Fconnectors">Click on the image</a> for a new way to code review</b>
</summary>

####

[![Review these changes using an interactive CodeSee Map](https://s3.us-east-2.amazonaws.com/maps.codesee.io/images/github/amp-labs/connectors/65/c25e0e1a/d78703f4da40988ab129addd63f1fedb457cab5a.svg)](https://app.codesee.io/r/reviews?pr=65&src=https%3A%2F%2Fgithub.com%2Famp-labs%2Fconnectors)

#### Legend
<img src="https://app.codesee.io/Legend_v3.png" alt="CodeSee Map legend" width="220" height="156">
</details>

Patch:
diff --git a/utils/credentials.go b/utils/credentials.go
new file mode 100644
index 00000000..10b0bb4a
--- /dev/null
+++ b/utils/credentials.go
@@ -0,0 +1,402 @@
+// nolint: ireturn
+package utils
+
+import (
+	"errors"
+	"fmt"
+	"log"
+	"log/slog"
+	"os"
+
+	"github.com/spyzhov/ajson"
+)
+
+var (
+	ErrWrongType            = errors.New("wrong type")
+	ErrEnvVarNotSet         = errors.New("environment variable not set")
+	ErrEmptyStringEnvVar    = errors.New("empty value for environment variable")
+	ErrEmptyFilePathPathVar = errors.New("empty value for file path")
+	ErrJSONPathNotFound     = errors.New("empty value at json path")
+	ErrReaderNotFound       = errors.New("Reader not found")
+	ErrCredentialNotFound   = errors.New("credential not found")
+)
+
+type CredentialsRegistry map[string]Reader
+
+type Reader interface {
+	Value() (any, error)
+	Key() (string, error)
+}
+
+type EnvReaderParam struct {
+	EnvName string `json:"envVar" validate:"required"`
+}
+
+type EnvReader struct {
+	EnvName string `json:"params" validate:"required"`
+	CredKey string `json:"string" validate:"required"`
+}
+
+func (r *EnvReader) Value() (any, error) {
+	value := os.Getenv(r.EnvName)
+	if value == "" {
+		return "", fmt.Errorf("%w: %s", ErrEnvVarNotSet, r.EnvName)
+	}
+
+	return value, nil
+}
+
+func (r *EnvReader) Key() (string, error) {
+	if r.CredKey == "" {
+		return "", fmt.Errorf("%w: %s", ErrEmptyStringEnvVar, r.EnvName)
+	}
+
+	return r.CredKey, nil
+}
+
+type JSONReader struct {
+	FilePath string `json:"filePath" validate:"required"`
+	JSONPath string `json:"jsonPath" validate:"required"`
+	CredKey  string `json:"string"   validate:"required"`
+}
+
+func (r *JSONReader) Key() (string, error) {
+	if r.CredKey == "" {
+		return "", fmt.Errorf("%w: %s", ErrEmptyFilePathPathVar, r.FilePath)
+	}
+
+	return r.CredKey, nil
+}
+
+func (r *JSONReader) Value() (any, error) {
+	data, err := os.ReadFile(r.FilePath)
+	if err != nil {
+		slog.Error("Error opening creds.json", "error", err)
+
+		return nil, err
+	}
+
+	credsMap, err := ajson.Unmarshal(data)
+	if err != nil {
+		return nil, err
+	}
+
+	list, err := credsMap.JSONPath(r.JSONPath)
+	if err != nil {
+		return nil, err
+	}
+
+	if len(list) == 0 || list[0] == nil {
+		return nil, fmt.Errorf("%w: %s", ErrJSONPathNotFound, r.JSONPath)
+	}
+
+	return list[0].Value()
+}
+
+func (c CredentialsRegistry) AddReader(reader Reader) error {
+	key, err := reader.Key()
+	if err != nil {
+		return err
+	}
+
+	c[key] = reader
+
+	return nil
+}
+
+func (c CredentialsRegistry) AddReaders(readers ...Reader) error {
+	for _, Reader := range readers {
+		if err := c.AddReader(Reader); err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+func (c CredentialsRegistry) Get(key string) (any, error) {
+	reader, ok := c[key]
+	if !ok {
+		return nil, fmt.Errorf("%w: %s", ErrReaderNotFound, key)
+	}
+
+	return reader.Value()
+}
+
+func (c CredentialsRegistry) GetString(key string) (string, error) {
+	reader, ok := c[key]
+	if !ok {
+		return "", fmt.Errorf("%w: %s", ErrReaderNotFound, key)
+	}
+
+	return getFromReader[string](reader)
+}
+
+func (c CredentialsRegistry) GetBool(key string) (bool, error) {
+	reader, ok := c[key]
+	if !ok {
+		return false, fmt.Errorf("%w: %s", ErrReaderNotFound, key)
+	}
+
+	return getFromReader[bool](reader)
+}
+
+func (c CredentialsRegistry) GetInt(key string) (int, error) {
+	reader, ok := c[key]
+	if !ok {
+		return 0, fmt.Errorf("%w: %s", ErrReaderNotFound, key)
+	}
+
+	return getFromReader[int](reader)
+}
+
+func (c CredentialsRegistry) GetFloat64(key string) (float64, error) {
+	reader, ok := c[key]
+	if !ok {
+		return 0, fmt.Errorf("%w: %s", ErrReaderNotFound, key)
+	}
+
+	return getFromReader[float64](reader)
+}
+
+func (c CredentialsRegistry) GetFloat32(key string) (float32, error) {
+	reader, ok := c[key]
+	if !ok {
+		return 0, fmt.Errorf("%w: %s", ErrReaderNotFound, key)
+	}
+
+	return getFromReader[float32](reader)
+}
+
+func (c CredentialsRegistry) GetInt64(key string) (int64, error) {
+	reader, ok := c[key]
+	if !ok {
+		return 0, fmt.Errorf("%w: %s", ErrReaderNotFound, key)
+	}
+
+	return getFromReader[int64](reader)
+}
+
+func (c CredentialsRegistry) GetInt32(key string) (int32, error) {
+	reader, ok := c[key]
+	if !ok {
+		return 0, fmt.Errorf("%w: %s", ErrReaderNotFound, key)
+	}
+
+	return getFromReader[int32](reader)
+}
+
+func (c CredentialsRegistry) GetInt16(key string) (int16, error) {
+	reader, ok := c[key]
+	if !ok {
+		return 0, fmt.Errorf("%w: %s", ErrReaderNotFound, key)
+	}
+
+	return getFromReader[int16](reader)
+}
+
+func (c CredentialsRegistry) GetInt8(key string) (int8, error) {
+	reader, ok := c[key]
+	if !ok {
+		return 0, fmt.Errorf("%w: %s", ErrReaderNotFound, key)
+	}
+
+	return getFromReader[int8](reader)
+}
+
+func (c CredentialsRegistry) GetUint64(key string) (uint64, error) {
+	reader, ok := c[key]
+	if !ok {
+		return 0, fmt.Errorf("%w: %s", ErrReaderNotFound, key)
+	}
+
+	return getFromReader[uint64](reader)
+}
+
+func (c CredentialsRegistry) GetUint32(key string) (uint32, error) {
+	reader, ok := c[key]
+	if !ok {
+		return 0, fmt.Errorf("%w: %s", ErrReaderNotFound, key)
+	}
+
+	return getFromReader[uint32](reader)
+}
+
+func (c CredentialsRegistry) GetUint16(key string) (uint16, error) {
+	reader, ok := c[key]
+	if !ok {
+		return 0, fmt.Errorf("%w: %s", ErrReaderNotFound, key)
+	}
+
+	return getFromReader[uint16](reader)
+}
+
+func (c CredentialsRegistry) GetUint8(key string) (uint8, error) {
+	reader, ok := c[key]
+	if !ok {
+		return 0, fmt.Errorf("%w: %s", ErrReaderNotFound, key)
+	}
+
+	return getFromReader[uint8](reader)
+}
+
+func (c CredentialsRegistry) GetUint(key string) (uint, error) {
+	reader, ok := c[key]
+	if !ok {
+		return 0, fmt.Errorf("%w: %s", ErrReaderNotFound, key)
+	}
+
+	return getFromReader[uint](reader)
+}
+
+func (c CredentialsRegistry) MustString(credKey string) string {
+	str, err := c.GetString(credKey)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	return str
+}
+
+func (c CredentialsRegistry) MustBool(credKey string) bool {
+	b, err := c.GetBool(credKey)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	return b
+}
+
+func (c CredentialsRegistry) MustInt(credKey string) int {
+	i, err := c.GetInt(credKey)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	return i
+}
+
+func (c CredentialsRegistry) MustFloat64(credKey string) float64 {
+	f, err := c.GetFloat64(credKey)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	return f
+}
+
+func (c CredentialsRegistry) MustFloat32(credKey string) float32 {
+	f, err := c.GetFloat32(credKey)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	return f
+}
+
+func (c CredentialsRegistry) MustInt64(credKey string) int64 {
+	i, err := c.GetInt64(credKey)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	return i
+}
+
+func (c CredentialsRegistry) MustInt32(credKey string) int32 {
+	i, err := c.GetInt32(credKey)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	return i
+}
+
+func (c CredentialsRegistry) MustInt16(credKey string) int16 {
+	i, err := c.GetInt16(credKey)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	return i
+}
+
+func (c CredentialsRegistry) MustInt8(credKey string) int8 {
+	i, err := c.GetInt8(credKey)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	return i
+}
+
+func (c CredentialsRegistry) MustUint64(credKey string) uint64 {
+	i, err := c.GetUint64(credKey)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	return i
+}
+
+func (c CredentialsRegistry) MustUint32(credKey string) uint32 {
+	i, err := c.GetUint32(credKey)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	return i
+}
+
+func (c CredentialsRegistry) MustUint16(credKey string) uint16 {
+	i, err := c.GetUint16(credKey)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	return i
+}
+
+func (c CredentialsRegistry) MustUint8(credKey string) uint8 {
+	i, err := c.GetUint8(credKey)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	return i
+}
+
+func (c CredentialsRegistry) MustUint(credKey string) uint {
+	i, err := c.GetUint(credKey)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	return i
+}
+
+func NewCredentialsRegistry() CredentialsRegistry {
+	return make(CredentialsRegistry)
+}
+
+func getFromReader[T any](reader Reader) (T, error) {
+	var of T
+
+	val, err := reader.Value()
+	if err != nil {
+		return of, err
+	}
+
+	return get[T](val)
+}
+
+func get[T any](val any) (T, error) {
+	var of T
+
+	v, ok := (val).(T)
+	if !ok {
+		return of, fmt.Errorf("%w. expected %T, got %T", ErrWrongType, of, val)
+	}
+
+	return v, nil
+}


Test patch:
diff --git a/utils/credentials_test.go b/utils/credentials_test.go
new file mode 100644
index 00000000..bda4c888
--- /dev/null
+++ b/utils/credentials_test.go
@@ -0,0 +1,80 @@
+//nolint:gochecknoglobals
+package utils
+
+import (
+	"os"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+)
+
+var testAccessTokenReader = &JSONReader{
+	FilePath: "testcred1.json",
+	JSONPath: "$['accessToken']",
+	CredKey:  "AccessToken",
+}
+
+var testPreset = []Reader{
+	&EnvReader{
+		EnvName: "TEST_ENV_CLIENT_ID",
+		CredKey: "ClientId",
+	},
+	&JSONReader{
+		FilePath: "testcred1.json",
+		JSONPath: "$['useToken']",
+		CredKey:  "UseToken",
+	},
+	&JSONReader{
+		FilePath: "testcred2.json",
+		JSONPath: "$['refreshToken']",
+		CredKey:  "RefreshToken",
+	},
+	&JSONReader{
+		FilePath: "testcred2.json",
+		JSONPath: "$['providers'][0]['name']",
+		CredKey:  "Provider",
+	},
+	&JSONReader{
+		FilePath: "testcred2.json",
+		JSONPath: "$['providers'][0]['number']",
+		CredKey:  "ProviderNumber",
+	},
+}
+
+func TestCredentialOptions(t *testing.T) {
+	t.Parallel()
+
+	if os.Setenv("TEST_ENV_CLIENT_ID", "clientId") != nil {
+		t.Fatal("Error setting environment variable")
+	}
+
+	opts := NewCredentialsRegistry()
+
+	require.NoError(t, opts.AddReader(testAccessTokenReader))
+
+	require.NoError(t, opts.AddReaders(testPreset...))
+
+	refreshToken, err := opts.GetString("RefreshToken")
+	require.NoError(t, err)
+	require.Equal(t, "refreshToken", refreshToken)
+
+	accessToken, err := opts.GetString("AccessToken")
+	require.NoError(t, err)
+	require.Equal(t, "accessToken", accessToken)
+
+	clientId, err := opts.GetString("ClientId")
+	require.NoError(t, err)
+	require.Equal(t, "clientId", clientId)
+
+	provider, err := opts.GetString("Provider")
+	require.NoError(t, err)
+	require.Equal(t, "provider", provider)
+
+	providerNumber, err := opts.GetFloat64("ProviderNumber")
+	require.NoError(t, err)
+	require.Equal(t, 3, int(providerNumber))
+
+	useToken, err := opts.GetBool("UseToken")
+	require.NoError(t, err)
+	require.True(t, useToken)
+}
diff --git a/utils/testcred1.json b/utils/testcred1.json
new file mode 100644
index 00000000..2dfeac54
--- /dev/null
+++ b/utils/testcred1.json
@@ -0,0 +1,4 @@
+{
+    "accessToken": "accessToken",
+    "useToken": true
+}
diff --git a/utils/testcred2.json b/utils/testcred2.json
new file mode 100644
index 00000000..c942e4f8
--- /dev/null
+++ b/utils/testcred2.json
@@ -0,0 +1,9 @@
+{
+    "refreshToken": "refreshToken",
+    "providers": [
+        {
+            "name": "provider",
+            "number": 3
+        }
+    ]
+}