Repository: clouditor/clouditor (go)

Issue:
Add Azure authentication via environment variables
**Is your feature request related to a problem? Please describe.**
I tried to use the credentials for an Azure testbed from a MEDINA partner. For that purpose we got the Application (Client) ID, the Tenant ID, Subscription ID, and the Client Secret.
1) I created a credential file including all these items. I set the `AZURE_AUTH_LOCATION` to link to this file - as required by the `NewAuthorizerFromFile` method of the azure package `autorest`. I didn't find any good example how to set it up but I after a lil bit of trial and error, I think it needs to be a JSON. But other than that, I always got some "Credentials\secret are\is missing" error or similar. Maybe some naming issues of the items in the credentials file.
2) Then I come across the `azidentity` package which seems to be the choice in the future according to https://docs.microsoft.com/en-us/azure/developer/go/azure-sdk-authorization: "Moving forward, authentication for the Azure SDK for Go will be done using the [Azure Identity](https://github.com/Azure/azure-sdk-for-go/tree/main/sdk/azidentity) package". The `Azure CLI` prompting a warn message when trying to create a credentials file like above with `--sdk-auth` option, supports this case.
I tried to replace the auto rest package in the case of the `ARMTemplate` discovery but had no success. I only got empty resource groups and therefore empty templates. Maybe we can address it in a separate issue. A nice feature of `azidenity` is that it has a `NewDefaultAzureCredential` method which includes a authentication chain which it goes through until it has one that works.
3) The quickest fix is to add to our "chain" of authentication (currently from file and over CLI) the option to authenticate via environment variables. I would prefer this way because even with credential file we have to set the path environment variable. Thus, we can also just simply add the client credentials via environment variables which can also be easily added to our K8s cluster? In addition, using credential files seems not to be the way of choice since it is no option in the preferred `azidentity` package and also the CLI prompts a warning when creating such files.

**Describe the solution you'd like**
Option 3) Add option to authenticate from environment.

**Describe alternatives you've considered**
See 1) and 2)


Pull request description:
Add Azure authorization from environment
This PR resolves #555

