From e2cca3274814b6dbe21816bad6c1db66492aebd9 Mon Sep 17 00:00:00 2001
From: Alan Meadows <alan.meadows@gmail.com>
Date: Fri, 27 Mar 2020 14:09:32 -0700
Subject: [PATCH] Add Redfish Authentication Support

This commit also introduces a dochelper concept.  This provides
some convenience methods to the document pkg that help extract data
from known document types as well as walk document relationships to
discover related information such as BMC credentials for baremetal
hosts.

Once merged, a follow up patchset will leverage these within the
cloud-init code to deduplicate some of these lookups.

Change-Id: Ie6a770ce4b34adbea30281917f0cb2fdc460b4fb
---
 pkg/bootstrap/cloudinit/cloud-init.go         |   2 +-
 pkg/document/bundle.go                        |  24 +++++
 pkg/document/dochelper_baremetalhost.go       |  60 +++++++++++
 pkg/document/dochelper_test.go                |  55 ++++++++++
 pkg/document/dochelper_utils.go               |  44 ++++++++
 pkg/document/errors.go                        |  21 ++++
 pkg/document/selectors.go                     |  11 +-
 pkg/document/selectors_test.go                |  10 +-
 .../testdata/dochelper/baremetalhost.yaml     |  16 +++
 .../testdata/dochelper/kustomization.yaml     |   3 +
 pkg/document/testdata/dochelper/secret.yaml   |  28 +++++
 .../testdata/selectors/valid/secret.yaml      |   3 +-
 pkg/remote/redfish/redfish.go                 |  25 ++++-
 pkg/remote/redfish/redfish_test.go            | 100 +++++++++++++-----
 pkg/remote/remote_direct.go                   |  56 +++++-----
 15 files changed, 393 insertions(+), 65 deletions(-)
 create mode 100644 pkg/document/dochelper_baremetalhost.go
 create mode 100644 pkg/document/dochelper_test.go
 create mode 100644 pkg/document/dochelper_utils.go
 create mode 100644 pkg/document/testdata/dochelper/baremetalhost.yaml
 create mode 100644 pkg/document/testdata/dochelper/kustomization.yaml
 create mode 100644 pkg/document/testdata/dochelper/secret.yaml

diff --git a/pkg/bootstrap/cloudinit/cloud-init.go b/pkg/bootstrap/cloudinit/cloud-init.go
index c8aef2e5b..77da0a6f3 100644
--- a/pkg/bootstrap/cloudinit/cloud-init.go
+++ b/pkg/bootstrap/cloudinit/cloud-init.go
@@ -74,7 +74,7 @@ func getNetworkData(docBundle document.Bundle) ([]byte, error) {
 	}
 
 	// try and find these documents in our bundle
-	selector, err = document.NewEphemeralNetworkDataSelector(bmhDoc)
+	selector, err = document.NewNetworkDataSelector(bmhDoc)
 	if err != nil {
 		return nil, err
 	}
