Преглед изворни кода

New Authorizer type (#3196)

Signed-off-by: Sean Holcomb <seanholcomb@gmail.com>
Sean Holcomb пре 11 месеци
родитељ
комит
433f729929

+ 71 - 1
pkg/cloud/azure/authorizer.go

@@ -9,7 +9,10 @@ import (
 	"github.com/opencost/opencost/pkg/cloud"
 )
 
-const DefaultCredentialAuthorizerType = "AzureDefaultCredential"
+const (
+	DefaultCredentialAuthorizerType = "AzureDefaultCredential"
+	ClientSecretCredentialType      = "AzureClientSecretCredential"
+)
 
 // Authorizer configs provide credentials from azidentity to connect to Azure services.
 type Authorizer interface {
@@ -22,6 +25,8 @@ func SelectAuthorizerByType(typeStr string) (Authorizer, error) {
 	switch typeStr {
 	case DefaultCredentialAuthorizerType:
 		return &DefaultAzureCredentialHolder{}, nil
+	case ClientSecretCredentialType:
+		return &ClientSecretCredential{}, nil
 	default:
 		return nil, fmt.Errorf("azure: provider authorizer type '%s' is not valid", typeStr)
 	}
@@ -58,3 +63,68 @@ func (dac *DefaultAzureCredentialHolder) Sanitize() cloud.Config {
 func (dac *DefaultAzureCredentialHolder) GetCredential() (azcore.TokenCredential, error) {
 	return azidentity.NewDefaultAzureCredential(nil)
 }
+
+type ClientSecretCredential struct {
+	TenantID     string `json:"tenantID"`
+	ClientID     string `json:"clientID"`
+	ClientSecret string `json:"clientSecret"`
+}
+
+func (csc *ClientSecretCredential) Validate() error {
+	if csc.TenantID == "" {
+		return fmt.Errorf("ClientSecretCredential: missing Tenant ID")
+	}
+	if csc.ClientID == "" {
+		return fmt.Errorf("ClientSecretCredential: missing Client ID")
+	}
+	if csc.ClientSecret == "" {
+		return fmt.Errorf("ClientSecretCredential: missing Client Secret")
+	}
+	return nil
+}
+
+func (csc *ClientSecretCredential) Sanitize() cloud.Config {
+	return &ClientSecretCredential{
+		TenantID:     csc.TenantID,
+		ClientID:     csc.ClientID,
+		ClientSecret: cloud.Redacted,
+	}
+}
+
+func (csc *ClientSecretCredential) Equals(config cloud.Config) bool {
+	if config == nil {
+		return false
+	}
+	thatConfig, ok := config.(*ClientSecretCredential)
+	if !ok {
+		return false
+	}
+
+	if csc.TenantID != thatConfig.TenantID {
+		return false
+	}
+	if csc.ClientID != thatConfig.ClientID {
+		return false
+	}
+	if csc.ClientSecret != thatConfig.ClientSecret {
+		return false
+	}
+	return true
+}
+
+func (csc *ClientSecretCredential) MarshalJSON() ([]byte, error) {
+	fmap := make(map[string]any, 1)
+	fmap[cloud.AuthorizerTypeProperty] = ClientSecretCredentialType
+	fmap["tenantID"] = csc.TenantID
+	fmap["clientID"] = csc.ClientID
+	fmap["clientSecret"] = csc.ClientSecret
+	return json.Marshal(fmap)
+}
+
+func (csc *ClientSecretCredential) GetCredential() (azcore.TokenCredential, error) {
+	cred, err := azidentity.NewClientSecretCredential(csc.TenantID, csc.ClientID, csc.ClientSecret, nil)
+	if err != nil {
+		return nil, fmt.Errorf("ClientSecretCredential: failed to retrieve credentials: %w", err)
+	}
+	return cred, nil
+}

+ 169 - 0
pkg/cloud/azure/authorizer_test.go

@@ -0,0 +1,169 @@
+package azure
+
+import (
+	"reflect"
+	"testing"
+
+	"github.com/opencost/opencost/pkg/cloud"
+)
+
+func TestClientSecretCredential_Validate(t *testing.T) {
+	tests := map[string]struct {
+		csc     *ClientSecretCredential
+		wantErr bool
+	}{
+		"missing TenantID": {
+			csc: &ClientSecretCredential{
+				TenantID:     "",
+				ClientID:     "clientID",
+				ClientSecret: "clientSecret",
+			},
+			wantErr: true,
+		},
+		"missing ClientID": {
+			csc: &ClientSecretCredential{
+				TenantID:     "tenantID",
+				ClientID:     "",
+				ClientSecret: "clientSecret",
+			},
+			wantErr: true,
+		},
+		"missing ClientSecret": {
+			csc: &ClientSecretCredential{
+				TenantID:     "tenantID",
+				ClientID:     "clientID",
+				ClientSecret: "",
+			},
+			wantErr: true,
+		},
+		"valid": {
+			csc: &ClientSecretCredential{
+				TenantID:     "tenantID",
+				ClientID:     "clientID",
+				ClientSecret: "clientSecret",
+			},
+			wantErr: false,
+		},
+	}
+	for name, tt := range tests {
+		t.Run(name, func(t *testing.T) {
+			if err := tt.csc.Validate(); (err != nil) != tt.wantErr {
+				t.Errorf("Validate() error = %v, wantErr %v", err, tt.wantErr)
+			}
+		})
+	}
+}
+
+func TestClientSecretCredential_Sanitize(t *testing.T) {
+
+	tests := map[string]struct {
+		csc  *ClientSecretCredential
+		want cloud.Config
+	}{
+		"Plain integration": {
+			csc: &ClientSecretCredential{
+				TenantID:     "tenantID",
+				ClientID:     "clientID",
+				ClientSecret: "clientSecret",
+			},
+			want: &ClientSecretCredential{
+				TenantID:     "tenantID",
+				ClientID:     "clientID",
+				ClientSecret: cloud.Redacted,
+			},
+		},
+	}
+	for name, tt := range tests {
+		t.Run(name, func(t *testing.T) {
+			if got := tt.csc.Sanitize(); !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("Sanitize() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func TestClientSecretCredential_Equals(t *testing.T) {
+	tests := map[string]struct {
+		csc    *ClientSecretCredential
+		config cloud.Config
+		want   bool
+	}{
+		"compare nil": {
+			csc: &ClientSecretCredential{
+				TenantID:     "tenantID",
+				ClientID:     "clientID",
+				ClientSecret: "clientSecret",
+			},
+			config: nil,
+			want:   false,
+		},
+		"different config": {
+			csc: &ClientSecretCredential{
+				TenantID:     "tenantID",
+				ClientID:     "clientID",
+				ClientSecret: "clientSecret",
+			},
+			config: &DefaultAzureCredentialHolder{},
+			want:   false,
+		},
+		"different TenantID": {
+			csc: &ClientSecretCredential{
+				TenantID:     "tenantID",
+				ClientID:     "clientID",
+				ClientSecret: "clientSecret",
+			},
+			config: &ClientSecretCredential{
+				TenantID:     "tenantID2",
+				ClientID:     "clientID",
+				ClientSecret: "clientSecret",
+			},
+			want: false,
+		},
+		"different ClientID": {
+			csc: &ClientSecretCredential{
+				TenantID:     "tenantID",
+				ClientID:     "clientID",
+				ClientSecret: "clientSecret",
+			},
+			config: &ClientSecretCredential{
+				TenantID:     "tenantID",
+				ClientID:     "clientID2",
+				ClientSecret: "clientSecret",
+			},
+			want: false,
+		},
+		"different ClientSecret": {
+			csc: &ClientSecretCredential{
+				TenantID:     "tenantID",
+				ClientID:     "clientID",
+				ClientSecret: "clientSecret2",
+			},
+			config: &ClientSecretCredential{
+				TenantID:     "tenantID2",
+				ClientID:     "clientID",
+				ClientSecret: "clientSecret",
+			},
+			want: false,
+		},
+		"equal": {
+			csc: &ClientSecretCredential{
+				TenantID:     "tenantID",
+				ClientID:     "clientID",
+				ClientSecret: "clientSecret",
+			},
+			config: &ClientSecretCredential{
+				TenantID:     "tenantID",
+				ClientID:     "clientID",
+				ClientSecret: "clientSecret",
+			},
+			want: true,
+		},
+	}
+	for name, tt := range tests {
+		t.Run(name, func(t *testing.T) {
+			if got := tt.csc.Equals(tt.config); got != tt.want {
+				t.Errorf("Equals() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}

+ 5 - 0
pkg/cloud/azure/storageauthorizer.go

@@ -122,3 +122,8 @@ func (ah *AuthorizerHolder) GetBlobClient(serviceURL string) (*azblob.Client, er
 	client, err := azblob.NewClient(serviceURL, cred, nil)
 	return client, err
 }
+
+// UnmarshalJSON passes the contained Authorizer to be unmarshalled into
+func (ah *AuthorizerHolder) UnmarshalJSON(b []byte) error {
+	return json.Unmarshal(b, ah.Authorizer)
+}

+ 16 - 0
pkg/cloud/azure/storageconfiguration_test.go

@@ -480,6 +480,22 @@ func TestStorageConfiguration_JSON(t *testing.T) {
 				},
 			},
 		},
+		"ClientSecretCredential Authorizer": {
+			config: StorageConfiguration{
+				SubscriptionID: "subscriptionID",
+				Account:        "account",
+				Container:      "container",
+				Path:           "path",
+				Cloud:          "cloud",
+				Authorizer: &AuthorizerHolder{
+					Authorizer: &ClientSecretCredential{
+						TenantID:     "tenantID",
+						ClientID:     "clientID",
+						ClientSecret: "clientSecret",
+					},
+				},
+			},
+		},
 	}
 
 	for name, testCase := range testCases {