Patch:
diff --git a/service/discovery/azure/azure.go b/service/discovery/azure/azure.go
index 970401e3..7e98fc36 100644
--- a/service/discovery/azure/azure.go
+++ b/service/discovery/azure/azure.go
@@ -31,8 +31,6 @@ import (
 	"fmt"
 	autorest_azure "github.com/Azure/go-autorest/autorest/azure"
 	"github.com/Azure/go-autorest/autorest/azure/auth"
-	"google.golang.org/grpc/codes"
-	"google.golang.org/grpc/status"
 	"strings"
 
 	"github.com/Azure/azure-sdk-for-go/profiles/2020-09-01/resources/mgmt/subscriptions"
@@ -40,7 +38,12 @@ import (
 	"github.com/sirupsen/logrus"
 )
 
-var log *logrus.Entry
+var (
+	log *logrus.Entry
+
+	ErrCouldNotAuthenticate = errors.New("could not authenticate to Azure with any authorizer " +
+		"(from environment, file, or CLI)")
+)
 
 type DiscoveryOption interface {
 	apply(*autorest.Client)
@@ -120,7 +123,7 @@ func (a *azureDiscovery) authorize() (err error) {
 	return nil
 }
 
-func (a azureDiscovery) apply(client *autorest.Client) {
+func (a *azureDiscovery) apply(client *autorest.Client) {
 	if a.authOption != nil {
 		a.authOption.apply(client)
 	}
@@ -130,24 +133,38 @@ func (a azureDiscovery) apply(client *autorest.Client) {
 	}
 }
 
-func NewAuthorizer() (autorest.Authorizer, error) {
-	// create an authorizer from file or as fallback from the CLI
-	// if authorizer is from CLI, the access token expires after 75 minutes
-	authorizer, err := auth.NewAuthorizerFromFile(autorest_azure.PublicCloud.ResourceManagerEndpoint)
-	if err != nil {
-		log.Errorf("Could not authenticate to Azure with authorizer from file: %v", err)
-		log.Infof("Fallback to Azure with authorizer from CLI.")
-		authorizer, err = auth.NewAuthorizerFromCLI()
-		if err != nil {
-			log.Errorf("Could not authenticate to Azure with authorizer from CLI: %v", err)
-			return nil, status.Errorf(codes.FailedPrecondition, "could not authenticate to Azure: %v", err)
-		}
-		log.Info("Using Azure authorizer from CLI. The discovery times out after 1 hour.")
-	} else {
-		log.Info("Using Azure authorizer from file.")
+// NewAuthorizer creates an authorizer for connecting to Azure using a custom credential chain (from ENV, file and CLI)
+func NewAuthorizer() (authorizer autorest.Authorizer, err error) {
+	// First, try to create authorizer via credentials from the environment
+	authorizer, err = auth.NewAuthorizerFromEnvironment()
+	if err == nil {
+		log.Infof("Using authorizer from environment")
+		return
+	}
+	log.Infof("Could not authenticate to Azure with authorizer from environment: %v", err)
+	log.Infof("Fallback to authorizer from file")
+
+	// Create authorizer from file
+	authorizer, err = auth.NewAuthorizerFromFile(autorest_azure.PublicCloud.ResourceManagerEndpoint)
+	if err == nil {
+		log.Infof("Using authorizer from file")
+		return
+	}
+	log.Infof("Could not authenticate to Azure with authorizer from file: %v", err)
+	log.Infof("Fallback to authorizer from CLI.")
+
+	// Create authorizer from CLI
+	authorizer, err = auth.NewAuthorizerFromCLI()
+	if err == nil {
+		// if authorizer is from CLI, the access token expires after 75 minutes
+		log.Info("Using authorizer from CLI. The discovery times out after 1 hour.")
+		return
 	}
+	log.Infof("Could not authenticate to Azure with authorizer from CLI: %v", err)
 
-	return authorizer, nil
+	// Authorizer couldn't be created
+	log.Error(ErrCouldNotAuthenticate)
+	return nil, ErrCouldNotAuthenticate
 }
 
 func getResourceGroupName(id string) string {


Test patch:
diff --git a/service/discovery/discovery_test.go b/service/discovery/discovery_test.go
index 8c822f62..a6ef6fe1 100644
--- a/service/discovery/discovery_test.go
+++ b/service/discovery/discovery_test.go
@@ -40,6 +40,7 @@ import (
 	"clouditor.io/clouditor/api/evidence"
 	"clouditor.io/clouditor/internal/testutil"
 	"clouditor.io/clouditor/internal/testutil/clitest"
+	"clouditor.io/clouditor/service/discovery/azure"
 	"clouditor.io/clouditor/voc"
 
 	oauth2 "github.com/oxisto/oauth2go"
@@ -255,17 +256,53 @@ func TestStart(t *testing.T) {
 		wantErr        bool
 		wantErrMessage string
 	}{
+		{
+			name: "Azure authorizer from ENV",
+			fields: fields{
+				hasRPCConnection: true,
+				envVariables: []envVariable{
+					// We must set AZURE_AUTH_LOCATION to the Azure credentials test file and the set HOME to a
+					// wrong path so that the Azure authorizer passes and the K8S authorizer fails
+					{
+						hasEnvVariable:   true,
+						envVariableKey:   "AZURE_TENANT_ID",
+						envVariableValue: "tenant-id-123",
+					},
+					{
+						hasEnvVariable:   true,
+						envVariableKey:   "AZURE_CLIENT_ID",
+						envVariableValue: "client-id-123",
+					},
+					{
+						hasEnvVariable:   true,
+						envVariableKey:   "AZURE_CLIENT_SECRET",
+						envVariableValue: "client-secret-456",
+					},
+				},
+			},
+			providers:      []string{ProviderAzure},
+			wantResp:       &discovery.StartDiscoveryResponse{Successful: true},
+			wantErr:        false,
+			wantErrMessage: "",
+		},
 		{
 			name: "Azure authorizer from file",
 			fields: fields{
 				hasRPCConnection: true,
 				envVariables: []envVariable{
-					// We must set AZURE_AUTH_LOCATION to the Azure credentials test file and the set HOME to a wrong path so that the Azure authorizer passes and the K8S authorizer fails
+					// We must set AZURE_AUTH_LOCATION to the Azure credentials test file and the set HOME to a
+					// wrong path so that the Azure authorizer passes and the K8S authorizer fails
 					{
 						hasEnvVariable:   true,
 						envVariableKey:   "AZURE_AUTH_LOCATION",
 						envVariableValue: "service/discovery/testdata/credentials_test_file",
 					},
+					// Set $AZURE_ENVIRONMENT to sth. invalid s.t. Authorizer from file (2nd option is used)
+					{
+						hasEnvVariable:   true,
+						envVariableKey:   "AZURE_ENVIRONMENT",
+						envVariableValue: "!?NoEnvironment!?",
+					},
 				},
 			},
 			providers:      []string{ProviderAzure},
@@ -277,8 +314,51 @@ func TestStart(t *testing.T) {
 			name: "No Azure authorizer",
 			fields: fields{
 				hasRPCConnection: true,
+				// We must set env variables accordingly s.t. all authorizer will fail
 				envVariables: []envVariable{
-					// We must set AZURE_AUTH_LOCATION and HOME to a wrong path so that both Azure authorizer fail
+					// Let `authorizer from ENV` fail
+					// It uses the order: 1. Client credentials 2. Client certificate 3. Username password 4. MSI
+					// 1. Set client credentials
+					{
+						hasEnvVariable:   true,
+						envVariableKey:   "AZURE_TENANT_ID",
+						envVariableValue: "",
+					},
+					{
+						hasEnvVariable:   true,
+						envVariableKey:   "AZURE_CLIENT_ID",
+						envVariableValue: "",
+					},
+					{
+						hasEnvVariable:   true,
+						envVariableKey:   "AZURE_CLIENT_SECRET",
+						envVariableValue: "",
+					},
+					// 2. set certificate path and certificate pw to empty string (client and tenant ID already empty)
+					{
+						hasEnvVariable:   true,
+						envVariableKey:   "AZURE_CERTIFICATE_PATH",
+						envVariableValue: "",
+					},
+					// 3. Set username and password to empty string (client and tenant ID already empty)
+					{
+						hasEnvVariable:   true,
+						envVariableKey:   "AZURE_USERNAME",
+						envVariableValue: "",
+					},
+					{
+						hasEnvVariable:   true,
+						envVariableKey:   "AZURE_PASSWORD",
+						envVariableValue: "",
+					},
+					// 4. Try to prevent getting authorizer from MSI: Set AZ ENV to sth. wrong (but not empty!)
+					{
+						hasEnvVariable:   true,
+						envVariableKey:   "AZURE_ENVIRONMENT",
+						envVariableValue: "!?NoEnvironment!?",
+					},
+
+					// Let `authorizer file and CLI` fail: Set $AZURE_AUTH_LOCATION and $HOME to a wrong path
 					{
 						hasEnvVariable:   true,
 						envVariableKey:   "AZURE_AUTH_LOCATION",
@@ -294,7 +374,7 @@ func TestStart(t *testing.T) {
 			providers:      []string{ProviderAzure},
 			wantResp:       nil,
 			wantErr:        true,
-			wantErrMessage: "could not authenticate to Azure",
+			wantErrMessage: azure.ErrCouldNotAuthenticate.Error(),
 		},
 		{
 			name: "No K8s authorizer",