diff --git a/pkg/document/bundle.go b/pkg/document/bundle.go
index 80326ad2b..7ecaa908b 100644
--- a/pkg/document/bundle.go
+++ b/pkg/document/bundle.go
@@ -47,6 +47,7 @@ type Bundle interface {
 	SetFileSystem(FileSystem) error
 	GetFileSystem() FileSystem
 	Select(selector Selector) ([]Document, error)
+	SelectOne(selector Selector) (Document, error)
 	SelectBundle(selector Selector) (Bundle, error)
 	SelectByFieldValue(string, func(interface{}) bool) (Bundle, error)
 	GetByGvk(string, string, string) ([]Document, error)
@@ -220,6 +221,29 @@ func (b *BundleFactory) Select(selector Selector) ([]Document, error) {
 	return docSet, err
 }
 
+// SelectOne serves the common use case where you expect one match
+// and only one match to your selector -- in other words, you want to
+// error if you didn't find any documents, and error if you found
+// more than one.  This reduces code repetition that would otherwise
+// be scattered around that evaluates the length of the doc set returned
+// for this common case
+func (b *BundleFactory) SelectOne(selector Selector) (Document, error) {
+	docSet, err := b.Select(selector)
+	if err != nil {
+		return nil, err
+	}
+
+	// evaluate docSet for at least one document, and no more than
+	// one document and raise errors as appropriate
+	switch numDocsFound := len(docSet); {
+	case numDocsFound == 0:
+		return nil, ErrDocNotFound{Selector: selector}
+	case numDocsFound > 1:
+		return nil, ErrMultipleDocsFound{Selector: selector}
+	}
+	return docSet[0], nil
+}
+
 // SelectBundle offers an interface to pass a Selector, built on top of kustomize Selector
 // to the bundle returning a new Bundle that matches the criteria.  This is useful
 // where you want to actually prune the underlying bundle you are working with
diff --git a/pkg/document/dochelper_baremetalhost.go b/pkg/document/dochelper_baremetalhost.go
new file mode 100644
index 000000000..95a6caeb4
--- /dev/null
+++ b/pkg/document/dochelper_baremetalhost.go
@@ -0,0 +1,60 @@
+package document
+
+// GetBMHNetworkData retrieves the associated network data string
+// for the bmh document supplied from the bundle supplied
+func GetBMHNetworkData(bmh Document, bundle Bundle) (string, error) {
+	// try and find these documents in our bundle
+	selector, err := NewNetworkDataSelector(bmh)
+	if err != nil {
+		return "", err
+	}
+	doc, err := bundle.SelectOne(selector)
+
+	if err != nil {
+		return "", err
+	}
+
+	networkData, err := GetSecretDataKey(doc, "networkData")
+	if err != nil {
+		return "", err
+	}
+	return networkData, nil
+}
+
+// GetBMHBMCAddress returns the bmc address for a particular the document supplied
+func GetBMHBMCAddress(bmh Document) (string, error) {
+	bmcAddress, err := bmh.GetString("spec.bmc.address")
+	if err != nil {
+		return "", err
+	}
+	return bmcAddress, nil
+}
+
+// GetBMHBMCCredentials returns the BMC credentials for the bmh document supplied from
+// the supplied bundle
+func GetBMHBMCCredentials(bmh Document, bundle Bundle) (username string, password string, err error) {
+	// extract the secret document name
+	bmcCredentialsName, err := bmh.GetString("spec.bmc.credentialsName")
+	if err != nil {
+		return "", "", err
+	}
+
+	// find the secret within the bundle supplied
+	selector := NewBMCCredentialsSelector(bmcCredentialsName)
+	doc, err := bundle.SelectOne(selector)
+	if err != nil {
+		return "", "", err
+	}
+
+	username, err = GetSecretDataKey(doc, "username")
+	if err != nil {
+		return "", "", err
+	}
+	password, err = GetSecretDataKey(doc, "password")
+	if err != nil {
+		return "", "", err
+	}
+
+	// extract the username and password from them
+	return username, password, nil
+}
diff --git a/pkg/document/dochelper_test.go b/pkg/document/dochelper_test.go
new file mode 100644
index 000000000..dde62ef9f
--- /dev/null
+++ b/pkg/document/dochelper_test.go
@@ -0,0 +1,55 @@
+package document_test
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+
+	"opendev.org/airship/airshipctl/pkg/document"
+	"opendev.org/airship/airshipctl/testutil"
+)
+
+func TestDocHelpers(t *testing.T) {
+	require := require.New(t)
+	assert := assert.New(t)
+
+	fSys := testutil.SetupTestFs(t, "testdata/dochelper")
+	bundle, err := document.NewBundle(fSys, "/", "/")
+	require.NoError(err, "Building Bundle Failed")
+	require.NotNil(bundle)
+
+	t.Run("GetBMHNetworkData", func(t *testing.T) {
+		// retrieve our single bmh in the dataset
+		selector := document.NewSelector().ByKind("BareMetalHost")
+		doc, err := bundle.SelectOne(selector)
+		require.NoError(err)
+
+		networkData, err := document.GetBMHNetworkData(doc, bundle)
+		require.NoError(err, "Unexpected error trying to GetBMHNetworkData")
+		assert.Equal(networkData, "some network data")
+	})
+
+	t.Run("GetBMHBMCAddress", func(t *testing.T) {
+		// retrieve our single bmh in the dataset
+		selector := document.NewSelector().ByKind("BareMetalHost")
+		doc, err := bundle.SelectOne(selector)
+		require.NoError(err)
+
+		bmcAddress, err := document.GetBMHBMCAddress(doc)
+		require.NoError(err, "Unexpected error trying to GetBMHBMCAddress")
+		assert.Equal(bmcAddress, "redfish+https://192.168.111.1/v1/Redfish/Foo/Bar")
+	})
+
+	t.Run("GetBMHBMCCredentials", func(t *testing.T) {
+		// retrieve our single bmh in the dataset
+		selector := document.NewSelector().ByKind("BareMetalHost")
+		doc, err := bundle.SelectOne(selector)
+		require.NoError(err)
+
+		bmcUsername, bmcPassword, err := document.GetBMHBMCCredentials(doc, bundle)
+		require.NoError(err, "Unexpected error trying to GetBMHBMCCredentials")
+		assert.Equal(bmcUsername, "username")
+		assert.Equal(bmcPassword, "password")
+	})
+}
diff --git a/pkg/document/dochelper_utils.go b/pkg/document/dochelper_utils.go
new file mode 100644
index 000000000..76d4750a4
--- /dev/null
+++ b/pkg/document/dochelper_utils.go
@@ -0,0 +1,44 @@
+package document
+
+import (
+	b64 "encoding/base64"
+)
+
+// GetSecretDataKey understands how to retrieve a specific top level key from a secret
+// that may have the data stored under a data or stringData field in which
+// case the key may be base64 encoded or it may be plain text
+//
+// it is meant to be used by other high level dochelpers
+func GetSecretDataKey(cfg Document, key string) (string, error) {
+	var needsBase64Decode = true
+	var docName = cfg.GetName()
+
+	// this purposely doesn't handle binaryData as that isn't
+	// something we could support anyways
+	data, err := cfg.GetStringMap("stringData")
+	if err == nil {
+		needsBase64Decode = false
+	} else {
+		data, err = cfg.GetStringMap("data")
+		if err != nil {
+			return "", ErrDocumentMalformed{
+				DocName: docName,
+				Message: "The secret document lacks a data or stringData top level field",
+			}
+		}
+	}
+
+	res, ok := data[key]
+	if !ok {
+		return "", ErrDocumentDataKeyNotFound{DocName: docName, Key: key}
+	}
+
+	if needsBase64Decode {
+		byteSlice, err := b64.StdEncoding.DecodeString(res)
+		if err != nil {
+			return "", err
+		}
+		return string(byteSlice), nil
+	}
+	return res, nil
+}
diff --git a/pkg/document/errors.go b/pkg/document/errors.go
index 769c7f208..84ca11334 100644
--- a/pkg/document/errors.go
+++ b/pkg/document/errors.go
@@ -9,6 +9,19 @@ type ErrDocNotFound struct {
 	Selector Selector
 }
 
