diff --git a/plugin/plugin.go b/plugin/plugin.go index 8fc6543..25cc5a3 100644 --- a/plugin/plugin.go +++ b/plugin/plugin.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "strings" + "time" "github.com/Azure/azure-sdk-for-go/profiles/latest/network/mgmt/network" "github.com/Azure/azure-sdk-for-go/profiles/latest/resources/mgmt/resources" @@ -27,6 +28,31 @@ var ( _ pb.HostPluginServiceServer = (*AzurePlugin)(nil) ) +func rotateCredFromCallback(ctx context.Context, catalog *hostcatalogs.HostCatalog) (*pb.HostCatalogPersisted, error) { + authzInfo, err := getAuthorizationInfo(catalog) + if err != nil { + return nil, fmt.Errorf("error getting hauth config when creating catalog: %w", err) + } + newPass, err := rotateCredential(ctx, authzInfo) + if err != nil { + return nil, fmt.Errorf("error rotating credentials when creating catalog: %w", err) + } + if newPass == nil { + return nil, errors.New("new credential back from rotate is nil") + } + newSecrets, err := structpb.NewStruct(map[string]interface{}{ + constSecretId: *newPass.KeyId, + constSecretValue: *newPass.SecretText, + constCredsLastRotatedTime: time.Now().Format(time.RFC3339Nano), + }) + if err != nil { + return nil, fmt.Errorf("error formatting new credential data as struct") + } + return &pb.HostCatalogPersisted{ + Secrets: newSecrets, + }, err +} + func (p *AzurePlugin) OnCreateCatalog(ctx context.Context, req *pb.OnCreateCatalogRequest) (*pb.OnCreateCatalogResponse, error) { if err := validateCatalog(req.GetCatalog()); err != nil { return nil, err diff --git a/plugin/plugin_test.go b/plugin/plugin_test.go index a505b3c..7e5015c 100644 --- a/plugin/plugin_test.go +++ b/plugin/plugin_test.go @@ -5,12 +5,16 @@ import ( "os" "path/filepath" "testing" + "time" "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/hostcatalogs" "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/hostsets" pb "github.com/hashicorp/boundary/sdk/pbs/plugin" "github.com/hashicorp/go-secure-stdlib/parseutil" "github.com/kr/pretty" + "github.com/manicminer/hamilton/auth" + "github.com/manicminer/hamilton/environments" + "github.com/manicminer/hamilton/msgraph" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "google.golang.org/protobuf/types/known/structpb" @@ -287,6 +291,33 @@ func TestUpdateCatalog(t *testing.T) { assert.Error(t, err) } +func waitForCreds(t *testing.T, authzInfo *AuthorizationInfo, shouldWork bool) auth.Authorizer { + t.Helper() + require := require.New(t) + ctx := context.Background() + auth, err := auth.NewClientSecretAuthorizer( + ctx, + environments.Global, + auth.MsGraph, + auth.TokenVersion2, + authzInfo.AuthParams.TenantId, + authzInfo.AuthParams.ClientId, + authzInfo.AuthParams.SecretValue) + require.NoError(err) + + for { + _, err := auth.Token() + switch { + case shouldWork && err == nil: + return auth + case !shouldWork && err != nil: + return nil + default: + time.Sleep(30 * time.Second) + } + } +} + func testGetHostStructs(t *testing.T) (*hostcatalogs.HostCatalog, []*hostsets.HostSet) { require := require.New(t) wd, err := os.Getwd() @@ -330,3 +361,44 @@ func testGetHostStructs(t *testing.T) (*hostcatalogs.HostCatalog, []*hostsets.Ho return hc, hses } + +// This function creates a new secret so we don't revoke the original one +// leading into a test. This will serve as our "initial secret" for a test. We +// also return a few cached items. It is expected that the host catalog contains +// Secrets. +func testCreateInitialSecret(t *testing.T, hostCatalog *hostcatalogs.HostCatalog) (*AuthorizationInfo, *msgraph.PasswordCredential, func()) { + require := require.New(t) + ctx := context.Background() + + var initialCred *msgraph.PasswordCredential + + // Set up the data, get the config + authzInfo, err := getAuthorizationInfo(hostCatalog) + require.NoError(err) + + // Get the credential + initialCred, err = addCredential(ctx, authzInfo) + require.NoError(err) + require.NotNil(initialCred) + require.NotNil(initialCred.KeyId) + require.NotNil(initialCred.SecretText) + + origAuthConfig := *authzInfo + origAuthConfig.AuthParams.SecretId = *initialCred.KeyId + cleanup := func() { + if err := removeCredential( + ctx, + &origAuthConfig, + ); err != nil { + // It may not exist because it's been rotated + require.Contains(err.Error(), "No password credential found with keyId", err.Error()) + } + } + + authzInfo.AuthParams.SecretId = *initialCred.KeyId + authzInfo.AuthParams.SecretValue = *initialCred.SecretText + authzInfo.HamiltonConfig.ClientSecret = *initialCred.SecretText + waitForCreds(t, authzInfo, true) + + return authzInfo, initialCred, cleanup +}