Repository: common-fate/glide (language: go)

Issue:
IDP and Provider settings should be checked when configured and or before deploying
Done = gdeploy CLI checks that the Azure AD directory sync settings are valid when gdeploy update or gdeploy create is run

Done = CLI should check by making a call to Microsoft Graph and checking for success (e.g. trying to list users)

Done = gdeploy should return a descriptive error if the API call fails.

Done = when running gdeploy sso configure the parameters should be checked for correctness before the SSM Parameter is written to AWS.

Done= When setting up azure provider, do a live check against the provided admin group id when setting in gdeploy

```
➜ gdeploy sso configure
? The SSO provider to deploy with Azure
[i] You can follow along with the Azure setup guide in our docs: https://docs.commonfate.io/granted-approvals/sso/azure
? Tenant ID: 0d928b90-2449-4c9e-90cc-acce6171ad53
? Client ID: 24bfc3be-4b7b-4d58-9fae-b916dfcaedc8
? Client Secret: ****************************************
[✔] SSM Parameters set successfully
```


Discussion / hints:

Fix patch:
diff --git a/accesshandler/pkg/providers/azure/ad/ad.go b/accesshandler/pkg/providers/azure/ad/ad.go
index cac8aae8b..5b83bb133 100644
--- a/accesshandler/pkg/providers/azure/ad/ad.go
+++ b/accesshandler/pkg/providers/azure/ad/ad.go
@@ -7,6 +7,7 @@ import (
 	"github.com/AzureAD/microsoft-authentication-library-for-go/apps/confidential"
 	"github.com/common-fate/granted-approvals/pkg/gconfig"
 	"github.com/invopop/jsonschema"
+	"github.com/pkg/errors"
 	"go.uber.org/zap"
 )
 
@@ -49,6 +50,17 @@ func (a *Provider) Init(ctx context.Context) error {
 	a.token.Set(token.AccessToken)
 	return nil
 }