+// ErrDocumentDataKeyNotFound returned if desired key within a document not found
+type ErrDocumentDataKeyNotFound struct {
+	DocName string
+	Key     string
+}
+
+// ErrDocumentMalformed returned if the document is structurally malformed
+// (e.g. missing required low level keys)
+type ErrDocumentMalformed struct {
+	DocName string
+	Message string
+}
+
 // ErrMultipleDocsFound returned if desired document not found
 type ErrMultipleDocsFound struct {
 	Selector Selector
@@ -18,6 +31,14 @@ func (e ErrDocNotFound) Error() string {
 	return fmt.Sprintf("Document filtered by selector %q found no documents", e.Selector)
 }
 
+func (e ErrDocumentDataKeyNotFound) Error() string {
+	return fmt.Sprintf("Document %q cannot retrieve data key %q", e.DocName, e.Key)
+}
+
+func (e ErrDocumentMalformed) Error() string {
+	return fmt.Sprintf("Document %q is malformed: %q", e.DocName, e.Message)
+}
+
 func (e ErrMultipleDocsFound) Error() string {
 	return fmt.Sprintf("Document filtered by selector %q found more than one document", e.Selector)
 }
diff --git a/pkg/document/selectors.go b/pkg/document/selectors.go
index 9dfd0595e..e4367c49c 100644
--- a/pkg/document/selectors.go
+++ b/pkg/document/selectors.go
@@ -64,7 +64,7 @@ func (s Selector) ByAnnotation(annotationSelector string) Selector {
 	return s
 }
 
-// EphemeralCloudDataSelector returns selector to get BaremetalHost for ephemeral node
+// NewEphemeralCloudDataSelector returns selector to get BaremetalHost for ephemeral node
 func NewEphemeralCloudDataSelector() Selector {
 	return NewSelector().ByKind(SecretKind).ByLabel(EphemeralUserDataSelector)
 }
@@ -74,11 +74,16 @@ func NewEphemeralBMHSelector() Selector {
 	return NewSelector().ByKind(BareMetalHostKind).ByLabel(EphemeralHostSelector)
 }
 
-// NewEphemeralNetworkDataSelector returns selector that can be used to get secret with
+// NewBMCCredentialsSelector returns selector to get BaremetalHost BMC credentials
+func NewBMCCredentialsSelector(name string) Selector {
+	return NewSelector().ByKind(SecretKind).ByName(name)
+}
+
+// NewNetworkDataSelector returns selector that can be used to get secret with
 // network data bmhDoc argument is a document interface, that should hold fields
 // spec.networkData.name and spec.networkData.namespace where to find the secret,
 // if either of these fields are not defined in Document error will be returned
-func NewEphemeralNetworkDataSelector(bmhDoc Document) (Selector, error) {
+func NewNetworkDataSelector(bmhDoc Document) (Selector, error) {
 	selector := NewSelector()
 	// extract the network data document pointer from the bmh document
 	netConfDocName, err := bmhDoc.GetString("spec.networkData.name")
diff --git a/pkg/document/selectors_test.go b/pkg/document/selectors_test.go
index fc038d58b..949221f94 100644
--- a/pkg/document/selectors_test.go
+++ b/pkg/document/selectors_test.go
@@ -24,7 +24,7 @@ func TestSelectorsPositive(t *testing.T) {
 		require.NoError(t, err)
 		assert.Len(t, docs, 1)
 		bmhDoc := docs[0]
-		selector, err := document.NewEphemeralNetworkDataSelector(bmhDoc)
+		selector, err := document.NewNetworkDataSelector(bmhDoc)
 		require.NoError(t, err)
 		assert.Equal(t, "validName", selector.Name)
 	})
@@ -42,12 +42,12 @@ func TestSelectorsNegative(t *testing.T) {
 	// test coverage
 	bundle := testutil.NewTestBundle(t, "testdata/selectors/invalid")
 
-	t.Run("TestNewEphemeralNetworkDataSelectorErr", func(t *testing.T) {
+	t.Run("TestNewNetworkDataSelectorErr", func(t *testing.T) {
 		docs, err := bundle.Select(document.NewEphemeralBMHSelector())
 		require.NoError(t, err)
 		assert.Len(t, docs, 2)
 		bmhDoc := docs[0]
-		_, err = document.NewEphemeralNetworkDataSelector(bmhDoc)
+		_, err = document.NewNetworkDataSelector(bmhDoc)
 		assert.Error(t, err)
 	})
 
@@ -56,7 +56,7 @@ func TestSelectorsNegative(t *testing.T) {
 		require.NoError(t, err)
 		assert.Len(t, docs, 2)
 		bmhDoc := docs[1]
-		_, err = document.NewEphemeralNetworkDataSelector(bmhDoc)
+		_, err = document.NewNetworkDataSelector(bmhDoc)
 		assert.Error(t, err)
 	})
 }
@@ -67,7 +67,7 @@ func TestSelectorsSkip(t *testing.T) {
 	// test coverage
 	bundle := testutil.NewTestBundle(t, "testdata/selectors/exclude-from-k8s")
 
-	t.Run("TestNewEphemeralNetworkDataSelectorErr", func(t *testing.T) {
+	t.Run("TestNewNetworkDataSelectorErr", func(t *testing.T) {
 		selector := document.NewDeployToK8sSelector()
 		docs, err := bundle.Select(selector)
 		require.NoError(t, err)
diff --git a/pkg/document/testdata/dochelper/baremetalhost.yaml b/pkg/document/testdata/dochelper/baremetalhost.yaml
new file mode 100644
index 000000000..f20e8c0da
--- /dev/null
+++ b/pkg/document/testdata/dochelper/baremetalhost.yaml
@@ -0,0 +1,16 @@
+---
+apiVersion: metal3.io/v1alpha1
+kind: BareMetalHost
+metadata:
+  labels:
+    airshipit.org/ephemeral-node: "true"
+  name: master-0
+spec:
+  online: true
+  bootMACAddress: 00:3b:8b:0c:ec:8b
+  bmc:
+    address: redfish+https://192.168.111.1/v1/Redfish/Foo/Bar
+    credentialsName: master-0-bmc
+  networkData:
+    name: master-0-networkdata
+    namespace: metal3
diff --git a/pkg/document/testdata/dochelper/kustomization.yaml b/pkg/document/testdata/dochelper/kustomization.yaml
new file mode 100644
index 000000000..3935bdb09
--- /dev/null
+++ b/pkg/document/testdata/dochelper/kustomization.yaml
@@ -0,0 +1,3 @@
+resources:
+ - baremetalhost.yaml
+ - secret.yaml
\ No newline at end of file
diff --git a/pkg/document/testdata/dochelper/secret.yaml b/pkg/document/testdata/dochelper/secret.yaml
new file mode 100644
index 000000000..4bafed861
--- /dev/null
+++ b/pkg/document/testdata/dochelper/secret.yaml
@@ -0,0 +1,28 @@
+apiVersion: v1
+kind: Secret
+metadata:
+  labels:
+    airshipit.org/ephemeral-user-data: "true"
+  name: ephemeral-user-data
+type: Opaque
+stringData:
+  userData: cloud-init
+---
+apiVersion: v1
+kind: Secret
+metadata:
+  name: master-0-bmc
+  namespace: metal3
+type: Opaque
+stringData:
+  username: username
+  password: password
+---
+apiVersion: v1
+kind: Secret
+metadata:
+  name: master-0-networkdata
+  namespace: metal3
+type: Opaque
+stringData:
+  networkData: some network data
\ No newline at end of file
diff --git a/pkg/document/testdata/selectors/valid/secret.yaml b/pkg/document/testdata/selectors/valid/secret.yaml
index 25a11a87c..f39541cd2 100644
--- a/pkg/document/testdata/selectors/valid/secret.yaml
+++ b/pkg/document/testdata/selectors/valid/secret.yaml
@@ -17,5 +17,4 @@ metadata:
   namespace: validNamespace
 type: Opaque
 stringData:
-  userData: cloud-init
-
+  userData: cloud-init
\ No newline at end of file
diff --git a/pkg/remote/redfish/redfish.go b/pkg/remote/redfish/redfish.go
index 6bacc6289..a20ab81db 100644
--- a/pkg/remote/redfish/redfish.go
+++ b/pkg/remote/redfish/redfish.go
@@ -29,14 +29,18 @@ type RemoteDirect struct {
 
 	// Redfish Client implementation
 	RedfishAPI redfishApi.RedfishAPI
+
+	// optional Username Authentication
+	Username string
+
+	// optional Password
+	Password string
 }
 
 // Top level function to handle Redfish remote direct
 func (cfg RemoteDirect) DoRemoteDirect() error {
 	alog.Debugf("Using Redfish Endpoint: '%s'", cfg.RemoteURL.String())
 
-	/* TODO: Add Authentication when redfish library supports it. */
-
 	/* Get system details */
 	systemID := cfg.EphemeralNodeID
 	system, _, err := cfg.RedfishAPI.GetSystem(cfg.Context, systemID)
@@ -79,10 +83,12 @@ func (cfg RemoteDirect) DoRemoteDirect() error {
 	return nil
 }
 
-// Creates a new Redfish remote direct client.
-func NewRedfishRemoteDirectClient(ctx context.Context,
+// NewRedfishRemoteDirectClient creates a new Redfish remote direct client.
+func NewRedfishRemoteDirectClient(
 	remoteURL string,
 	ephNodeID string,
+	username string,
+	password string,
 	isoPath string,
 	insecure bool,
 	useproxy bool,
@@ -101,6 +107,17 @@ func NewRedfishRemoteDirectClient(ctx context.Context,
 			}
 	}
 
+	var ctx context.Context
+	if username != "" && password != "" {
+		ctx = context.WithValue(
+			context.Background(),
+			redfishClient.ContextBasicAuth,
+			redfishClient.BasicAuth{UserName: username, Password: password},
+		)
+	} else {
+		ctx = context.Background()
+	}
+
 	if isoPath == "" {
 		return RemoteDirect{},
 			ErrRedfishMissingConfig{
diff --git a/pkg/remote/redfish/redfish_test.go b/pkg/remote/redfish/redfish_test.go
index a1765c084..29914122a 100644
--- a/pkg/remote/redfish/redfish_test.go
+++ b/pkg/remote/redfish/redfish_test.go
@@ -28,37 +28,44 @@ func TestRedfishRemoteDirectNormal(t *testing.T) {
 
 	systemID := computerSystemID
 	httpResp := &http.Response{StatusCode: 200}
-	m.On("GetSystem", context.Background(), systemID).Times(1).
+
+	ctx := context.WithValue(
+		context.Background(),
+		redfishClient.ContextBasicAuth,
+		redfishClient.BasicAuth{UserName: "username", Password: "password"},
+	)
+
+	m.On("GetSystem", ctx, systemID).Times(1).
 		Return(getTestSystem(), httpResp, nil)
-	m.On("InsertVirtualMedia", context.Background(), "manager-1", "Cd", mock.Anything).
+	m.On("InsertVirtualMedia", ctx, "manager-1", "Cd", mock.Anything).
 		Return(redfishClient.RedfishError{}, httpResp, nil)
 
-	m.On("GetSystem", context.Background(), systemID).Times(1).
+	m.On("GetSystem", ctx, systemID).Times(1).
 		Return(getTestSystem(), httpResp, nil)
 	systemReq := redfishClient.ComputerSystem{
 		Boot: redfishClient.Boot{
 			BootSourceOverrideTarget: redfishClient.BOOTSOURCE_CD,
 		},
 	}
-	m.On("SetSystem", context.Background(), systemID, systemReq).
+	m.On("SetSystem", ctx, systemID, systemReq).
 		Times(1).
 		Return(redfishClient.ComputerSystem{}, httpResp, nil)
 
 	resetReq := redfishClient.ResetRequestBody{}
 	resetReq.ResetType = redfishClient.RESETTYPE_FORCE_OFF
-	m.On("ResetSystem", context.Background(), systemID, resetReq).
+	m.On("ResetSystem", ctx, systemID, resetReq).
 		Times(1).
 		Return(redfishClient.RedfishError{}, httpResp, nil)
 
-	m.On("GetSystem", context.Background(), systemID).Times(1).
+	m.On("GetSystem", ctx, systemID).Times(1).
 		Return(redfishClient.ComputerSystem{PowerState: redfishClient.POWERSTATE_OFF}, httpResp, nil)
 
 	resetReq.ResetType = redfishClient.RESETTYPE_ON
-	m.On("ResetSystem", context.Background(), systemID, resetReq).
+	m.On("ResetSystem", ctx, systemID, resetReq).
 		Times(1).
 		Return(redfishClient.RedfishError{}, httpResp, nil)
 
-	m.On("GetSystem", context.Background(), systemID).Times(1).
+	m.On("GetSystem", ctx, systemID).Times(1).
 		Return(redfishClient.ComputerSystem{PowerState: redfishClient.POWERSTATE_ON}, httpResp, nil)
 
 	rDCfg := getDefaultRedfishRemoteDirectObj(t, m)
@@ -72,13 +79,19 @@ func TestRedfishRemoteDirectInvalidSystemId(t *testing.T) {
 	m := &redfishMocks.RedfishAPI{}
 	defer m.AssertExpectations(t)
 
+	ctx := context.WithValue(
+		context.Background(),
+		redfishClient.ContextBasicAuth,
+		redfishClient.BasicAuth{UserName: "username", Password: "password"},
+	)
+
 	systemID := "invalid-server"
 	localRDCfg := getDefaultRedfishRemoteDirectObj(t, m)
 
 	localRDCfg.EphemeralNodeID = systemID
 
 	realErr := fmt.Errorf("%s system do not exist", systemID)
-	m.On("GetSystem", context.Background(), systemID).
+	m.On("GetSystem", ctx, systemID).
 		Times(1).
 		Return(redfishClient.ComputerSystem{}, nil, realErr)
 
@@ -92,10 +105,16 @@ func TestRedfishRemoteDirectGetSystemNetworkError(t *testing.T) {
 	m := &redfishMocks.RedfishAPI{}
 	defer m.AssertExpectations(t)
 
+	ctx := context.WithValue(
+		context.Background(),
+		redfishClient.ContextBasicAuth,
+		redfishClient.BasicAuth{UserName: "username", Password: "password"},
+	)
+
 	systemID := computerSystemID
 	realErr := fmt.Errorf("server request timeout")
 	httpResp := &http.Response{StatusCode: 408}
-	m.On("GetSystem", context.Background(), systemID).
+	m.On("GetSystem", ctx, systemID).
 		Times(1).
 		Return(redfishClient.ComputerSystem{}, httpResp, realErr)
 
@@ -111,6 +130,12 @@ func TestRedfishRemoteDirectInvalidIsoPath(t *testing.T) {
 	m := &redfishMocks.RedfishAPI{}
 	defer m.AssertExpectations(t)
 
+	ctx := context.WithValue(
+		context.Background(),
+		redfishClient.ContextBasicAuth,
+		redfishClient.BasicAuth{UserName: "username", Password: "password"},
+	)
+
 	systemID := computerSystemID
 	rDCfg := getDefaultRedfishRemoteDirectObj(t, m)
 	localRDCfg := rDCfg
@@ -118,11 +143,11 @@ func TestRedfishRemoteDirectInvalidIsoPath(t *testing.T) {
 
 	realErr := redfishClient.GenericOpenAPIError{}
 	httpResp := &http.Response{StatusCode: 500}
-	m.On("GetSystem", context.Background(), systemID).
+	m.On("GetSystem", ctx, systemID).
 		Times(1).
 		Return(getTestSystem(), nil, nil)
 
-	m.On("InsertVirtualMedia", context.Background(), "manager-1", "Cd", mock.Anything).
+	m.On("InsertVirtualMedia", ctx, "manager-1", "Cd", mock.Anything).
 		Return(redfishClient.RedfishError{}, httpResp, realErr)
 
 	err := localRDCfg.DoRemoteDirect()
@@ -135,6 +160,12 @@ func TestRedfishRemoteDirectCdDvdNotAvailableInBootSources(t *testing.T) {
 	m := &redfishMocks.RedfishAPI{}
 	defer m.AssertExpectations(t)
 
+	ctx := context.WithValue(
+		context.Background(),
+		redfishClient.ContextBasicAuth,
+		redfishClient.BasicAuth{UserName: "username", Password: "password"},
+	)
+
 	systemID := computerSystemID
 	invalidSystem := getTestSystem()
 	invalidSystem.Boot.BootSourceOverrideTargetRedfishAllowableValues = []redfishClient.BootSource{
@@ -142,10 +173,10 @@ func TestRedfishRemoteDirectCdDvdNotAvailableInBootSources(t *testing.T) {
 		redfishClient.BOOTSOURCE_PXE,
 	}
 
-	m.On("GetSystem", context.Background(), systemID).
+	m.On("GetSystem", ctx, systemID).
 		Return(invalidSystem, nil, nil)
 
-	m.On("InsertVirtualMedia", context.Background(), "manager-1", "Cd", mock.Anything).
+	m.On("InsertVirtualMedia", ctx, "manager-1", "Cd", mock.Anything).
 		Return(redfishClient.RedfishError{}, nil, nil)
 
 	rDCfg := getDefaultRedfishRemoteDirectObj(t, m)
@@ -160,15 +191,21 @@ func TestRedfishRemoteDirectSetSystemBootSourceFailed(t *testing.T) {
 	m := &redfishMocks.RedfishAPI{}
 	defer m.AssertExpectations(t)
 
+	ctx := context.WithValue(
+		context.Background(),
+		redfishClient.ContextBasicAuth,
+		redfishClient.BasicAuth{UserName: "username", Password: "password"},
+	)
+
 	systemID := computerSystemID
 	httpSuccResp := &http.Response{StatusCode: 200}
-	m.On("GetSystem", context.Background(), systemID).
+	m.On("GetSystem", ctx, systemID).
 		Return(getTestSystem(), httpSuccResp, nil)
 
-	m.On("InsertVirtualMedia", context.Background(), "manager-1", "Cd", mock.Anything).
+	m.On("InsertVirtualMedia", ctx, "manager-1", "Cd", mock.Anything).
 		Return(redfishClient.RedfishError{}, httpSuccResp, nil)
 
-	m.On("SetSystem", context.Background(), systemID, mock.Anything).
+	m.On("SetSystem", ctx, systemID, mock.Anything).
 		Times(1).
 		Return(redfishClient.ComputerSystem{}, &http.Response{StatusCode: 401},
 			redfishClient.GenericOpenAPIError{})
@@ -185,21 +222,27 @@ func TestRedfishRemoteDirectSystemRebootFailed(t *testing.T) {
 	m := &redfishMocks.RedfishAPI{}
 	defer m.AssertExpectations(t)
 
+	ctx := context.WithValue(
+		context.Background(),
+		redfishClient.ContextBasicAuth,
+		redfishClient.BasicAuth{UserName: "username", Password: "password"},
+	)
+
 	systemID := computerSystemID
 	httpSuccResp := &http.Response{StatusCode: 200}
-	m.On("GetSystem", context.Background(), systemID).
+	m.On("GetSystem", ctx, systemID).
 		Return(getTestSystem(), httpSuccResp, nil)
 
-	m.On("InsertVirtualMedia", context.Background(), mock.Anything, mock.Anything, mock.Anything).
+	m.On("InsertVirtualMedia", ctx, mock.Anything, mock.Anything, mock.Anything).
 		Return(redfishClient.RedfishError{}, httpSuccResp, nil)
 
-	m.On("SetSystem", context.Background(), systemID, mock.Anything).
+	m.On("SetSystem", ctx, systemID, mock.Anything).
 		Times(1).
 		Return(redfishClient.ComputerSystem{}, httpSuccResp, nil)
 
 	resetReq := redfishClient.ResetRequestBody{}
 	resetReq.ResetType = redfishClient.RESETTYPE_FORCE_OFF
-	m.On("ResetSystem", context.Background(), systemID, resetReq).
+	m.On("ResetSystem", ctx, systemID, resetReq).
 		Times(1).
 		Return(redfishClient.RedfishError{}, &http.Response{StatusCode: 401},
 			redfishClient.GenericOpenAPIError{})
@@ -244,9 +287,10 @@ func TestNewRedfishRemoteDirectClient(t *testing.T) {
 	defer m.AssertExpectations(t)
 
 	_, err := NewRedfishRemoteDirectClient(
-		context.Background(),
 		defaultURL,
 		computerSystemID,
+		"username",
+		"password",
 		"/tmp/test.iso",
 		true,
 		false,
@@ -255,9 +299,10 @@ func TestNewRedfishRemoteDirectClient(t *testing.T) {
 
 	// Test with empty remote URL
 	_, err = NewRedfishRemoteDirectClient(
-		context.Background(),
 		"",
 		computerSystemID,
+		"username",
+		"password",
 		"/tmp/test.iso",
 		false,
 		false,
@@ -267,9 +312,10 @@ func TestNewRedfishRemoteDirectClient(t *testing.T) {
 
 	// Test with empty ephemeral NodeID
 	_, err = NewRedfishRemoteDirectClient(
-		context.Background(),
 		defaultURL,
 		"",
+		"username",
+		"password",
 		"/tmp/test.iso",
 		false,
 		false,
@@ -279,9 +325,10 @@ func TestNewRedfishRemoteDirectClient(t *testing.T) {
 
 	// Test with empty Iso Path
 	_, err = NewRedfishRemoteDirectClient(
-		context.Background(),
 		defaultURL,
 		computerSystemID,
+		"username",
+		"password",
 		"",
 		false,
 		false,
@@ -294,9 +341,10 @@ func getDefaultRedfishRemoteDirectObj(t *testing.T, api redfishAPI.RedfishAPI) R
 	t.Helper()
 
 	rDCfg, err := NewRedfishRemoteDirectClient(
-		context.Background(),
 		defaultURL,
 		computerSystemID,
+		"username",
+		"password",
 		"/tmp/test.iso",
 		false,
 		false,
diff --git a/pkg/remote/remote_direct.go b/pkg/remote/remote_direct.go
index 0a328ef21..0c8784d6c 100644
--- a/pkg/remote/remote_direct.go
+++ b/pkg/remote/remote_direct.go
@@ -1,7 +1,6 @@
 package remote
 
 import (
-	"context"
 	"fmt"
 	"net/url"
 	"strings"
@@ -23,7 +22,11 @@ type Client interface {
 }
 
 // Get remotedirect client based on config
-func getRemoteDirectClient(remoteConfig *config.RemoteDirect, remoteURL string) (Client, error) {
+func getRemoteDirectClient(
+	remoteConfig *config.RemoteDirect,
+	remoteURL string,
+	username string,
+	password string) (Client, error) {
 	var client Client
 	switch remoteConfig.RemoteType {
 	case AirshipRemoteTypeRedfish:
@@ -44,9 +47,10 @@ func getRemoteDirectClient(remoteConfig *config.RemoteDirect, remoteURL string)
 		nodeID := urlPath[len(urlPath)-1]
 
 		client, err = redfish.NewRedfishRemoteDirectClient(
-			context.Background(),
 			baseURL,
 			nodeID,
+			username,
+			password,
 			remoteConfig.IsoURL,
 			remoteConfig.Insecure,
 			remoteConfig.UseProxy,
@@ -63,56 +67,60 @@ func getRemoteDirectClient(remoteConfig *config.RemoteDirect, remoteURL string)
 	return client, nil
 }
 
-func getRemoteDirectConfig(settings *environment.AirshipCTLSettings) (*config.RemoteDirect, string, error) {
+func getRemoteDirectConfig(settings *environment.AirshipCTLSettings) (
+	remoteConfig *config.RemoteDirect,
+	remoteURL string,
+	username string,
+	password string,
+	err error) {
 	cfg := settings.Config()
 	bootstrapSettings, err := cfg.CurrentContextBootstrapInfo()
 	if err != nil {
-		return nil, "", err
+		return nil, "", "", "", err
 	}
 
-	remoteConfig := bootstrapSettings.RemoteDirect
+	remoteConfig = bootstrapSettings.RemoteDirect
 	if remoteConfig == nil {
-		return nil, "", config.ErrMissingConfig{What: "RemoteDirect options not defined in bootstrap config"}
+		return nil, "", "", "", config.ErrMissingConfig{What: "RemoteDirect options not defined in bootstrap config"}
 	}
 
-	root, err := cfg.CurrentContextEntryPoint(config.Ephemeral, "")
+	bundlePath, err := cfg.CurrentContextEntryPoint(config.Ephemeral, "")
 	if err != nil {
-		return nil, "", err
+		return nil, "", "", "", err
 	}
 
-	docBundle, err := document.NewBundleByPath(root)
+	docBundle, err := document.NewBundleByPath(bundlePath)
 	if err != nil {
-		return nil, "", err
+		return nil, "", "", "", err
 	}
 
 	selector := document.NewEphemeralBMHSelector()
-	docs, err := docBundle.Select(selector)
+	doc, err := docBundle.SelectOne(selector)
 	if err != nil {
-		return nil, "", err
-	}
-	if len(docs) == 0 {
-		return nil, "", document.ErrDocNotFound{
-			Selector: selector,
-		}
+		return nil, "", "", "", err
 	}
 
-	// NOTE If filter returned more than one document chose first
-	remoteURL, err := docs[0].GetString("spec.bmc.address")
+	remoteURL, err = document.GetBMHBMCAddress(doc)
 	if err != nil {
-		return nil, "", err
+		return nil, "", "", "", err
 	}
 
-	return remoteConfig, remoteURL, nil
+	username, password, err = document.GetBMHBMCCredentials(doc, docBundle)
+	if err != nil {
+		return nil, "", "", "", err
+	}
+
+	return remoteConfig, remoteURL, username, password, nil
 }
 
 // Top level function to execute remote direct based on remote type
 func DoRemoteDirect(settings *environment.AirshipCTLSettings) error {
-	remoteConfig, remoteURL, err := getRemoteDirectConfig(settings)
+	remoteConfig, remoteURL, username, password, err := getRemoteDirectConfig(settings)
 	if err != nil {
 		return err
 	}
 
-	client, err := getRemoteDirectClient(remoteConfig, remoteURL)
+	client, err := getRemoteDirectClient(remoteConfig, remoteURL, username, password)
 	if err != nil {
 		return err
 	}