From 53c741eaa97d5b484d0a89b79ad2fe2503a7b72c Mon Sep 17 00:00:00 2001 From: Steve Ramage Date: Wed, 20 Nov 2024 11:57:51 -0800 Subject: [PATCH] WIP 193 --- cmd/create.go | 132 +------------------------------- cmd/delete.go | 116 +--------------------------- cmd/get.go | 122 +----------------------------- cmd/login.go | 13 ++-- cmd/oidc.go | 46 ++++++------ cmd/reset-store.go | 11 +-- cmd/update.go | 94 +---------------------- external/oidc/done.gohtml | 27 +++---- external/oidc/error.gohtml | 4 +- external/oidc/index.gohtml | 61 ++++++++++++--- external/oidc/success.gohtml | 25 ++++++- external/rest/create.go | 141 +++++++++++++++++++++++++++++++++++ external/rest/delete.go | 125 +++++++++++++++++++++++++++++++ external/rest/get.go | 130 ++++++++++++++++++++++++++++++++ external/rest/update.go | 100 +++++++++++++++++++++++++ 15 files changed, 628 insertions(+), 519 deletions(-) create mode 100644 external/rest/create.go create mode 100644 external/rest/delete.go create mode 100644 external/rest/get.go create mode 100644 external/rest/update.go diff --git a/cmd/create.go b/cmd/create.go index 3d404fa..7a9baf5 100644 --- a/cmd/create.go +++ b/cmd/create.go @@ -5,18 +5,13 @@ import ( gojson "encoding/json" "fmt" "github.com/elasticpath/epcc-cli/external/aliases" - "github.com/elasticpath/epcc-cli/external/autofill" "github.com/elasticpath/epcc-cli/external/completion" - "github.com/elasticpath/epcc-cli/external/encoding" "github.com/elasticpath/epcc-cli/external/httpclient" "github.com/elasticpath/epcc-cli/external/json" "github.com/elasticpath/epcc-cli/external/resources" - "github.com/elasticpath/epcc-cli/external/shutdown" + "github.com/elasticpath/epcc-cli/external/rest" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" - "io" - "net/http" - "net/url" "strings" ) @@ -105,7 +100,7 @@ func NewCreateCommand(parentCmd *cobra.Command) func() { } } - body, err := createInternal(context.Background(), overrides, append([]string{resourceName}, args...), autoFillOnCreate, setAlias, skipAliases) + body, err := rest.CreateInternal(context.Background(), overrides, append([]string{resourceName}, args...), autoFillOnCreate, setAlias, skipAliases) if err != nil { return err @@ -241,126 +236,3 @@ func NewCreateCommand(parentCmd *cobra.Command) func() { return resetFunc } - -func createInternal(ctx context.Context, overrides *httpclient.HttpParameterOverrides, args []string, autoFillOnCreate bool, aliasName string, skipAliases bool) (string, error) { - shutdown.OutstandingOpCounter.Add(1) - defer shutdown.OutstandingOpCounter.Done() - - // Find Resource - resource, ok := resources.GetResourceByName(args[0]) - if !ok { - return "", fmt.Errorf("could not find resource %s", args[0]) - } - - if resource.CreateEntityInfo == nil { - return "", fmt.Errorf("resource %s doesn't support CREATE", args[0]) - } - - // Count ids in CreateEntity - resourceURL := resource.CreateEntityInfo.Url - - idCount, err := resources.GetNumberOfVariablesNeeded(resourceURL) - - if err != nil { - return "", err - } - - // Replace ids with args in resourceURL - resourceURL, err = resources.GenerateUrl(resource.CreateEntityInfo, args[1:], true) - - if overrides.OverrideUrlPath != "" { - log.Warnf("Overriding URL Path from %s to %s", resourceURL, overrides.OverrideUrlPath) - resourceURL = overrides.OverrideUrlPath - } - - if err != nil { - return "", err - } - - var resp *http.Response = nil - var resBody []byte - - if resource.CreateEntityInfo.ContentType == "multipart/form-data" { - - byteBuf, contentType, err := encoding.ToMultiPartEncoding(args[(idCount+1):], resource.NoWrapping, resource.JsonApiFormat == "complaint", resource.Attributes) - if err != nil { - return "", err - } - - // Submit request - resp, err = httpclient.DoFileRequest(ctx, resourceURL, byteBuf, contentType) - - } else { - // Assume it's application/json - - params := url.Values{} - - for _, v := range overrides.QueryParameters { - keyAndValue := strings.SplitN(v, "=", 2) - if len(keyAndValue) != 2 { - return "", fmt.Errorf("Could not parse query parameter %v, all query parameters should be a key and value format", keyAndValue) - } - params.Add(keyAndValue[0], keyAndValue[1]) - } - - if !resource.NoWrapping { - args = append(args, "type", resource.JsonApiType) - } - // Create the body from remaining args - - jsonArgs := args[(idCount + 1):] - if autoFillOnCreate { - autofilledData := autofill.GetJsonArrayForResource(&resource) - - jsonArgs = append(autofilledData, jsonArgs...) - } - - body, err := json.ToJson(jsonArgs, resource.NoWrapping, resource.JsonApiFormat == "compliant", resource.Attributes, true) - - if err != nil { - return "", err - } - - // Submit request - resp, err = httpclient.DoRequest(ctx, "POST", resourceURL, params.Encode(), strings.NewReader(body)) - - } - - if err != nil { - return "", fmt.Errorf("got error %s", err.Error()) - } else if resp == nil { - return "", fmt.Errorf("got nil response with request: %s", resourceURL) - } - - if resp.Body != nil { - defer resp.Body.Close() - - // Print the body - resBody, err = io.ReadAll(resp.Body) - if err != nil { - log.Fatal(err) - } - - // Check if error response - if resp.StatusCode >= 400 && resp.StatusCode <= 600 { - json.PrintJson(string(resBody)) - return "", fmt.Errorf(resp.Status) - } - - // 204 is no content, so we will skip it. - if resp.StatusCode != 204 { - if !skipAliases { - aliases.SaveAliasesForResources(string(resBody)) - } - } - - if aliasName != "" { - aliases.SetAliasForResource(string(resBody), aliasName) - } - - return string(resBody), nil - } else { - return "", nil - } - -} diff --git a/cmd/delete.go b/cmd/delete.go index eaafd5e..d42aced 100644 --- a/cmd/delete.go +++ b/cmd/delete.go @@ -8,13 +8,9 @@ import ( "github.com/elasticpath/epcc-cli/external/httpclient" "github.com/elasticpath/epcc-cli/external/json" "github.com/elasticpath/epcc-cli/external/resources" - "github.com/elasticpath/epcc-cli/external/shutdown" + "github.com/elasticpath/epcc-cli/external/rest" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" - "io" - "net/http" - "net/url" - "strings" ) func NewDeleteCommand(parentCmd *cobra.Command) func() { @@ -94,7 +90,7 @@ func NewDeleteCommand(parentCmd *cobra.Command) func() { } } - body, err := deleteInternal(context.Background(), overrides, allow404, append([]string{resourceName}, args...)) + body, err := rest.DeleteInternal(context.Background(), overrides, allow404, append([]string{resourceName}, args...)) if err != nil { if body != "" { @@ -193,111 +189,3 @@ func NewDeleteCommand(parentCmd *cobra.Command) func() { return resetFunc } -func deleteInternal(ctx context.Context, overrides *httpclient.HttpParameterOverrides, allow404 bool, args []string) (string, error) { - shutdown.OutstandingOpCounter.Add(1) - defer shutdown.OutstandingOpCounter.Done() - - resource, ok := resources.GetResourceByName(args[0]) - if !ok { - return "", fmt.Errorf("could not find resource %s", args[0]) - } - - resp, err := deleteResource(ctx, overrides, args) - if err != nil { - return "", err - } - - if resp == nil { - return "", fmt.Errorf("got nil response") - } - - if resp.StatusCode < 400 { - idToDelete := aliases.ResolveAliasValuesOrReturnIdentity(resource.JsonApiType, resource.AlternateJsonApiTypesForAliases, args[len(args)-1], "id") - aliases.DeleteAliasesById(idToDelete, resource.JsonApiType) - } - if resp.Body != nil { - - defer resp.Body.Close() - - // Print the body - body, err := io.ReadAll(resp.Body) - - if err != nil { - log.Fatal(err) - } - - // Check if error response - if resp.StatusCode >= 400 && resp.StatusCode <= 600 { - if resp.StatusCode != 404 || !allow404 { - return string(body), fmt.Errorf(resp.Status) - } - } - - return string(body), nil - } else { - return "", nil - } - -} - -func deleteResource(ctx context.Context, overrides *httpclient.HttpParameterOverrides, args []string) (*http.Response, error) { - // Find Resource - resource, ok := resources.GetResourceByName(args[0]) - if !ok { - return nil, fmt.Errorf("could not find resource %s", args[0]) - } - - if resource.DeleteEntityInfo == nil { - return nil, fmt.Errorf("resource %s doesn't support DELETE", args[0]) - } - - // Replace ids with args in resourceURL - resourceURL, err := resources.GenerateUrl(resource.DeleteEntityInfo, args[1:], true) - - if err != nil { - return nil, err - } - - if overrides.OverrideUrlPath != "" { - log.Warnf("Overriding URL Path from %s to %s", resourceURL, overrides.OverrideUrlPath) - resourceURL = overrides.OverrideUrlPath - } - - params := url.Values{} - - for _, v := range overrides.QueryParameters { - keyAndValue := strings.SplitN(v, "=", 2) - if len(keyAndValue) != 2 { - return nil, fmt.Errorf("Could not parse query parameter %v, all query parameters should be a key and value format", keyAndValue) - } - params.Add(keyAndValue[0], keyAndValue[1]) - } - - idCount, err := resources.GetNumberOfVariablesNeeded(resource.DeleteEntityInfo.Url) - - if !resource.NoWrapping { - args = append(args, "type", resource.JsonApiType) - } - // Create the body from remaining args - - jsonArgs := args[(idCount + 1):] - - var payload io.Reader = nil - if len(jsonArgs) > 0 { - body, err := json.ToJson(jsonArgs, resource.NoWrapping, resource.JsonApiFormat == "compliant", resource.Attributes, true) - - if err != nil { - return nil, err - } - - payload = strings.NewReader(body) - } - - // Submit request - resp, err := httpclient.DoRequest(ctx, "DELETE", resourceURL, params.Encode(), payload) - if err != nil { - return nil, fmt.Errorf("got error %s", err.Error()) - } - - return resp, nil -} diff --git a/cmd/get.go b/cmd/get.go index d39d9e8..73e50fc 100644 --- a/cmd/get.go +++ b/cmd/get.go @@ -9,13 +9,9 @@ import ( "github.com/elasticpath/epcc-cli/external/httpclient" "github.com/elasticpath/epcc-cli/external/json" "github.com/elasticpath/epcc-cli/external/resources" - "github.com/elasticpath/epcc-cli/external/shutdown" + "github.com/elasticpath/epcc-cli/external/rest" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" - "io" - "net/http" - "net/url" - "strings" "time" ) @@ -149,7 +145,7 @@ func NewGetCommand(parentCmd *cobra.Command) func() { retriesFailedError := fmt.Errorf("Maximum number of retries hit %d and condition [%s] always true", retryWhileJQMaxAttempts, retryWhileJQ) for attempt := uint16(0); attempt < retryWhileJQMaxAttempts; attempt++ { - body, err = getInternal(context.Background(), overrides, append([]string{resourceName}, args...), skipAliases) + body, err = rest.GetInternal(context.Background(), overrides, append([]string{resourceName}, args...), skipAliases) if retryWhileJQ == "" { retriesFailedError = nil break @@ -305,117 +301,3 @@ func NewGetCommand(parentCmd *cobra.Command) func() { return resetFunc } - -func getInternal(ctx context.Context, overrides *httpclient.HttpParameterOverrides, args []string, skipAliases bool) (string, error) { - resp, err := getResource(ctx, overrides, args) - - if err != nil { - return "", err - } else if resp == nil { - return "", fmt.Errorf("got nil response") - } - - if resp.Body != nil { - defer resp.Body.Close() - - // Print the body - body, err := io.ReadAll(resp.Body) - if err != nil { - log.Fatal(err) - } - - // Check if error response - if resp.StatusCode >= 400 && resp.StatusCode <= 600 { - json.PrintJson(string(body)) - return "", fmt.Errorf(resp.Status) - } - - if !skipAliases { - aliases.SaveAliasesForResources(string(body)) - } - - return string(body), nil - } else { - return "", nil - } -} - -func getUrl(resource resources.Resource, args []string) (*resources.CrudEntityInfo, error) { - - if resource.GetCollectionInfo == nil && resource.GetEntityInfo == nil { - return nil, fmt.Errorf("resource %s doesn't support GET", args[0]) - } else if resource.GetCollectionInfo != nil && resource.GetEntityInfo == nil { - return resource.GetCollectionInfo, nil - } else if resource.GetCollectionInfo == nil && resource.GetEntityInfo != nil { - return resource.GetEntityInfo, nil - } else { - if _, ok := resources.GetPluralResources()[args[0]]; ok { - return resource.GetCollectionInfo, nil - } else { - return resource.GetEntityInfo, nil - } - } -} - -func getResource(ctx context.Context, overrides *httpclient.HttpParameterOverrides, args []string) (*http.Response, error) { - shutdown.OutstandingOpCounter.Add(1) - defer shutdown.OutstandingOpCounter.Done() - - // Find Resource - resource, ok := resources.GetResourceByName(args[0]) - if !ok { - return nil, fmt.Errorf("could not find resource %s", args[0]) - } - - var idCount int - - resourceUrlInfo, err2 := getUrl(resource, args) - if err2 != nil { - return nil, err2 - } - - idCount, err := resources.GetNumberOfVariablesNeeded(resourceUrlInfo.Url) - - if err != nil { - return nil, err - } - - // Replace ids with args in resourceURL - resourceURL, err := resources.GenerateUrl(resourceUrlInfo, args[1:], true) - - if err != nil { - return nil, err - } - - if overrides.OverrideUrlPath != "" { - log.Warnf("Overriding URL Path from %s to %s", resourceURL, overrides.OverrideUrlPath) - resourceURL = overrides.OverrideUrlPath - } - - // Add remaining args as query params - params := url.Values{} - for i := idCount + 1; i+1 < len(args); i = i + 2 { - params.Add(args[i], args[i+1]) - } - - if (idCount-len(args)+1)%2 != 0 { - log.Warnf("Extra argument at the end of the command %s", args[len(args)-1]) - } - - for _, v := range overrides.QueryParameters { - keyAndValue := strings.SplitN(v, "=", 2) - if len(keyAndValue) != 2 { - return nil, fmt.Errorf("Could not parse query parameter %v, all query parameters should be a key and value format", keyAndValue) - } - params.Add(keyAndValue[0], keyAndValue[1]) - } - - // Submit request - resp, err := httpclient.DoRequest(ctx, "GET", resourceURL, params.Encode(), nil) - - if err != nil { - return nil, fmt.Errorf("got error %s", err.Error()) - } - - return resp, nil -} diff --git a/cmd/login.go b/cmd/login.go index 73e3355..a969d1f 100644 --- a/cmd/login.go +++ b/cmd/login.go @@ -13,6 +13,7 @@ import ( "github.com/elasticpath/epcc-cli/external/httpclient" "github.com/elasticpath/epcc-cli/external/json" "github.com/elasticpath/epcc-cli/external/resources" + "github.com/elasticpath/epcc-cli/external/rest" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "net/url" @@ -330,7 +331,7 @@ var loginCustomer = &cobra.Command{ newArgs = append(newArgs, "customer-token") newArgs = append(newArgs, args...) - body, err := createInternal(ctx, overrides, newArgs, false, "", false) + body, err := rest.CreateInternal(ctx, overrides, newArgs, false, "", false) if err != nil { log.Warnf("Login not completed successfully") @@ -360,7 +361,7 @@ var loginCustomer = &cobra.Command{ if customerTokenResponse != nil { // Get the customer so we have aliases where we need the id. - getCustomerBody, err := getInternal(ctx, overrides, []string{"customer", customerTokenResponse.Data.CustomerId}, false) + getCustomerBody, err := rest.GetInternal(ctx, overrides, []string{"customer", customerTokenResponse.Data.CustomerId}, false) if err != nil { log.Warnf("Could not retrieve customer") @@ -460,7 +461,7 @@ var loginAccountManagement = &cobra.Command{ } // Populate an alias to get the authentication_realm. - _, err := getInternal(ctx, overrides, []string{"account-authentication-settings"}, false) + _, err := rest.GetInternal(ctx, overrides, []string{"account-authentication-settings"}, false) if err != nil { return fmt.Errorf("couldn't determine authentication realm: %w", err) @@ -485,7 +486,7 @@ var loginAccountManagement = &cobra.Command{ // Try and auto-detect the password profile id if passwordAuthentication { - resp, err := getInternal(ctx, overrides, []string{"password-profiles", "related_authentication_realm_for_account_authentication_settings_last_read=entity"}, false) + resp, err := rest.GetInternal(ctx, overrides, []string{"password-profiles", "related_authentication_realm_for_account_authentication_settings_last_read=entity"}, false) if err != nil { return fmt.Errorf("couldn't determine password profile: %w", err) @@ -540,7 +541,7 @@ var loginAccountManagement = &cobra.Command{ } // Do the login and get back a list of accounts - body, err := createInternal(ctx, overrides, loginArgs, false, "", false) + body, err := rest.CreateInternal(ctx, overrides, loginArgs, false, "", false) if err != nil { log.Warnf("Login not completed successfully") @@ -600,7 +601,7 @@ var loginAccountManagement = &cobra.Command{ authentication.SaveAccountManagementAuthenticationToken(*selectedAccount) - accountMembers, err := getInternal(ctx, overrides, []string{"account-members"}, false) + accountMembers, err := rest.GetInternal(ctx, overrides, []string{"account-members"}, false) if err == nil { accountMemberId, _ := json.RunJQOnString(".data[0].id", accountMembers) diff --git a/cmd/oidc.go b/cmd/oidc.go index 7d08324..7298080 100644 --- a/cmd/oidc.go +++ b/cmd/oidc.go @@ -12,6 +12,7 @@ import ( "github.com/elasticpath/epcc-cli/external/httpclient" "github.com/elasticpath/epcc-cli/external/json" "github.com/elasticpath/epcc-cli/external/oidc" + "github.com/elasticpath/epcc-cli/external/rest" "github.com/google/uuid" "github.com/mitchellh/mapstructure" log "github.com/sirupsen/logrus" @@ -38,7 +39,7 @@ func StartOIDCServer(port int) error { } // Get customer and account authentication settings to populate the aliases - customerAuthSettings, err := getInternal(ctx, overrides, []string{"customer-authentication-settings"}, false) + customerAuthSettings, err := rest.GetInternal(ctx, overrides, []string{"customer-authentication-settings"}, false) if err != nil { log.Errorf("Could not retrieve customer authentication settings") @@ -47,7 +48,7 @@ func StartOIDCServer(port int) error { } - accountAuthSettings, err := getInternal(ctx, overrides, []string{"account-authentication-settings"}, false) + accountAuthSettings, err := rest.GetInternal(ctx, overrides, []string{"account-authentication-settings"}, false) if err != nil { log.Errorf("Could not retrieve account authentication settings") @@ -120,14 +121,15 @@ func StartOIDCServer(port int) error { } profiles := LoginPageInfo{ - CustomerProfiles: customerProfiles, - CustomerClientId: customerClientId, - AccountProfiles: accountProfiles, - AccountClientId: accountClientId, - State: uuid.New().String(), - RedirectUri: fmt.Sprintf("%s%d%s", "http%3A%2F%2Flocalhost%3A", port, "%2Fcallback"), - CodeVerifier: verifier, - CodeChallenge: challenge, + CustomerProfiles: customerProfiles, + CustomerClientId: customerClientId, + AccountProfiles: accountProfiles, + AccountClientId: accountClientId, + State: uuid.New().String(), + RedirectUriEncoded: fmt.Sprintf("%s%d%s", "http%3A%2F%2Flocalhost%3A", port, "/callback"), + RedirectUriUnencoded: fmt.Sprintf("%s%d%s", "http://localhost:", port, "/callback"), + CodeVerifier: verifier, + CodeChallenge: challenge, } if err != nil { @@ -181,7 +183,7 @@ func StartOIDCServer(port int) error { return } - result, err := createInternal(context.Background(), &httpclient.HttpParameterOverrides{}, []string{"account-management-authentication-token", + result, err := rest.CreateInternal(context.Background(), &httpclient.HttpParameterOverrides{}, []string{"account-management-authentication-token", "authentication_mechanism", "oidc", "oauth_authorization_code", data["code"], "oauth_redirect_uri", fmt.Sprintf("http://localhost:%d/callback", port), @@ -315,14 +317,15 @@ func StartOIDCServer(port int) error { } type LoginPageInfo struct { - CustomerProfiles []OidcProfileInfo - CustomerClientId string - AccountProfiles []OidcProfileInfo - AccountClientId string - RedirectUri string - State string - CodeVerifier string - CodeChallenge string + CustomerProfiles []OidcProfileInfo + CustomerClientId string + AccountProfiles []OidcProfileInfo + AccountClientId string + RedirectUriUnencoded string + RedirectUriEncoded string + State string + CodeVerifier string + CodeChallenge string } type SuccessPageInfo struct { @@ -334,16 +337,17 @@ type SuccessPageInfo struct { type OidcProfileInfo struct { Name string `mapstructure:"name"` AuthorizationLink string `mapstructure:"authorization_link"` + Idp string } func getOidcProfilesForRealm(ctx context.Context, overrides *httpclient.HttpParameterOverrides, realmId string) ([]OidcProfileInfo, error) { - res, err := getInternal(ctx, overrides, []string{"oidc-profiles", realmId}, false) + res, err := rest.GetInternal(ctx, overrides, []string{"oidc-profiles", realmId}, false) if err != nil { return nil, err } - resObj, err := json.RunJQOnString(".data | map({name: .name, authorization_link: .links[\"authorization-endpoint\"]})", res) + resObj, err := json.RunJQOnString(".data | map({name: .name, authorization_link: .links[\"authorization-endpoint\"], idp: .meta.issuer})", res) if err != nil { log.Errorf("Couldn't get oidc profile information: %v", err) diff --git a/cmd/reset-store.go b/cmd/reset-store.go index 1cfcecf..cac506e 100644 --- a/cmd/reset-store.go +++ b/cmd/reset-store.go @@ -9,6 +9,7 @@ import ( "github.com/elasticpath/epcc-cli/external/httpclient" "github.com/elasticpath/epcc-cli/external/json" "github.com/elasticpath/epcc-cli/external/resources" + "github.com/elasticpath/epcc-cli/external/rest" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" "io" @@ -70,25 +71,25 @@ var ResetStore = &cobra.Command{ // We would also need locking to go faster. // Get customer and account authentication settings to populate the aliases - _, err = getInternal(ctx, overrides, []string{"customer-authentication-settings"}, false) + _, err = rest.GetInternal(ctx, overrides, []string{"customer-authentication-settings"}, false) if err != nil { errors = append(errors, err.Error()) } - _, err = getInternal(ctx, overrides, []string{"account-authentication-settings"}, false) + _, err = rest.GetInternal(ctx, overrides, []string{"account-authentication-settings"}, false) if err != nil { errors = append(errors, err.Error()) } - _, err = getInternal(ctx, overrides, []string{"merchant-realm-mappings"}, false) + _, err = rest.GetInternal(ctx, overrides, []string{"merchant-realm-mappings"}, false) if err != nil { errors = append(errors, err.Error()) } - _, err = getInternal(ctx, overrides, []string{"authentication-realms"}, false) + _, err = rest.GetInternal(ctx, overrides, []string{"authentication-realms"}, false) if err != nil { errors = append(errors, err.Error()) @@ -200,7 +201,7 @@ func resetResourcesUndeletableResources(ctx context.Context, overrides *httpclie errors := make([]string, 0) for _, resetCmd := range resetCmds { - body, err := updateInternal(ctx, overrides, false, resetCmd) + body, err := rest.UpdateInternal(ctx, overrides, false, resetCmd) if err != nil { errors = append(errors, fmt.Errorf("error resetting %s: %v", resetCmd[0], err).Error()) diff --git a/cmd/update.go b/cmd/update.go index f09e6ca..e617983 100644 --- a/cmd/update.go +++ b/cmd/update.go @@ -9,12 +9,9 @@ import ( "github.com/elasticpath/epcc-cli/external/httpclient" "github.com/elasticpath/epcc-cli/external/json" "github.com/elasticpath/epcc-cli/external/resources" - "github.com/elasticpath/epcc-cli/external/shutdown" + "github.com/elasticpath/epcc-cli/external/rest" log "github.com/sirupsen/logrus" "github.com/spf13/cobra" - "io" - "net/url" - "strings" ) func NewUpdateCommand(parentCmd *cobra.Command) func() { @@ -89,13 +86,13 @@ func NewUpdateCommand(parentCmd *cobra.Command) func() { aliasId := aliases.ResolveAliasValuesOrReturnIdentity(resource.JsonApiType, resource.AlternateJsonApiTypesForAliases, ifAliasDoesNotExist, "id") if aliasId != ifAliasDoesNotExist { - // If the aliasId is different than the request then it does exist. + // If the aliasId is different from the request then it does exist. log.Infof("Alias [%s] does exist (value: %s), not continuing run", ifAliasDoesNotExist, aliasId) return nil } } - body, err := updateInternal(context.Background(), overrides, skipAliases, append([]string{resourceName}, args...)) + body, err := rest.UpdateInternal(context.Background(), overrides, skipAliases, append([]string{resourceName}, args...)) if err != nil { return err @@ -213,88 +210,3 @@ func NewUpdateCommand(parentCmd *cobra.Command) func() { return resetFunc } - -func updateInternal(ctx context.Context, overrides *httpclient.HttpParameterOverrides, skipAliases bool, args []string) (string, error) { - shutdown.OutstandingOpCounter.Add(1) - defer shutdown.OutstandingOpCounter.Done() - - // Find Resource - resource, ok := resources.GetResourceByName(args[0]) - if !ok { - return "", fmt.Errorf("could not find resource %s", args[0]) - } - - if resource.UpdateEntityInfo == nil { - return "", fmt.Errorf("resource %s doesn't support UPDATE", args[0]) - } - - // Count ids in UpdateEntity - resourceUrlInfo := resource.UpdateEntityInfo - idCount, err := resources.GetNumberOfVariablesNeeded(resourceUrlInfo.Url) - if err != nil { - return "", err - } - - // Replace ids with args in resourceURL - resourceURL, err := resources.GenerateUrl(resourceUrlInfo, args[1:], true) - if err != nil { - return "", err - } - - if overrides.OverrideUrlPath != "" { - log.Warnf("Overriding URL Path from %s to %s", resourceURL, overrides.OverrideUrlPath) - resourceURL = overrides.OverrideUrlPath - } - - args = append(args, "type", resource.JsonApiType) - // Create the body from remaining args - body, err := json.ToJson(args[(idCount+1):], resource.NoWrapping, resource.JsonApiFormat == "compliant", resource.Attributes, true) - if err != nil { - return "", err - } - - params := url.Values{} - - for _, v := range overrides.QueryParameters { - keyAndValue := strings.SplitN(v, "=", 2) - if len(keyAndValue) != 2 { - return "", fmt.Errorf("Could not parse query parameter %v, all query parameters should be a key and value format", keyAndValue) - } - params.Add(keyAndValue[0], keyAndValue[1]) - } - - // Submit request - resp, err := httpclient.DoRequest(ctx, "PUT", resourceURL, params.Encode(), strings.NewReader(body)) - if err != nil { - return "", fmt.Errorf("got error %s", err.Error()) - } else if resp == nil { - return "", fmt.Errorf("got nil response") - } - - if resp.Body != nil { - defer resp.Body.Close() - - // Print the body - resBody, err := io.ReadAll(resp.Body) - if err != nil { - log.Fatal(err) - } - - // Check if error response - if resp.StatusCode >= 400 && resp.StatusCode <= 600 { - json.PrintJson(string(resBody)) - return "", fmt.Errorf(resp.Status) - } - - // 204 is no content, so we will skip it. - if resp.StatusCode != 204 { - if !skipAliases { - aliases.SaveAliasesForResources(string(resBody)) - } - } - - return string(resBody), nil - } else { - return "", nil - } -} diff --git a/external/oidc/done.gohtml b/external/oidc/done.gohtml index d46f233..b399795 100644 --- a/external/oidc/done.gohtml +++ b/external/oidc/done.gohtml @@ -3,25 +3,22 @@ - OIDC Signin Complete - - + EPCC CLI OIDC Tester + -
-

OIDC Login Complete

+
+

EPCC CLI OIDC Tester

-

You are now logged in as Account {{ .AccountName }} ({{ .AccountId }})

- Click here to close this window + You have successfully logged in as: +
+
+ Account Name: {{ .AccountName }}
+ Account ID: {{ .AccountId }}
+ + +

You may now close this window.

\ No newline at end of file diff --git a/external/oidc/error.gohtml b/external/oidc/error.gohtml index 440ef69..1c1de49 100644 --- a/external/oidc/error.gohtml +++ b/external/oidc/error.gohtml @@ -4,10 +4,10 @@ OIDC Error - + -
+

OIDC Test Failed

diff --git a/external/oidc/index.gohtml b/external/oidc/index.gohtml index 89eaab3..52b7375 100644 --- a/external/oidc/index.gohtml +++ b/external/oidc/index.gohtml @@ -21,13 +21,13 @@ const textarea = document.getElementById(id); if (textarea) { const url = textarea.value.trim(); // Get the URL value from the textarea - if (url) { + if (url) { window.location.href = url; // Redirect to the URL } else { alert("Please enter a valid URL."); } } else { - console.error(`No textarea found with ID: ${id}`); + console.error(`No teuuuonteuheuohxtarea found with ID: ${id}`); } } @@ -38,22 +38,59 @@

OIDC Tester

-

Account Management OIDC Profiles

+

Welcome

+ This utility allows you to test Single Sign-On (SSO) with Elastic Path Commerce Cloud. + + You can authenticate two ways: + + + +

Account Management

+ + In order to successfully authenticate with Account Management following the HOWTO Guide, you will need to + make the following changes to the store: +
    +
  1. Add {{ $.RedirectUriUnencoded }} to the Account Management Authentication Realm allowed redirect URIs. + + +
    +
    + epcc get account-authentication-settings
    + epcc get authentication-realm related_authentication_realm_for_account_authentication_settings_last_read=entity
    +
    +
    + After inspecting the list of redirect_uris, you can add a new one with the following syntax: + +
    +
    + epcc update authentication-realm related_authentication_realm_for_account_authentication_settings_last_read=entity redirect_uris[0] {{ $.RedirectUriUnencoded }}
    +
    +
    +
  2. +
  3. Add an OpenID Connect Profile that connects to the Identity Provider: + +
    +
    + epcc create oidc-profile related_authentication_realm_for_account_authentication_settings_last_read=entity name "EPCC CLI Test OIDC Profile" client_id my_client_id client_secret my_client_secret discovery_url my_discovery_url +
    +
    +
    +
  4. Refresh this page, you should then see each OpenID Connect Profile and can authenticate, by clicking the Login button after perhaps editing the URL. The URL will contain all the necessarily arguments.
  5. +
+

OpenID Connect Profiles

{{ range $i, $item := .AccountProfiles }} -

{{ $item.Name }}

-
+

Profile: {{ $item.Name }} ( {{ $item.Idp }} )

+

- {{ end }} - To add a new redirect URI, use: - - epcc update - - -

Customer OIDC Profiles

+

Customers

+

OpenID Connect Profiles

{{ range $i, $item := .CustomerProfiles }}

{{ $item.Name }}

diff --git a/external/oidc/success.gohtml b/external/oidc/success.gohtml index 4ac4605..3e71ae9 100644 --- a/external/oidc/success.gohtml +++ b/external/oidc/success.gohtml @@ -7,16 +7,35 @@ -
-

OIDC Test Success

+
+

OIDC Tester

{{ if eq .LoginType "AM" }} -

Please Select An Account

+

Authentication Successful

+ + To finish the authentication process, you must select an account. If you see no accounts make sure that auto_create_account_for_account_members is enabled or manually create an account. + +
+ epcc update account-authentication-setting auto_create_account_for_account_members true +
+
+

Please Select An Account

{{ range $i, $item := .AccountTokenResponse.Data }} {{ $item.AccountName }} ({{ $item.AccountId }}) {{ end }} + + + {{ end }} + +
+
+
+
+
+
+ To return to start Click here.
\ No newline at end of file diff --git a/external/rest/create.go b/external/rest/create.go new file mode 100644 index 0000000..576b65a --- /dev/null +++ b/external/rest/create.go @@ -0,0 +1,141 @@ +package rest + +import ( + "context" + "fmt" + "github.com/elasticpath/epcc-cli/external/aliases" + "github.com/elasticpath/epcc-cli/external/autofill" + "github.com/elasticpath/epcc-cli/external/encoding" + "github.com/elasticpath/epcc-cli/external/httpclient" + "github.com/elasticpath/epcc-cli/external/json" + "github.com/elasticpath/epcc-cli/external/resources" + "github.com/elasticpath/epcc-cli/external/shutdown" + log "github.com/sirupsen/logrus" + "io" + "net/http" + "net/url" + "strings" +) + +func CreateInternal(ctx context.Context, overrides *httpclient.HttpParameterOverrides, args []string, autoFillOnCreate bool, aliasName string, skipAliases bool) (string, error) { + shutdown.OutstandingOpCounter.Add(1) + defer shutdown.OutstandingOpCounter.Done() + + // Find Resource + resource, ok := resources.GetResourceByName(args[0]) + if !ok { + return "", fmt.Errorf("could not find resource %s", args[0]) + } + + if resource.CreateEntityInfo == nil { + return "", fmt.Errorf("resource %s doesn't support CREATE", args[0]) + } + + // Count ids in CreateEntity + resourceURL := resource.CreateEntityInfo.Url + + idCount, err := resources.GetNumberOfVariablesNeeded(resourceURL) + + if err != nil { + return "", err + } + + // Replace ids with args in resourceURL + resourceURL, err = resources.GenerateUrl(resource.CreateEntityInfo, args[1:], true) + + if overrides.OverrideUrlPath != "" { + log.Warnf("Overriding URL Path from %s to %s", resourceURL, overrides.OverrideUrlPath) + resourceURL = overrides.OverrideUrlPath + } + + if err != nil { + return "", err + } + + var resp *http.Response = nil + var resBody []byte + + if resource.CreateEntityInfo.ContentType == "multipart/form-data" { + + byteBuf, contentType, err := encoding.ToMultiPartEncoding(args[(idCount+1):], resource.NoWrapping, resource.JsonApiFormat == "complaint", resource.Attributes) + if err != nil { + return "", err + } + + // Submit request + resp, err = httpclient.DoFileRequest(ctx, resourceURL, byteBuf, contentType) + + } else { + // Assume it's application/json + + params := url.Values{} + + for _, v := range overrides.QueryParameters { + keyAndValue := strings.SplitN(v, "=", 2) + if len(keyAndValue) != 2 { + return "", fmt.Errorf("Could not parse query parameter %v, all query parameters should be a key and value format", keyAndValue) + } + params.Add(keyAndValue[0], keyAndValue[1]) + } + + if !resource.NoWrapping { + args = append(args, "type", resource.JsonApiType) + } + // Create the body from remaining args + + jsonArgs := args[(idCount + 1):] + if autoFillOnCreate { + autofilledData := autofill.GetJsonArrayForResource(&resource) + + jsonArgs = append(autofilledData, jsonArgs...) + } + + body, err := json.ToJson(jsonArgs, resource.NoWrapping, resource.JsonApiFormat == "compliant", resource.Attributes, true) + + if err != nil { + return "", err + } + + // Submit request + resp, err = httpclient.DoRequest(ctx, "POST", resourceURL, params.Encode(), strings.NewReader(body)) + + } + + if err != nil { + return "", fmt.Errorf("got error %s", err.Error()) + } else if resp == nil { + return "", fmt.Errorf("got nil response with request: %s", resourceURL) + } + + if resp.Body != nil { + defer resp.Body.Close() + + // Print the body + resBody, err = io.ReadAll(resp.Body) + if err != nil { + log.Fatal(err) + } + + // Check if error response + if resp.StatusCode >= 400 && resp.StatusCode <= 600 { + json.PrintJson(string(resBody)) + return "", fmt.Errorf(resp.Status) + } + + // 204 is no content, so we will skip it. + if resp.StatusCode != 204 { + if !skipAliases { + aliases.SaveAliasesForResources(string(resBody)) + } + } + + if aliasName != "" { + aliases.SetAliasForResource(string(resBody), aliasName) + } + + return string(resBody), nil + } else { + return "", nil + } + +} diff --git a/external/rest/delete.go b/external/rest/delete.go new file mode 100644 index 0000000..07fedbb --- /dev/null +++ b/external/rest/delete.go @@ -0,0 +1,125 @@ +package rest + +import ( + "context" + "fmt" + "github.com/elasticpath/epcc-cli/external/aliases" + "github.com/elasticpath/epcc-cli/external/httpclient" + "github.com/elasticpath/epcc-cli/external/json" + "github.com/elasticpath/epcc-cli/external/resources" + "github.com/elasticpath/epcc-cli/external/shutdown" + log "github.com/sirupsen/logrus" + "io" + "net/http" + "net/url" + "strings" +) + +func DeleteResource(ctx context.Context, overrides *httpclient.HttpParameterOverrides, args []string) (*http.Response, error) { + // Find Resource + resource, ok := resources.GetResourceByName(args[0]) + if !ok { + return nil, fmt.Errorf("could not find resource %s", args[0]) + } + + if resource.DeleteEntityInfo == nil { + return nil, fmt.Errorf("resource %s doesn't support DELETE", args[0]) + } + + // Replace ids with args in resourceURL + resourceURL, err := resources.GenerateUrl(resource.DeleteEntityInfo, args[1:], true) + + if err != nil { + return nil, err + } + + if overrides.OverrideUrlPath != "" { + log.Warnf("Overriding URL Path from %s to %s", resourceURL, overrides.OverrideUrlPath) + resourceURL = overrides.OverrideUrlPath + } + + params := url.Values{} + + for _, v := range overrides.QueryParameters { + keyAndValue := strings.SplitN(v, "=", 2) + if len(keyAndValue) != 2 { + return nil, fmt.Errorf("Could not parse query parameter %v, all query parameters should be a key and value format", keyAndValue) + } + params.Add(keyAndValue[0], keyAndValue[1]) + } + + idCount, err := resources.GetNumberOfVariablesNeeded(resource.DeleteEntityInfo.Url) + + if !resource.NoWrapping { + args = append(args, "type", resource.JsonApiType) + } + // Create the body from remaining args + + jsonArgs := args[(idCount + 1):] + + var payload io.Reader = nil + if len(jsonArgs) > 0 { + body, err := json.ToJson(jsonArgs, resource.NoWrapping, resource.JsonApiFormat == "compliant", resource.Attributes, true) + + if err != nil { + return nil, err + } + + payload = strings.NewReader(body) + } + + // Submit request + resp, err := httpclient.DoRequest(ctx, "DELETE", resourceURL, params.Encode(), payload) + if err != nil { + return nil, fmt.Errorf("got error %s", err.Error()) + } + + return resp, nil +} + +func DeleteInternal(ctx context.Context, overrides *httpclient.HttpParameterOverrides, allow404 bool, args []string) (string, error) { + shutdown.OutstandingOpCounter.Add(1) + defer shutdown.OutstandingOpCounter.Done() + + resource, ok := resources.GetResourceByName(args[0]) + if !ok { + return "", fmt.Errorf("could not find resource %s", args[0]) + } + + resp, err := DeleteResource(ctx, overrides, args) + if err != nil { + return "", err + } + + if resp == nil { + return "", fmt.Errorf("got nil response") + } + + if resp.StatusCode < 400 { + idToDelete := aliases.ResolveAliasValuesOrReturnIdentity(resource.JsonApiType, resource.AlternateJsonApiTypesForAliases, args[len(args)-1], "id") + aliases.DeleteAliasesById(idToDelete, resource.JsonApiType) + } + if resp.Body != nil { + + defer resp.Body.Close() + + // Print the body + body, err := io.ReadAll(resp.Body) + + if err != nil { + log.Fatal(err) + } + + // Check if error response + if resp.StatusCode >= 400 && resp.StatusCode <= 600 { + if resp.StatusCode != 404 || !allow404 { + return string(body), fmt.Errorf(resp.Status) + } + } + + return string(body), nil + } else { + return "", nil + } + +} diff --git a/external/rest/get.go b/external/rest/get.go new file mode 100644 index 0000000..48a20c5 --- /dev/null +++ b/external/rest/get.go @@ -0,0 +1,130 @@ +package rest + +import ( + "context" + "fmt" + "github.com/elasticpath/epcc-cli/external/aliases" + "github.com/elasticpath/epcc-cli/external/httpclient" + "github.com/elasticpath/epcc-cli/external/json" + "github.com/elasticpath/epcc-cli/external/resources" + "github.com/elasticpath/epcc-cli/external/shutdown" + log "github.com/sirupsen/logrus" + "io" + "net/http" + "net/url" + "strings" +) + +func GetInternal(ctx context.Context, overrides *httpclient.HttpParameterOverrides, args []string, skipAliases bool) (string, error) { + resp, err := GetResource(ctx, overrides, args) + + if err != nil { + return "", err + } else if resp == nil { + return "", fmt.Errorf("got nil response") + } + + if resp.Body != nil { + defer resp.Body.Close() + + // Print the body + body, err := io.ReadAll(resp.Body) + if err != nil { + log.Fatal(err) + } + + // Check if error response + if resp.StatusCode >= 400 && resp.StatusCode <= 600 { + json.PrintJson(string(body)) + return "", fmt.Errorf(resp.Status) + } + + if !skipAliases { + aliases.SaveAliasesForResources(string(body)) + } + + return string(body), nil + } else { + return "", nil + } +} + +func GetUrl(resource resources.Resource, args []string) (*resources.CrudEntityInfo, error) { + + if resource.GetCollectionInfo == nil && resource.GetEntityInfo == nil { + return nil, fmt.Errorf("resource %s doesn't support GET", args[0]) + } else if resource.GetCollectionInfo != nil && resource.GetEntityInfo == nil { + return resource.GetCollectionInfo, nil + } else if resource.GetCollectionInfo == nil && resource.GetEntityInfo != nil { + return resource.GetEntityInfo, nil + } else { + if _, ok := resources.GetPluralResources()[args[0]]; ok { + return resource.GetCollectionInfo, nil + } else { + return resource.GetEntityInfo, nil + } + } +} + +func GetResource(ctx context.Context, overrides *httpclient.HttpParameterOverrides, args []string) (*http.Response, error) { + shutdown.OutstandingOpCounter.Add(1) + defer shutdown.OutstandingOpCounter.Done() + + // Find Resource + resource, ok := resources.GetResourceByName(args[0]) + if !ok { + return nil, fmt.Errorf("could not find resource %s", args[0]) + } + + var idCount int + + resourceUrlInfo, err2 := GetUrl(resource, args) + if err2 != nil { + return nil, err2 + } + + idCount, err := resources.GetNumberOfVariablesNeeded(resourceUrlInfo.Url) + + if err != nil { + return nil, err + } + + // Replace ids with args in resourceURL + resourceURL, err := resources.GenerateUrl(resourceUrlInfo, args[1:], true) + + if err != nil { + return nil, err + } + + if overrides.OverrideUrlPath != "" { + log.Warnf("Overriding URL Path from %s to %s", resourceURL, overrides.OverrideUrlPath) + resourceURL = overrides.OverrideUrlPath + } + + // Add remaining args as query params + params := url.Values{} + for i := idCount + 1; i+1 < len(args); i = i + 2 { + params.Add(args[i], args[i+1]) + } + + if (idCount-len(args)+1)%2 != 0 { + log.Warnf("Extra argument at the end of the command %s", args[len(args)-1]) + } + + for _, v := range overrides.QueryParameters { + keyAndValue := strings.SplitN(v, "=", 2) + if len(keyAndValue) != 2 { + return nil, fmt.Errorf("Could not parse query parameter %v, all query parameters should be a key and value format", keyAndValue) + } + params.Add(keyAndValue[0], keyAndValue[1]) + } + + // Submit request + resp, err := httpclient.DoRequest(ctx, "GET", resourceURL, params.Encode(), nil) + + if err != nil { + return nil, fmt.Errorf("got error %s", err.Error()) + } + + return resp, nil +} diff --git a/external/rest/update.go b/external/rest/update.go new file mode 100644 index 0000000..a8e83bb --- /dev/null +++ b/external/rest/update.go @@ -0,0 +1,100 @@ +package rest + +import ( + "context" + "fmt" + "github.com/elasticpath/epcc-cli/external/aliases" + "github.com/elasticpath/epcc-cli/external/httpclient" + "github.com/elasticpath/epcc-cli/external/json" + "github.com/elasticpath/epcc-cli/external/resources" + "github.com/elasticpath/epcc-cli/external/shutdown" + log "github.com/sirupsen/logrus" + "io" + "net/url" + "strings" +) + +func UpdateInternal(ctx context.Context, overrides *httpclient.HttpParameterOverrides, skipAliases bool, args []string) (string, error) { + shutdown.OutstandingOpCounter.Add(1) + defer shutdown.OutstandingOpCounter.Done() + + // Find Resource + resource, ok := resources.GetResourceByName(args[0]) + if !ok { + return "", fmt.Errorf("could not find resource %s", args[0]) + } + + if resource.UpdateEntityInfo == nil { + return "", fmt.Errorf("resource %s doesn't support UPDATE", args[0]) + } + + // Count ids in UpdateEntity + resourceUrlInfo := resource.UpdateEntityInfo + idCount, err := resources.GetNumberOfVariablesNeeded(resourceUrlInfo.Url) + if err != nil { + return "", err + } + + // Replace ids with args in resourceURL + resourceURL, err := resources.GenerateUrl(resourceUrlInfo, args[1:], true) + if err != nil { + return "", err + } + + if overrides.OverrideUrlPath != "" { + log.Warnf("Overriding URL Path from %s to %s", resourceURL, overrides.OverrideUrlPath) + resourceURL = overrides.OverrideUrlPath + } + + args = append(args, "type", resource.JsonApiType) + // Create the body from remaining args + body, err := json.ToJson(args[(idCount+1):], resource.NoWrapping, resource.JsonApiFormat == "compliant", resource.Attributes, true) + if err != nil { + return "", err + } + + params := url.Values{} + + for _, v := range overrides.QueryParameters { + keyAndValue := strings.SplitN(v, "=", 2) + if len(keyAndValue) != 2 { + return "", fmt.Errorf("Could not parse query parameter %v, all query parameters should be a key and value format", keyAndValue) + } + params.Add(keyAndValue[0], keyAndValue[1]) + } + + // Submit request + resp, err := httpclient.DoRequest(ctx, "PUT", resourceURL, params.Encode(), strings.NewReader(body)) + if err != nil { + return "", fmt.Errorf("got error %s", err.Error()) + } else if resp == nil { + return "", fmt.Errorf("got nil response") + } + + if resp.Body != nil { + defer resp.Body.Close() + + // Print the body + resBody, err := io.ReadAll(resp.Body) + if err != nil { + log.Fatal(err) + } + + // Check if error response + if resp.StatusCode >= 400 && resp.StatusCode <= 600 { + json.PrintJson(string(resBody)) + return "", fmt.Errorf(resp.Status) + } + + // 204 is no content, so we will skip it. + if resp.StatusCode != 204 { + if !skipAliases { + aliases.SaveAliasesForResources(string(resBody)) + } + } + + return string(resBody), nil + } else { + return "", nil + } +}