+func (p *Provider) TestConfig(ctx context.Context) error {
+	_, err := p.ListUsers(ctx)
+	if err != nil {
+		return errors.Wrap(err, "failed to list users while testing azure provider configuration")
+	}
+	_, err = p.ListGroups(ctx)
+	if err != nil {
+		return errors.Wrap(err, "failed to list groups while testing azure provider configuration")
+	}
+	return nil
+}
 
 // ArgSchema returns the schema for the AzureAD provider.
 func (o *Provider) ArgSchema() *jsonschema.Schema {
diff --git a/accesshandler/pkg/providers/okta/okta.go b/accesshandler/pkg/providers/okta/okta.go
index 869d1e894..d3e726985 100644
--- a/accesshandler/pkg/providers/okta/okta.go
+++ b/accesshandler/pkg/providers/okta/okta.go
@@ -6,6 +6,8 @@ import (
 	"github.com/common-fate/granted-approvals/pkg/gconfig"
 	"github.com/invopop/jsonschema"
 	"github.com/okta/okta-sdk-golang/v2/okta"
+	"github.com/okta/okta-sdk-golang/v2/okta/query"
+	"github.com/pkg/errors"
 	"go.uber.org/zap"
 )
 
@@ -35,6 +37,17 @@ func (o *Provider) Init(ctx context.Context) error {
 	o.client = client
 	return nil
 }
+func (p *Provider) TestConfig(ctx context.Context) error {
+	_, _, err := p.client.User.ListUsers(ctx, &query.Params{})
+	if err != nil {
+		return errors.Wrap(err, "failed to list users while testing okta provider configuration")
+	}
+	_, _, err = p.client.Group.ListGroups(ctx, &query.Params{})
+	if err != nil {
+		return errors.Wrap(err, "failed to list groups while testing okta provider configuration")
+	}
+	return nil
+}
 
 // ArgSchema returns the schema for the Okta provider.
 func (o *Provider) ArgSchema() *jsonschema.Schema {
diff --git a/cmd/gdeploy/commands/notifications/slack/configure.go b/cmd/gdeploy/commands/notifications/slack/configure.go
index 642bf14c0..b7f281c3d 100644
--- a/cmd/gdeploy/commands/notifications/slack/configure.go
+++ b/cmd/gdeploy/commands/notifications/slack/configure.go
@@ -63,8 +63,10 @@ var configureSlackCommand = cli.Command{
 			}
 		}
 
-		// @TODO add the provider test call here before progressing
-		// e.g idp.IdentityProvider.TestConfig(ctx)
+		err = deploy.RunConfigTest(ctx, &slack)
+		if err != nil {
+			return err
+		}
 
 		// if tests pass, dump the config and update in the deployment config
 		newConfig, err := cfg.Dump(ctx, gconfig.SSMDumper{Suffix: dc.Deployment.Parameters.DeploymentSuffix})
@@ -76,10 +78,10 @@ var configureSlackCommand = cli.Command{
 		if err != nil {
 			return err
 		}
-		clio.Warn("Your changes won't be applied until you redeploy. Run 'gdeploy update' to apply the changes to your CloudFormation deployment.")
-		clio.Success("Successfully enabled Slack")
 
-		clio.Info("Run: `gdeploy notifications slack test --email=<your_slack_email>` to test your Slack notifications")
+		clio.Success("Successfully configured Slack")
+		clio.Warn("Your changes won't be applied until you redeploy. Run 'gdeploy update' to apply the changes to your CloudFormation deployment.")
+		clio.Warn("Run: `gdeploy notifications slack test --email=<your_slack_email>` to send a test DM")
 
 		return nil
 	},
diff --git a/cmd/gdeploy/commands/provider/add.go b/cmd/gdeploy/commands/provider/add.go
index 42fb9ef85..ccbae9d5b 100644
--- a/cmd/gdeploy/commands/provider/add.go
+++ b/cmd/gdeploy/commands/provider/add.go
@@ -78,8 +78,10 @@ var addCommand = cli.Command{
 			}
 		}
 
-		// @TODO add the provider test call here before progressing
-		// e.g provider.Provider.TestConfig(ctx)
+		err = deploy.RunConfigTest(ctx, provider.Provider)
+		if err != nil {
+			return err
+		}
 
 		// if tests pass, dump the config and update in the deployment config
 		// secret path args requires the id, all provider config includes the provider ID in the path
diff --git a/cmd/gdeploy/commands/sso/sso.go b/cmd/gdeploy/commands/sso/sso.go
index e767811e7..8be0b19da 100644
--- a/cmd/gdeploy/commands/sso/sso.go
+++ b/cmd/gdeploy/commands/sso/sso.go
@@ -88,9 +88,10 @@ var configureCommand = cli.Command{
 				}
 			}
 
