diff --git a/providers/goTo.go b/providers/goTo.go index 856c96a46..1a2f945c4 100644 --- a/providers/goTo.go +++ b/providers/goTo.go @@ -1,7 +1,17 @@ package providers -const GoTo Provider = "goTo" +import "github.com/amp-labs/connectors/common" +const ( + GoTo Provider = "goTo" + // ModuleGoTo covers the api.getgo.com base URL, which serves multiple GoTo + // products (admin, meetings, webinars, etc). We name it just "goTo" so users + // don't have to guess which specific product it maps to. + ModuleGoTo common.ModuleID = "goTo" + ModuleGoToConnect common.ModuleID = "goToConnect" +) + +// nolint: funlen func init() { SetInfo(GoTo, ProviderInfo{ DisplayName: "GoTo", @@ -39,5 +49,38 @@ func init() { Subscribe: false, Write: false, }, + PostAuthInfoNeeded: true, + Metadata: &ProviderMetadata{ + PostAuthentication: []MetadataItemPostAuthentication{ + { + Name: "accountKey", + ModuleDependencies: &ModuleDependencies{ + ModuleGoTo: ModuleDependency{}, + ModuleGoToConnect: ModuleDependency{}, + }, + }, + }, + }, + DefaultModule: ModuleGoTo, + Modules: &Modules{ + ModuleGoTo: { + BaseURL: "https://api.getgo.com", + DisplayName: "GoTo", + Support: Support{ + Read: false, + Subscribe: false, + Write: false, + }, + }, + ModuleGoToConnect: { + BaseURL: "https://api.goto.com", + DisplayName: "GoTo Connect", + Support: Support{ + Read: false, + Subscribe: false, + Write: false, + }, + }, + }, }) } diff --git a/providers/goto/authmetadata.go b/providers/goto/authmetadata.go new file mode 100644 index 000000000..b712f8014 --- /dev/null +++ b/providers/goto/authmetadata.go @@ -0,0 +1,69 @@ +package gotoconn + +import ( + "context" + "strconv" + + "github.com/amp-labs/connectors/common" + "github.com/amp-labs/connectors/common/urlbuilder" +) + +func (c *Connector) GetPostAuthInfo(ctx context.Context) (*common.PostAuthInfo, error) { + accountKey, err := c.retrieveAccountKey(ctx) + if err != nil { + return nil, err + } + + if accountKey == "" { + return nil, common.ErrMissingExpectedValues + } + + c.accountKey = accountKey + + catalogVars := map[string]string{ + "accountKey": accountKey, + } + + return &common.PostAuthInfo{ + CatalogVars: &catalogVars, + }, nil +} + +func (c *Connector) retrieveAccountKey(ctx context.Context) (string, error) { + url, err := c.getMeURL() + if err != nil { + return "", err + } + + resp, err := c.JSONHTTPClient().Get(ctx, url.String()) + if err != nil { + return "", err + } + + data, err := common.UnmarshalJSON[map[string]any](resp) + if err != nil { + return "", common.ErrFailedToUnmarshalBody + } + + if data == nil { + return "", common.ErrMissingExpectedValues + } + + rawAccountKey, ok := (*data)["accountKey"] + if !ok { + return "", common.ErrMissingExpectedValues + } + + switch v := rawAccountKey.(type) { + case string: + return v, nil + case float64: + return strconv.FormatInt(int64(v), 10), nil + default: + return "", common.ErrMissingExpectedValues + } +} + +func (c *Connector) getMeURL() (*urlbuilder.URL, error) { + return urlbuilder.New(c.ProviderInfo().BaseURL, "/admin/rest/v1/me") +} diff --git a/providers/goto/authmetamodel.go b/providers/goto/authmetamodel.go new file mode 100644 index 000000000..12253b7a3 --- /dev/null +++ b/providers/goto/authmetamodel.go @@ -0,0 +1,18 @@ +package gotoconn + +type AuthMetadataVars struct { + AccountKey string +} + +// NewAuthMetadataVars parses map into the model. +func NewAuthMetadataVars(dictionary map[string]string) *AuthMetadataVars { + return &AuthMetadataVars{ + AccountKey: dictionary["accountKey"], + } +} + +func (v AuthMetadataVars) AsMap() *map[string]string { + return &map[string]string{ + "accountKey": v.AccountKey, + } +} diff --git a/providers/goto/connector.go b/providers/goto/connector.go new file mode 100644 index 000000000..23f1ceae2 --- /dev/null +++ b/providers/goto/connector.go @@ -0,0 +1,40 @@ +// Package gotoconn implements the GoTo connector. +// +// The package is named "gotoconn" instead of "goto" because "goto" is a +// reserved keyword in Go and cannot be used as a package identifier. The +// "conn" suffix is short for "connector". +package gotoconn + +import ( + "github.com/amp-labs/connectors/common" + "github.com/amp-labs/connectors/internal/components" + "github.com/amp-labs/connectors/providers" +) + +type Connector struct { + // Basic connector + *components.Connector + + // Require authenticated client & account + common.RequireAuthenticatedClient + common.PostAuthInfo + + accountKey string +} + +func NewConnector(params common.ConnectorParams) (*Connector, error) { + conn, err := components.Initialize(providers.GoTo, params, constructor) + if err != nil { + return nil, err + } + + authMetadata := NewAuthMetadataVars(params.Metadata) + + conn.accountKey = authMetadata.AccountKey + + return conn, nil +} + +func constructor(base *components.Connector) (*Connector, error) { + return &Connector{Connector: base}, nil +} diff --git a/test/goto/auth-metadata/main.go b/test/goto/auth-metadata/main.go new file mode 100644 index 000000000..b15734987 --- /dev/null +++ b/test/goto/auth-metadata/main.go @@ -0,0 +1,33 @@ +package main + +import ( + "context" + "log/slog" + "os/signal" + "syscall" + + "github.com/amp-labs/connectors/providers/goto" + connTest "github.com/amp-labs/connectors/test/goto" + "github.com/amp-labs/connectors/test/utils" +) + +func main() { + // Handle Ctrl-C gracefully. + ctx, done := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) + defer done() + + // Set up slog logging. + utils.SetupLogging() + + conn := connTest.GetGoToConnector(ctx) + + info, err := conn.GetPostAuthInfo(ctx) + if err != nil || info.CatalogVars == nil { + utils.Fail("error obtaining auth info", "error", err) + } + + accountKey := gotoconn.NewAuthMetadataVars(*info.CatalogVars).AccountKey + + // Log the retrieved account key. + slog.Info("retrieved auth metadata", "account key", accountKey) +} diff --git a/test/goto/connector.go b/test/goto/connector.go new file mode 100644 index 000000000..66d3fd8e9 --- /dev/null +++ b/test/goto/connector.go @@ -0,0 +1,51 @@ +package goto_test + +import ( + "context" + "net/http" + + "github.com/amp-labs/connectors/common" + "github.com/amp-labs/connectors/common/scanning/credscanning" + "github.com/amp-labs/connectors/providers" + "github.com/amp-labs/connectors/providers/goto" + "github.com/amp-labs/connectors/test/utils" + "golang.org/x/oauth2" +) + +func GetGoToConnector(ctx context.Context) *gotoconn.Connector { + filePath := credscanning.LoadPath(providers.GoTo) + reader := utils.MustCreateProvCredJSON(filePath, true) + + client, err := common.NewOAuthHTTPClient(ctx, + common.WithOAuthClient(http.DefaultClient), + common.WithOAuthConfig(getConfig(reader)), + common.WithOAuthToken(reader.GetOauthToken()), + ) + if err != nil { + utils.Fail(err.Error()) + } + + conn, err := gotoconn.NewConnector(common.ConnectorParams{ + AuthenticatedClient: client, + }) + if err != nil { + utils.Fail("create goto connector", "error: ", err) + } + + return conn +} + +func getConfig(reader *credscanning.ProviderCredentials) *oauth2.Config { + cfg := &oauth2.Config{ + ClientID: reader.Get(credscanning.Fields.ClientId), + ClientSecret: reader.Get(credscanning.Fields.ClientSecret), + RedirectURL: "https://dev-api.withampersand.com/callbacks/v1/oauth", + Endpoint: oauth2.Endpoint{ + AuthURL: "https://authentication.logmeininc.com/oauth/authorize", + TokenURL: "https://authentication.logmeininc.com/oauth/token", + AuthStyle: oauth2.AuthStyleInParams, + }, + } + + return cfg +}