-			// @TODO add the provider test call here before progressing
-			// e.g idp.IdentityProvider.TestConfig(ctx)
-
+			err = deploy.RunConfigTest(ctx, idp.IdentityProvider)
+			if err != nil {
+				return err
+			}
 			// if tests pass, dump the config and update in the deployment config
 			newConfig, err := cfg.Dump(ctx, gconfig.SSMDumper{Suffix: dc.Deployment.Parameters.DeploymentSuffix})
 			if err != nil {
diff --git a/pkg/deploy/config.go b/pkg/deploy/config.go
index 8d2fc7d1f..5820d8b2c 100644
--- a/pkg/deploy/config.go
+++ b/pkg/deploy/config.go
@@ -21,6 +21,7 @@ import (
 	"github.com/pkg/errors"
 	"github.com/segmentio/ksuid"
 	"github.com/urfave/cli/v2"
+	"go.uber.org/zap"
 	"gopkg.in/yaml.v3"
 )
 
@@ -169,6 +170,35 @@ func UnmarshalProviderMap(data string) (ProviderMap, error) {
 	return i, nil
 }
 
+// RunConfigTest runs ConfigTest() if it is implemented on the interface
+func RunConfigTest(ctx context.Context, testable interface{}) error {
+	if tester, ok := testable.(gconfig.Tester); ok {
+		si := spinner.New(spinner.CharSets[14], 100*time.Millisecond)
+		si.Suffix = " running tests using this configuration."
+		si.Writer = os.Stderr
+		si.Start()
+		defer si.Stop()
+
+		// disable zap logging which can be emitted by components intended to run in a server environement.
+		// after the test, the original logger is restored
+		restore := zap.ReplaceGlobals(zap.NewNop())
+		defer restore()
+		if initer, ok := testable.(gconfig.Initer); ok {
+			err := initer.Init(ctx)
+			if err != nil {
+				return err
+			}
+		}
+		err := tester.TestConfig(ctx)
+		if err != nil {
+			return err
+		}
+		si.Stop()
+		clio.Success("Configuration tests passed!")
+	}
+	return nil
+}
+
 // CLIPrompt prompts the user to enter a value for the config varsiable
 // in a CLI context. If the config variable implements Defaulter, the
 // default value is returned and the user is not prompted for any input.
diff --git a/pkg/gconfig/interfaces.go b/pkg/gconfig/interfaces.go
index 18c8c7a80..22a18aa1e 100644
--- a/pkg/gconfig/interfaces.go
+++ b/pkg/gconfig/interfaces.go
@@ -10,3 +10,9 @@ type Configer interface {
 type Initer interface {
 	Init(ctx context.Context) error
 }
+
+// Tester interface is used to run tests on config
+type Tester interface {
+	// TestConfig is expected to be called on a loaded config after Init has been called.
+	TestConfig(ctx context.Context) error
+}
diff --git a/pkg/identity/identitysync/azure.go b/pkg/identity/identitysync/azure.go
index 2ac4d1ace..8f973aafa 100644
--- a/pkg/identity/identitysync/azure.go
+++ b/pkg/identity/identitysync/azure.go
@@ -11,6 +11,7 @@ import (
 	"github.com/AzureAD/microsoft-authentication-library-for-go/apps/confidential"
 	"github.com/common-fate/granted-approvals/pkg/gconfig"
 	"github.com/common-fate/granted-approvals/pkg/identity"
+	"github.com/pkg/errors"
 )
 
 const MSGraphBaseURL = "https://graph.microsoft.com/v1.0"
@@ -49,6 +50,17 @@ func (s *AzureSync) Init(ctx context.Context) error {
 	s.token.Set(token.AccessToken)
 	return nil
 }
+func (s *AzureSync) TestConfig(ctx context.Context) error {
+	_, err := s.ListUsers(ctx)
+	if err != nil {
+		return errors.Wrap(err, "failed to list users while testing azure identity provider configuration")
+	}
+	_, err = s.ListGroups(ctx)
+	if err != nil {
+		return errors.Wrap(err, "failed to list groups while testing azure identity provider configuration")
+	}
+	return nil
+}
 
 type ListUsersResponse struct {
 	OdataContext  string      `json:"@odata.context"`
diff --git a/pkg/identity/identitysync/google.go b/pkg/identity/identitysync/google.go
index 64a6639b2..2fb342776 100644
--- a/pkg/identity/identitysync/google.go
+++ b/pkg/identity/identitysync/google.go
@@ -5,6 +5,7 @@ import (
 
 	"github.com/common-fate/granted-approvals/pkg/gconfig"
 	"github.com/common-fate/granted-approvals/pkg/identity"
+	"github.com/pkg/errors"
 	"golang.org/x/oauth2/google"
 	admin "google.golang.org/api/admin/directory/v1"
 	"google.golang.org/api/option"
@@ -40,7 +41,17 @@ func (s *GoogleSync) Init(ctx context.Context) error {
 	s.client = adminService
 	return nil
 }
-
+func (s *GoogleSync) TestConfig(ctx context.Context) error {
+	_, err := s.ListUsers(ctx)
+	if err != nil {
+		return errors.Wrap(err, "failed to list users while testing google identity provider configuration")
+	}
+	_, err = s.ListGroups(ctx)
+	if err != nil {
+		return errors.Wrap(err, "failed to list groups while testing google identity provider configuration")
+	}
+	return nil
+}
 func (c *GoogleSync) ListGroups(ctx context.Context) ([]identity.IdpGroup, error) {
 
 	idpGroups := []identity.IdpGroup{}
diff --git a/pkg/identity/identitysync/okta.go b/pkg/identity/identitysync/okta.go
index 23d3ffa83..2957829d4 100644
--- a/pkg/identity/identitysync/okta.go
+++ b/pkg/identity/identitysync/okta.go
@@ -7,6 +7,7 @@ import (
 	"github.com/common-fate/granted-approvals/pkg/identity"
 	"github.com/okta/okta-sdk-golang/v2/okta"
 	"github.com/okta/okta-sdk-golang/v2/okta/query"
+	"github.com/pkg/errors"
 )
 
 type OktaSync struct {
@@ -35,6 +36,18 @@ func (s *OktaSync) Init(ctx context.Context) error {
 	return nil
 }
 
+func (s *OktaSync) TestConfig(ctx context.Context) error {
+	_, err := s.ListUsers(ctx)
+	if err != nil {
+		return errors.Wrap(err, "failed to list users while testing okta identity provider configuration")
+	}
+	_, err = s.ListGroups(ctx)
+	if err != nil {
+		return errors.Wrap(err, "failed to list groups while testing okta identity provider configuration")
+	}
+	return nil
+}
+
 // userFromOktaUser converts a Okta user to the identityprovider interface user type
 func (o *OktaSync) idpUserFromOktaUser(ctx context.Context, oktaUser *okta.User) (identity.IdpUser, error) {
 	u := identity.IdpUser{
diff --git a/pkg/notifiers/slack/notifier.go b/pkg/notifiers/slack/notifier.go
index 0bb7d4d65..fa04736c8 100644
--- a/pkg/notifiers/slack/notifier.go
+++ b/pkg/notifiers/slack/notifier.go
@@ -7,6 +7,7 @@ import (
 	"github.com/aws/aws-lambda-go/events"
 	"github.com/common-fate/ddb"
 	"github.com/common-fate/granted-approvals/pkg/gconfig"
+	"github.com/pkg/errors"
 	"github.com/slack-go/slack"
 	"go.uber.org/zap"
 )
@@ -31,7 +32,13 @@ func (s *SlackNotifier) Init(ctx context.Context) error {
 	s.client = slack.New(s.apiToken.Get())
 	return nil
 }
-
+func (s *SlackNotifier) TestConfig(ctx context.Context) error {
+	_, err := s.client.GetUsersContext(ctx)
+	if err != nil {
+		return errors.Wrap(err, "failed to list users while testing slack configuration")
+	}
+	return nil
+}
 func (n *SlackNotifier) HandleEvent(ctx context.Context, event events.CloudWatchEvent) (err error) {
 	log := zap.S()
 


Test patch:
diff --git a/cmd/gdeploy/commands/notifications/slack/test.go b/cmd/gdeploy/commands/notifications/slack/test.go
index be4125d67..03d98b1a6 100644
--- a/cmd/gdeploy/commands/notifications/slack/test.go
+++ b/cmd/gdeploy/commands/notifications/slack/test.go
@@ -40,7 +40,7 @@ var testSlackCommand = cli.Command{
 		if err != nil {
 			return err
 		}
-		clio.Success("Successfully send slack test message")
+		clio.Success("Successfully sent a slack test message to %s", c.String("email"))
 		return nil
 	},
 }