From 9a1ae0d91d95499b3cb696313785a299798b6a23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Go=CC=88kc=CC=A7e=20Go=CC=88k=20Klingel?= Date: Mon, 19 Aug 2024 11:40:21 +0200 Subject: [PATCH 01/10] implement allowed-url-domain config option --- docs/stackit_config_set.md | 1 + docs/stackit_config_unset.md | 1 + internal/cmd/config/set/set.go | 4 +++ internal/cmd/config/unset/unset.go | 7 ++++ internal/cmd/config/unset/unset_test.go | 13 ++++++++ internal/cmd/curl/curl.go | 2 +- internal/pkg/auth/utils.go | 2 +- internal/pkg/config/config.go | 5 +++ internal/pkg/utils/utils.go | 19 +++++++++-- internal/pkg/utils/utils_test.go | 43 ++++++++++++++++++------- 10 files changed, 81 insertions(+), 16 deletions(-) diff --git a/docs/stackit_config_set.md b/docs/stackit_config_set.md index be39928f8..3dc522165 100644 --- a/docs/stackit_config_set.md +++ b/docs/stackit_config_set.md @@ -29,6 +29,7 @@ stackit config set [flags] ### Options ``` + --allowed-url-domain string Domain name, used for the verification of the URLs that are given in the IDP endpoint and curl commands --argus-custom-endpoint string Argus API base URL, used in calls to this API --authorization-custom-endpoint string Authorization API base URL, used in calls to this API --dns-custom-endpoint string DNS API base URL, used in calls to this API diff --git a/docs/stackit_config_unset.md b/docs/stackit_config_unset.md index 11c9c459a..c7bdfad22 100644 --- a/docs/stackit_config_unset.md +++ b/docs/stackit_config_unset.md @@ -26,6 +26,7 @@ stackit config unset [flags] ### Options ``` + --allowed-url-domain Domain name, used for the verification of the URLs that are given in the IDP endpoint and curl commands --argus-custom-endpoint Argus API base URL. If unset, uses the default base URL --async Configuration option to run commands asynchronously --authorization-custom-endpoint Authorization API base URL. If unset, uses the default base URL diff --git a/internal/cmd/config/set/set.go b/internal/cmd/config/set/set.go index e44372455..fdba579d2 100644 --- a/internal/cmd/config/set/set.go +++ b/internal/cmd/config/set/set.go @@ -20,6 +20,7 @@ const ( sessionTimeLimitFlag = "session-time-limit" identityProviderCustomEndpointFlag = "identity-provider-custom-endpoint" identityProviderCustomClientIdFlag = "identity-provider-custom-client-id" + allowedUrlDomainFlag = "allowed-url-domain" argusCustomEndpointFlag = "argus-custom-endpoint" authorizationCustomEndpointFlag = "authorization-custom-endpoint" @@ -131,6 +132,7 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().String(sessionTimeLimitFlag, "", "Maximum time before authentication is required again. After this time, you will be prompted to login again to execute commands that require authentication. Can't be larger than 24h. Requires authentication after being set to take effect. Examples: 3h, 5h30m40s (BETA: currently values greater than 2h have no effect)") cmd.Flags().String(identityProviderCustomEndpointFlag, "", "Identity Provider base URL, used for user authentication") cmd.Flags().String(identityProviderCustomClientIdFlag, "", "Identity Provider client ID, used for user authentication") + cmd.Flags().String(allowedUrlDomainFlag, "", "Domain name, used for the verification of the URLs that are given in the IDP endpoint and curl commands") cmd.Flags().String(argusCustomEndpointFlag, "", "Argus API base URL, used in calls to this API") cmd.Flags().String(authorizationCustomEndpointFlag, "", "Authorization API base URL, used in calls to this API") cmd.Flags().String(dnsCustomEndpointFlag, "", "DNS API base URL, used in calls to this API") @@ -159,6 +161,8 @@ func configureFlags(cmd *cobra.Command) { cobra.CheckErr(err) err = viper.BindPFlag(config.IdentityProviderCustomClientIdKey, cmd.Flags().Lookup(identityProviderCustomClientIdFlag)) cobra.CheckErr(err) + err = viper.BindPFlag(config.AllowedUrlDomainKey, cmd.Flags().Lookup(allowedUrlDomainFlag)) + cobra.CheckErr(err) err = viper.BindPFlag(config.ArgusCustomEndpointKey, cmd.Flags().Lookup(argusCustomEndpointFlag)) cobra.CheckErr(err) diff --git a/internal/cmd/config/unset/unset.go b/internal/cmd/config/unset/unset.go index a879dd5b0..afd4bc363 100644 --- a/internal/cmd/config/unset/unset.go +++ b/internal/cmd/config/unset/unset.go @@ -23,6 +23,7 @@ const ( sessionTimeLimitFlag = "session-time-limit" identityProviderCustomEndpointFlag = "identity-provider-custom-endpoint" identityProviderCustomClientIdFlag = "identity-provider-custom-client-id" + allowedUrlDomainFlag = "allowed-url-domain" argusCustomEndpointFlag = "argus-custom-endpoint" authorizationCustomEndpointFlag = "authorization-custom-endpoint" @@ -56,6 +57,7 @@ type inputModel struct { SessionTimeLimit bool IdentityProviderCustomEndpoint bool IdentityProviderCustomClientID bool + AllowedUrlDomain bool ArgusCustomEndpoint bool AuthorizationCustomEndpoint bool @@ -122,6 +124,9 @@ func NewCmd(p *print.Printer) *cobra.Command { if model.IdentityProviderCustomClientID { viper.Set(config.IdentityProviderCustomClientIdKey, "") } + if model.AllowedUrlDomain { + viper.Set(config.AllowedUrlDomainKey, "") + } if model.ArgusCustomEndpoint { viper.Set(config.ArgusCustomEndpointKey, "") @@ -207,6 +212,7 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().Bool(sessionTimeLimitFlag, false, fmt.Sprintf("Maximum time before authentication is required again. If unset, defaults to %s", config.SessionTimeLimitDefault)) cmd.Flags().Bool(identityProviderCustomEndpointFlag, false, "Identity Provider base URL. If unset, uses the default base URL") cmd.Flags().Bool(identityProviderCustomClientIdFlag, false, "Identity Provider client ID, used for user authentication") + cmd.Flags().Bool(allowedUrlDomainFlag, false, "Domain name, used for the verification of the URLs that are given in the IDP endpoint and curl commands") cmd.Flags().Bool(argusCustomEndpointFlag, false, "Argus API base URL. If unset, uses the default base URL") cmd.Flags().Bool(authorizationCustomEndpointFlag, false, "Authorization API base URL. If unset, uses the default base URL") @@ -241,6 +247,7 @@ func parseInput(p *print.Printer, cmd *cobra.Command) *inputModel { SessionTimeLimit: flags.FlagToBoolValue(p, cmd, sessionTimeLimitFlag), IdentityProviderCustomEndpoint: flags.FlagToBoolValue(p, cmd, identityProviderCustomEndpointFlag), IdentityProviderCustomClientID: flags.FlagToBoolValue(p, cmd, identityProviderCustomClientIdFlag), + AllowedUrlDomain: flags.FlagToBoolValue(p, cmd, allowedUrlDomainFlag), ArgusCustomEndpoint: flags.FlagToBoolValue(p, cmd, argusCustomEndpointFlag), AuthorizationCustomEndpoint: flags.FlagToBoolValue(p, cmd, authorizationCustomEndpointFlag), diff --git a/internal/cmd/config/unset/unset_test.go b/internal/cmd/config/unset/unset_test.go index 9f64136f5..fb77c4589 100644 --- a/internal/cmd/config/unset/unset_test.go +++ b/internal/cmd/config/unset/unset_test.go @@ -19,6 +19,7 @@ func fixtureFlagValues(mods ...func(flagValues map[string]bool)) map[string]bool sessionTimeLimitFlag: true, identityProviderCustomEndpointFlag: true, identityProviderCustomClientIdFlag: true, + allowedUrlDomainFlag: true, argusCustomEndpointFlag: true, authorizationCustomEndpointFlag: true, @@ -55,6 +56,7 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { SessionTimeLimit: true, IdentityProviderCustomEndpoint: true, IdentityProviderCustomClientID: true, + AllowedUrlDomain: true, ArgusCustomEndpoint: true, AuthorizationCustomEndpoint: true, @@ -107,6 +109,7 @@ func TestParseInput(t *testing.T) { model.SessionTimeLimit = false model.IdentityProviderCustomEndpoint = false model.IdentityProviderCustomClientID = false + model.AllowedUrlDomain = false model.ArgusCustomEndpoint = false model.AuthorizationCustomEndpoint = false @@ -168,6 +171,16 @@ func TestParseInput(t *testing.T) { model.IdentityProviderCustomClientID = false }), }, + { + description: "allowed url domain empty", + flagValues: fixtureFlagValues(func(flagValues map[string]bool) { + flagValues[allowedUrlDomainFlag] = false + }), + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.AllowedUrlDomain = false + }), + }, { description: "argus custom endpoint empty", flagValues: fixtureFlagValues(func(flagValues map[string]bool) { diff --git a/internal/cmd/curl/curl.go b/internal/cmd/curl/curl.go index 05caa082c..1dc2e0428 100644 --- a/internal/cmd/curl/curl.go +++ b/internal/cmd/curl/curl.go @@ -67,7 +67,7 @@ func NewCmd(p *print.Printer) *cobra.Command { `$ stackit curl https://dns.api.stackit.cloud/v1/projects/xxx/zones -X POST -H "Authorization: Bearer yyy" --fail`, ), ), - Args: args.SingleArg(urlArg, utils.ValidateSTACKITURL), + Args: args.SingleArg(urlArg, utils.ValidateURL), RunE: func(cmd *cobra.Command, args []string) (err error) { model, err := parseInput(p, cmd, args) if err != nil { diff --git a/internal/pkg/auth/utils.go b/internal/pkg/auth/utils.go index a60e25a5c..dbd80f92b 100644 --- a/internal/pkg/auth/utils.go +++ b/internal/pkg/auth/utils.go @@ -15,7 +15,7 @@ func getIDPEndpoint() (string, error) { if customIDPEndpoint != "" { idpEndpoint = customIDPEndpoint } - err := utils.ValidateSTACKITURL(idpEndpoint) + err := utils.ValidateURL(idpEndpoint) if err != nil { return "", fmt.Errorf("validate custom identity provider endpoint: %w", err) } diff --git a/internal/pkg/config/config.go b/internal/pkg/config/config.go index 548827ddb..268b78c38 100644 --- a/internal/pkg/config/config.go +++ b/internal/pkg/config/config.go @@ -19,6 +19,7 @@ const ( IdentityProviderCustomEndpointKey = "identity_provider_custom_endpoint" IdentityProviderCustomClientIdKey = "identity_provider_custom_client_id" + AllowedUrlDomainKey = "allowed_url_domain" ArgusCustomEndpointKey = "argus_custom_endpoint" AuthorizationCustomEndpointKey = "authorization_custom_endpoint" @@ -47,6 +48,8 @@ const ( AsyncDefault = false SessionTimeLimitDefault = "2h" + + AllowedUrlDomainDefault = "stackit.cloud" ) const ( @@ -69,6 +72,7 @@ var ConfigKeys = []string{ IdentityProviderCustomEndpointKey, IdentityProviderCustomClientIdKey, + AllowedUrlDomainKey, DNSCustomEndpointKey, LoadBalancerCustomEndpointKey, @@ -151,6 +155,7 @@ func setConfigDefaults() { viper.SetDefault(SessionTimeLimitKey, SessionTimeLimitDefault) viper.SetDefault(IdentityProviderCustomEndpointKey, "") viper.SetDefault(IdentityProviderCustomClientIdKey, "") + viper.SetDefault(AllowedUrlDomainKey, "") viper.SetDefault(DNSCustomEndpointKey, "") viper.SetDefault(ArgusCustomEndpointKey, "") viper.SetDefault(AuthorizationCustomEndpointKey, "") diff --git a/internal/pkg/utils/utils.go b/internal/pkg/utils/utils.go index c369b92d7..6606718ca 100644 --- a/internal/pkg/utils/utils.go +++ b/internal/pkg/utils/utils.go @@ -7,6 +7,12 @@ import ( "github.com/google/uuid" "github.com/spf13/cobra" + "github.com/spf13/viper" + "github.com/stackitcloud/stackit-cli/internal/pkg/config" +) + +const ( + defaultAllowedUrlDomain = "stackit.cloud" ) // Ptr Returns the pointer to any type T @@ -51,7 +57,7 @@ func ConvertInt64PToFloat64P(i *int64) *float64 { return &f } -func ValidateSTACKITURL(value string) error { +func ValidateURL(value string) error { urlStruct, err := url.Parse(value) if err != nil { return fmt.Errorf("parse url: %w", err) @@ -60,8 +66,15 @@ func ValidateSTACKITURL(value string) error { if urlHost == "" { return fmt.Errorf("bad url") } - if !strings.HasSuffix(urlHost, "stackit.cloud") { - return fmt.Errorf(`only urls belonging to STACKIT are allowed, hostname must end in "stackit.cloud"`) + + allowedUrlDomain := viper.GetString(config.AllowedUrlDomainKey) + + if allowedUrlDomain == "" { + allowedUrlDomain = defaultAllowedUrlDomain + } + + if !strings.HasSuffix(urlHost, allowedUrlDomain) { + return fmt.Errorf(`only urls belonging to domain %s are allowed"`, allowedUrlDomain) } return nil } diff --git a/internal/pkg/utils/utils_test.go b/internal/pkg/utils/utils_test.go index 77f5cffef..0b1ee83e3 100644 --- a/internal/pkg/utils/utils_test.go +++ b/internal/pkg/utils/utils_test.go @@ -2,6 +2,9 @@ package utils import ( "testing" + + "github.com/spf13/viper" + "github.com/stackitcloud/stackit-cli/internal/pkg/config" ) func TestConvertInt64PToFloat64P(t *testing.T) { @@ -46,21 +49,36 @@ func TestConvertInt64PToFloat64P(t *testing.T) { } } -func TestValidateSTACKITURL(t *testing.T) { +func TestValidateURL(t *testing.T) { tests := []struct { - name string - input string - isValid bool + name string + allowedUrlDomain string + isValid bool + input string }{ { - name: "STACKIT URL", - input: "https://example.stackit.cloud", - isValid: true, + name: "STACKIT URL valid", + allowedUrlDomain: "stackit.cloud", + input: "https://example.stackit.cloud", + isValid: true, }, { - name: "non-STACKIT URL", - input: "https://www.very-suspicious-website.com/", - isValid: false, + name: "STACKIT URL invalid", + allowedUrlDomain: "example.com", + input: "https://example.stackit.cloud", + isValid: false, + }, + { + name: "non-STACKIT URL invalid", + allowedUrlDomain: "stackit.cloud", + input: "https://www.very-suspicious-website.com/", + isValid: false, + }, + { + name: "non-STACKIT URL valid", + allowedUrlDomain: "example.com", + input: "https://www.test.example.com/", + isValid: true, }, { name: "invalid URL", @@ -71,7 +89,10 @@ func TestValidateSTACKITURL(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := ValidateSTACKITURL(tt.input) + viper.Reset() + viper.Set(config.AllowedUrlDomainKey, tt.allowedUrlDomain) + + err := ValidateURL(tt.input) if tt.isValid && err != nil { t.Errorf("expected URL to be valid, got error: %v", err) } From 56b3d7506530c764257f225e69f9a43cb8b02933 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Go=CC=88kc=CC=A7e=20Go=CC=88k=20Klingel?= Date: Mon, 19 Aug 2024 16:14:37 +0200 Subject: [PATCH 02/10] implement jwks and token endpoints in cfg command --- docs/stackit_auth_activate-service-account.md | 2 - docs/stackit_config_set.md | 2 + docs/stackit_config_unset.md | 2 + .../activate_service_account.go | 13 +++--- .../activate_service_account_test.go | 45 ++++++++++++------- internal/cmd/config/set/set.go | 8 ++++ internal/cmd/config/unset/unset.go | 8 ++++ internal/cmd/config/unset/unset_test.go | 26 +++++++++++ internal/pkg/config/config.go | 8 +++- 9 files changed, 88 insertions(+), 26 deletions(-) diff --git a/docs/stackit_auth_activate-service-account.md b/docs/stackit_auth_activate-service-account.md index 18feefe3d..e39102530 100644 --- a/docs/stackit_auth_activate-service-account.md +++ b/docs/stackit_auth_activate-service-account.md @@ -29,11 +29,9 @@ stackit auth activate-service-account [flags] ``` -h, --help Help for "stackit auth activate-service-account" - --jwks-custom-endpoint string Custom endpoint for the jwks API, which is used to get the json web key sets (jwks) to validate tokens when the service-account authentication is activated --private-key-path string RSA private key path. It takes precedence over the private key included in the service account key, if present --service-account-key-path string Service account key path --service-account-token string Service account long-lived access token - --token-custom-endpoint string Custom endpoint for the token API, which is used to request access tokens when the service-account authentication is activated ``` ### Options inherited from parent commands diff --git a/docs/stackit_config_set.md b/docs/stackit_config_set.md index 3dc522165..191dfc823 100644 --- a/docs/stackit_config_set.md +++ b/docs/stackit_config_set.md @@ -37,6 +37,7 @@ stackit config set [flags] --iaas-custom-endpoint string IaaS API base URL, used in calls to this API --identity-provider-custom-client-id string Identity Provider client ID, used for user authentication --identity-provider-custom-endpoint string Identity Provider base URL, used for user authentication + --jwks-custom-endpoint string Custom endpoint for the jwks API, which is used to get the json web key sets (jwks) to validate tokens when the service-account authentication is activated --load-balancer-custom-endpoint string Load Balancer API base URL, used in calls to this API --logme-custom-endpoint string LogMe API base URL, used in calls to this API --mariadb-custom-endpoint string MariaDB API base URL, used in calls to this API @@ -55,6 +56,7 @@ stackit config set [flags] --session-time-limit string Maximum time before authentication is required again. After this time, you will be prompted to login again to execute commands that require authentication. Can't be larger than 24h. Requires authentication after being set to take effect. Examples: 3h, 5h30m40s (BETA: currently values greater than 2h have no effect) --ske-custom-endpoint string SKE API base URL, used in calls to this API --sqlserverflex-custom-endpoint string SQLServer Flex API base URL, used in calls to this API + --token-custom-endpoint string Custom endpoint for the token API, which is used to request access tokens when the service-account authentication is activated ``` ### Options inherited from parent commands diff --git a/docs/stackit_config_unset.md b/docs/stackit_config_unset.md index c7bdfad22..70843510d 100644 --- a/docs/stackit_config_unset.md +++ b/docs/stackit_config_unset.md @@ -35,6 +35,7 @@ stackit config unset [flags] --iaas-custom-endpoint IaaS API base URL. If unset, uses the default base URL --identity-provider-custom-client-id Identity Provider client ID, used for user authentication --identity-provider-custom-endpoint Identity Provider base URL. If unset, uses the default base URL + --jwks-custom-endpoint Custom endpoint for the jwks API, which is used to get the json web key sets (jwks) to validate tokens when the service-account authentication is activated --load-balancer-custom-endpoint Load Balancer API base URL. If unset, uses the default base URL --logme-custom-endpoint LogMe API base URL. If unset, uses the default base URL --mariadb-custom-endpoint MariaDB API base URL. If unset, uses the default base URL @@ -55,6 +56,7 @@ stackit config unset [flags] --session-time-limit Maximum time before authentication is required again. If unset, defaults to 2h --ske-custom-endpoint SKE API base URL. If unset, uses the default base URL --sqlserverflex-custom-endpoint SQLServer Flex API base URL. If unset, uses the default base URL + --token-custom-endpoint Custom endpoint for the token API, which is used to request access tokens when the service-account authentication is activated --verbosity Verbosity of the CLI ``` diff --git a/internal/cmd/auth/activate-service-account/activate_service_account.go b/internal/cmd/auth/activate-service-account/activate_service_account.go index c0d806518..37ae10e3e 100644 --- a/internal/cmd/auth/activate-service-account/activate_service_account.go +++ b/internal/cmd/auth/activate-service-account/activate_service_account.go @@ -4,8 +4,10 @@ import ( "errors" "fmt" + "github.com/spf13/viper" "github.com/stackitcloud/stackit-cli/internal/pkg/args" "github.com/stackitcloud/stackit-cli/internal/pkg/auth" + "github.com/stackitcloud/stackit-cli/internal/pkg/config" cliErr "github.com/stackitcloud/stackit-cli/internal/pkg/errors" "github.com/stackitcloud/stackit-cli/internal/pkg/examples" "github.com/stackitcloud/stackit-cli/internal/pkg/flags" @@ -20,8 +22,6 @@ const ( serviceAccountTokenFlag = "service-account-token" serviceAccountKeyPathFlag = "service-account-key-path" privateKeyPathFlag = "private-key-path" - tokenCustomEndpointFlag = "token-custom-endpoint" - jwksCustomEndpointFlag = "jwks-custom-endpoint" ) type inputModel struct { @@ -100,17 +100,18 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().String(serviceAccountTokenFlag, "", "Service account long-lived access token") cmd.Flags().String(serviceAccountKeyPathFlag, "", "Service account key path") cmd.Flags().String(privateKeyPathFlag, "", "RSA private key path. It takes precedence over the private key included in the service account key, if present") - cmd.Flags().String(tokenCustomEndpointFlag, "", "Custom endpoint for the token API, which is used to request access tokens when the service-account authentication is activated") - cmd.Flags().String(jwksCustomEndpointFlag, "", "Custom endpoint for the jwks API, which is used to get the json web key sets (jwks) to validate tokens when the service-account authentication is activated") } func parseInput(p *print.Printer, cmd *cobra.Command) *inputModel { + tokenCustomEndpoint := viper.GetString(config.TokenCustomEndpointKey) + jwksCustomEndpoint := viper.GetString(config.JwksCustomEndpointKey) + model := inputModel{ ServiceAccountToken: flags.FlagToStringValue(p, cmd, serviceAccountTokenFlag), ServiceAccountKeyPath: flags.FlagToStringValue(p, cmd, serviceAccountKeyPathFlag), PrivateKeyPath: flags.FlagToStringValue(p, cmd, privateKeyPathFlag), - TokenCustomEndpoint: flags.FlagToStringValue(p, cmd, tokenCustomEndpointFlag), - JwksCustomEndpoint: flags.FlagToStringValue(p, cmd, jwksCustomEndpointFlag), + TokenCustomEndpoint: tokenCustomEndpoint, + JwksCustomEndpoint: jwksCustomEndpoint, } if p.IsVerbosityDebug() { diff --git a/internal/cmd/auth/activate-service-account/activate_service_account_test.go b/internal/cmd/auth/activate-service-account/activate_service_account_test.go index a9e12b30c..c70601ab0 100644 --- a/internal/cmd/auth/activate-service-account/activate_service_account_test.go +++ b/internal/cmd/auth/activate-service-account/activate_service_account_test.go @@ -3,7 +3,9 @@ package activateserviceaccount import ( "testing" + "github.com/spf13/viper" "github.com/stackitcloud/stackit-cli/internal/pkg/auth" + "github.com/stackitcloud/stackit-cli/internal/pkg/config" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" "github.com/stackitcloud/stackit-cli/internal/pkg/print" "github.com/zalando/go-keyring" @@ -11,13 +13,14 @@ import ( "github.com/google/go-cmp/cmp" ) +var testTokenCustomEndpoint = "token_url" +var testJwksCustomEndpoint = "jwks_url" + func fixtureFlagValues(mods ...func(flagValues map[string]string)) map[string]string { flagValues := map[string]string{ serviceAccountTokenFlag: "token", serviceAccountKeyPathFlag: "sa_key", privateKeyPathFlag: "private_key", - tokenCustomEndpointFlag: "token_url", - jwksCustomEndpointFlag: "jwks_url", } for _, mod := range mods { mod(flagValues) @@ -41,21 +44,27 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { func TestParseInput(t *testing.T) { tests := []struct { - description string - flagValues map[string]string - isValid bool - expectedModel *inputModel + description string + flagValues map[string]string + tokenCustomEndpoint string + jwksCustomEndpoint string + isValid bool + expectedModel *inputModel }{ { - description: "base", - flagValues: fixtureFlagValues(), - isValid: true, - expectedModel: fixtureInputModel(), + description: "base", + flagValues: fixtureFlagValues(), + tokenCustomEndpoint: testTokenCustomEndpoint, + jwksCustomEndpoint: testJwksCustomEndpoint, + isValid: true, + expectedModel: fixtureInputModel(), }, { - description: "no values", - flagValues: map[string]string{}, - isValid: true, + description: "no values", + flagValues: map[string]string{}, + tokenCustomEndpoint: "", + jwksCustomEndpoint: "", + isValid: true, expectedModel: &inputModel{ ServiceAccountToken: "", ServiceAccountKeyPath: "", @@ -70,10 +79,10 @@ func TestParseInput(t *testing.T) { serviceAccountTokenFlag: "", serviceAccountKeyPathFlag: "", privateKeyPathFlag: "", - tokenCustomEndpointFlag: "", - jwksCustomEndpointFlag: "", }, - isValid: true, + tokenCustomEndpoint: "", + jwksCustomEndpoint: "", + isValid: true, expectedModel: &inputModel{ ServiceAccountToken: "", ServiceAccountKeyPath: "", @@ -110,6 +119,10 @@ func TestParseInput(t *testing.T) { } } + viper.Reset() + viper.Set(config.TokenCustomEndpointKey, tt.tokenCustomEndpoint) + viper.Set(config.JwksCustomEndpointKey, tt.jwksCustomEndpoint) + model := parseInput(p, cmd) if !tt.isValid { diff --git a/internal/cmd/config/set/set.go b/internal/cmd/config/set/set.go index fdba579d2..1c6e25ec5 100644 --- a/internal/cmd/config/set/set.go +++ b/internal/cmd/config/set/set.go @@ -43,6 +43,8 @@ const ( skeCustomEndpointFlag = "ske-custom-endpoint" sqlServerFlexCustomEndpointFlag = "sqlserverflex-custom-endpoint" iaasCustomEndpointFlag = "iaas-custom-endpoint" + tokenCustomEndpointFlag = "token-custom-endpoint" + jwksCustomEndpointFlag = "jwks-custom-endpoint" ) type inputModel struct { @@ -154,6 +156,8 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().String(skeCustomEndpointFlag, "", "SKE API base URL, used in calls to this API") cmd.Flags().String(sqlServerFlexCustomEndpointFlag, "", "SQLServer Flex API base URL, used in calls to this API") cmd.Flags().String(iaasCustomEndpointFlag, "", "IaaS API base URL, used in calls to this API") + cmd.Flags().String(tokenCustomEndpointFlag, "", "Custom endpoint for the token API, which is used to request access tokens when the service-account authentication is activated") + cmd.Flags().String(jwksCustomEndpointFlag, "", "Custom endpoint for the jwks API, which is used to get the json web key sets (jwks) to validate tokens when the service-account authentication is activated") err := viper.BindPFlag(config.SessionTimeLimitKey, cmd.Flags().Lookup(sessionTimeLimitFlag)) cobra.CheckErr(err) @@ -206,6 +210,10 @@ func configureFlags(cmd *cobra.Command) { cobra.CheckErr(err) err = viper.BindPFlag(config.IaaSCustomEndpointKey, cmd.Flags().Lookup(iaasCustomEndpointFlag)) cobra.CheckErr(err) + err = viper.BindPFlag(config.TokenCustomEndpointKey, cmd.Flags().Lookup(tokenCustomEndpointFlag)) + cobra.CheckErr(err) + err = viper.BindPFlag(config.JwksCustomEndpointKey, cmd.Flags().Lookup(jwksCustomEndpointFlag)) + cobra.CheckErr(err) } func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { diff --git a/internal/cmd/config/unset/unset.go b/internal/cmd/config/unset/unset.go index afd4bc363..d24186c37 100644 --- a/internal/cmd/config/unset/unset.go +++ b/internal/cmd/config/unset/unset.go @@ -46,6 +46,8 @@ const ( skeCustomEndpointFlag = "ske-custom-endpoint" sqlServerFlexCustomEndpointFlag = "sqlserverflex-custom-endpoint" iaasCustomEndpointFlag = "iaas-custom-endpoint" + tokenCustomEndpointFlag = "token-custom-endpoint" + jwksCustomEndpointFlag = "jwks-custom-endpoint" ) type inputModel struct { @@ -80,6 +82,8 @@ type inputModel struct { SKECustomEndpoint bool SQLServerFlexCustomEndpoint bool IaaSCustomEndpoint bool + TokenCustomEndpoint bool + JwksCustomEndpoint bool } func NewCmd(p *print.Printer) *cobra.Command { @@ -235,6 +239,8 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().Bool(skeCustomEndpointFlag, false, "SKE API base URL. If unset, uses the default base URL") cmd.Flags().Bool(sqlServerFlexCustomEndpointFlag, false, "SQLServer Flex API base URL. If unset, uses the default base URL") cmd.Flags().Bool(iaasCustomEndpointFlag, false, "IaaS API base URL. If unset, uses the default base URL") + cmd.Flags().Bool(tokenCustomEndpointFlag, false, "Custom endpoint for the token API, which is used to request access tokens when the service-account authentication is activated") + cmd.Flags().Bool(jwksCustomEndpointFlag, false, "Custom endpoint for the jwks API, which is used to get the json web key sets (jwks) to validate tokens when the service-account authentication is activated") } func parseInput(p *print.Printer, cmd *cobra.Command) *inputModel { @@ -270,6 +276,8 @@ func parseInput(p *print.Printer, cmd *cobra.Command) *inputModel { SKECustomEndpoint: flags.FlagToBoolValue(p, cmd, skeCustomEndpointFlag), SQLServerFlexCustomEndpoint: flags.FlagToBoolValue(p, cmd, sqlServerFlexCustomEndpointFlag), IaaSCustomEndpoint: flags.FlagToBoolValue(p, cmd, iaasCustomEndpointFlag), + TokenCustomEndpoint: flags.FlagToBoolValue(p, cmd, tokenCustomEndpointFlag), + JwksCustomEndpoint: flags.FlagToBoolValue(p, cmd, jwksCustomEndpointFlag), } if p.IsVerbosityDebug() { diff --git a/internal/cmd/config/unset/unset_test.go b/internal/cmd/config/unset/unset_test.go index fb77c4589..5d33ae3ce 100644 --- a/internal/cmd/config/unset/unset_test.go +++ b/internal/cmd/config/unset/unset_test.go @@ -39,6 +39,8 @@ func fixtureFlagValues(mods ...func(flagValues map[string]bool)) map[string]bool skeCustomEndpointFlag: true, sqlServerFlexCustomEndpointFlag: true, iaasCustomEndpointFlag: true, + tokenCustomEndpointFlag: true, + jwksCustomEndpointFlag: true, } for _, mod := range mods { mod(flagValues) @@ -76,6 +78,8 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { SKECustomEndpoint: true, SQLServerFlexCustomEndpoint: true, IaaSCustomEndpoint: true, + TokenCustomEndpoint: true, + JwksCustomEndpoint: true, } for _, mod := range mods { mod(model) @@ -129,6 +133,8 @@ func TestParseInput(t *testing.T) { model.SKECustomEndpoint = false model.SQLServerFlexCustomEndpoint = false model.IaaSCustomEndpoint = false + model.TokenCustomEndpoint = false + model.JwksCustomEndpoint = false }), }, { @@ -261,6 +267,26 @@ func TestParseInput(t *testing.T) { model.RunCommandCustomEndpoint = false }), }, + { + description: "token custom endpoint empty", + flagValues: fixtureFlagValues(func(flagValues map[string]bool) { + flagValues[tokenCustomEndpointFlag] = false + }), + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.TokenCustomEndpoint = false + }), + }, + { + description: "jwks custom endpoint empty", + flagValues: fixtureFlagValues(func(flagValues map[string]bool) { + flagValues[jwksCustomEndpointFlag] = false + }), + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.JwksCustomEndpoint = false + }), + }, } for _, tt := range tests { t.Run(tt.description, func(t *testing.T) { diff --git a/internal/pkg/config/config.go b/internal/pkg/config/config.go index 268b78c38..02823d109 100644 --- a/internal/pkg/config/config.go +++ b/internal/pkg/config/config.go @@ -42,14 +42,14 @@ const ( SKECustomEndpointKey = "ske_custom_endpoint" SQLServerFlexCustomEndpointKey = "sqlserverflex_custom_endpoint" IaaSCustomEndpointKey = "iaas_custom_endpoint" + TokenCustomEndpointKey = "token_custom_endpoint" + JwksCustomEndpointKey = "jwks_custom_endpoint" ProjectNameKey = "project_name" DefaultProfileName = "default" AsyncDefault = false SessionTimeLimitDefault = "2h" - - AllowedUrlDomainDefault = "stackit.cloud" ) const ( @@ -96,6 +96,8 @@ var ConfigKeys = []string{ SKECustomEndpointKey, SQLServerFlexCustomEndpointKey, IaaSCustomEndpointKey, + TokenCustomEndpointKey, + JwksCustomEndpointKey, } var defaultConfigFolderPath string @@ -172,6 +174,8 @@ func setConfigDefaults() { viper.SetDefault(SKECustomEndpointKey, "") viper.SetDefault(SQLServerFlexCustomEndpointKey, "") viper.SetDefault(IaaSCustomEndpointKey, "") + viper.SetDefault(TokenCustomEndpointKey, "") + viper.SetDefault(JwksCustomEndpointKey, "") } func getConfigFilePath(configFolder string) string { From 61949e5e13692349b19ff1204c5814361fc84864 Mon Sep 17 00:00:00 2001 From: GokceGK <161626272+GokceGK@users.noreply.github.com> Date: Mon, 19 Aug 2024 16:32:42 +0200 Subject: [PATCH 03/10] Update internal/cmd/config/set/set.go MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: João Palet --- internal/cmd/config/set/set.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/cmd/config/set/set.go b/internal/cmd/config/set/set.go index 1c6e25ec5..7aa7ce727 100644 --- a/internal/cmd/config/set/set.go +++ b/internal/cmd/config/set/set.go @@ -134,7 +134,7 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().String(sessionTimeLimitFlag, "", "Maximum time before authentication is required again. After this time, you will be prompted to login again to execute commands that require authentication. Can't be larger than 24h. Requires authentication after being set to take effect. Examples: 3h, 5h30m40s (BETA: currently values greater than 2h have no effect)") cmd.Flags().String(identityProviderCustomEndpointFlag, "", "Identity Provider base URL, used for user authentication") cmd.Flags().String(identityProviderCustomClientIdFlag, "", "Identity Provider client ID, used for user authentication") - cmd.Flags().String(allowedUrlDomainFlag, "", "Domain name, used for the verification of the URLs that are given in the IDP endpoint and curl commands") + cmd.Flags().String(allowedUrlDomainFlag, "", `Domain name, used for the verification of the URLs that are given in the custom identidy provider endpoint and "STACKIT curl" command`) cmd.Flags().String(argusCustomEndpointFlag, "", "Argus API base URL, used in calls to this API") cmd.Flags().String(authorizationCustomEndpointFlag, "", "Authorization API base URL, used in calls to this API") cmd.Flags().String(dnsCustomEndpointFlag, "", "DNS API base URL, used in calls to this API") From 0b1565958b5b7ac28e157a79aac160db26960478 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Go=CC=88kc=CC=A7e=20Go=CC=88k=20Klingel?= Date: Tue, 20 Aug 2024 08:10:44 +0200 Subject: [PATCH 04/10] extend unset command for token and jwks --- internal/cmd/config/unset/unset.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/internal/cmd/config/unset/unset.go b/internal/cmd/config/unset/unset.go index d24186c37..b1ef0046e 100644 --- a/internal/cmd/config/unset/unset.go +++ b/internal/cmd/config/unset/unset.go @@ -195,6 +195,12 @@ func NewCmd(p *print.Printer) *cobra.Command { if model.IaaSCustomEndpoint { viper.Set(config.IaaSCustomEndpointKey, "") } + if model.TokenCustomEndpoint { + viper.Set(config.TokenCustomEndpointKey, "") + } + if model.JwksCustomEndpoint { + viper.Set(config.JwksCustomEndpointKey, "") + } err := config.Write() if err != nil { From 2d5e0e1eae07552f1f583a1bda8de9c4d90c3dd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Go=CC=88kc=CC=A7e=20Go=CC=88k=20Klingel?= Date: Tue, 20 Aug 2024 08:12:55 +0200 Subject: [PATCH 05/10] Change method name --- internal/cmd/curl/curl.go | 2 +- internal/pkg/auth/utils.go | 2 +- internal/pkg/utils/utils.go | 2 +- internal/pkg/utils/utils_test.go | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/internal/cmd/curl/curl.go b/internal/cmd/curl/curl.go index 1dc2e0428..e78accf1a 100644 --- a/internal/cmd/curl/curl.go +++ b/internal/cmd/curl/curl.go @@ -67,7 +67,7 @@ func NewCmd(p *print.Printer) *cobra.Command { `$ stackit curl https://dns.api.stackit.cloud/v1/projects/xxx/zones -X POST -H "Authorization: Bearer yyy" --fail`, ), ), - Args: args.SingleArg(urlArg, utils.ValidateURL), + Args: args.SingleArg(urlArg, utils.ValidateURLDomain), RunE: func(cmd *cobra.Command, args []string) (err error) { model, err := parseInput(p, cmd, args) if err != nil { diff --git a/internal/pkg/auth/utils.go b/internal/pkg/auth/utils.go index dbd80f92b..34f5cbf1b 100644 --- a/internal/pkg/auth/utils.go +++ b/internal/pkg/auth/utils.go @@ -15,7 +15,7 @@ func getIDPEndpoint() (string, error) { if customIDPEndpoint != "" { idpEndpoint = customIDPEndpoint } - err := utils.ValidateURL(idpEndpoint) + err := utils.ValidateURLDomain(idpEndpoint) if err != nil { return "", fmt.Errorf("validate custom identity provider endpoint: %w", err) } diff --git a/internal/pkg/utils/utils.go b/internal/pkg/utils/utils.go index 6606718ca..f3abe7ce0 100644 --- a/internal/pkg/utils/utils.go +++ b/internal/pkg/utils/utils.go @@ -57,7 +57,7 @@ func ConvertInt64PToFloat64P(i *int64) *float64 { return &f } -func ValidateURL(value string) error { +func ValidateURLDomain(value string) error { urlStruct, err := url.Parse(value) if err != nil { return fmt.Errorf("parse url: %w", err) diff --git a/internal/pkg/utils/utils_test.go b/internal/pkg/utils/utils_test.go index 0b1ee83e3..6538dea61 100644 --- a/internal/pkg/utils/utils_test.go +++ b/internal/pkg/utils/utils_test.go @@ -49,7 +49,7 @@ func TestConvertInt64PToFloat64P(t *testing.T) { } } -func TestValidateURL(t *testing.T) { +func TestValidateURLDomain(t *testing.T) { tests := []struct { name string allowedUrlDomain string @@ -92,7 +92,7 @@ func TestValidateURL(t *testing.T) { viper.Reset() viper.Set(config.AllowedUrlDomainKey, tt.allowedUrlDomain) - err := ValidateURL(tt.input) + err := ValidateURLDomain(tt.input) if tt.isValid && err != nil { t.Errorf("expected URL to be valid, got error: %v", err) } From ff51bf9cc539997b657237d62d20234b0ae0ec35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Go=CC=88kc=CC=A7e=20Go=CC=88k=20Klingel?= Date: Tue, 20 Aug 2024 08:41:54 +0200 Subject: [PATCH 06/10] Change endpoint handling --- docs/stackit_config_set.md | 2 +- .../activate_service_account.go | 28 +++++------ .../activate_service_account_test.go | 46 +++++++++---------- 3 files changed, 35 insertions(+), 41 deletions(-) diff --git a/docs/stackit_config_set.md b/docs/stackit_config_set.md index 191dfc823..d739983cc 100644 --- a/docs/stackit_config_set.md +++ b/docs/stackit_config_set.md @@ -29,7 +29,7 @@ stackit config set [flags] ### Options ``` - --allowed-url-domain string Domain name, used for the verification of the URLs that are given in the IDP endpoint and curl commands + --allowed-url-domain string Domain name, used for the verification of the URLs that are given in the custom identidy provider endpoint and "STACKIT curl" command --argus-custom-endpoint string Argus API base URL, used in calls to this API --authorization-custom-endpoint string Authorization API base URL, used in calls to this API --dns-custom-endpoint string DNS API base URL, used in calls to this API diff --git a/internal/cmd/auth/activate-service-account/activate_service_account.go b/internal/cmd/auth/activate-service-account/activate_service_account.go index 37ae10e3e..e49ec48df 100644 --- a/internal/cmd/auth/activate-service-account/activate_service_account.go +++ b/internal/cmd/auth/activate-service-account/activate_service_account.go @@ -28,8 +28,6 @@ type inputModel struct { ServiceAccountToken string ServiceAccountKeyPath string PrivateKeyPath string - TokenCustomEndpoint string - JwksCustomEndpoint string } func NewCmd(p *print.Printer) *cobra.Command { @@ -56,7 +54,7 @@ func NewCmd(p *print.Printer) *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { model := parseInput(p, cmd) - err := storeFlags(model) + tokenCustomEndpoint, jwksCustomEndpoint, err := storeFlags() if err != nil { return err } @@ -65,8 +63,8 @@ func NewCmd(p *print.Printer) *cobra.Command { Token: model.ServiceAccountToken, ServiceAccountKeyPath: model.ServiceAccountKeyPath, PrivateKeyPath: model.PrivateKeyPath, - TokenCustomUrl: model.TokenCustomEndpoint, - JWKSCustomUrl: model.JwksCustomEndpoint, + TokenCustomUrl: tokenCustomEndpoint, + JWKSCustomUrl: jwksCustomEndpoint, } // Setup authentication based on the provided credentials and the environment @@ -103,15 +101,10 @@ func configureFlags(cmd *cobra.Command) { } func parseInput(p *print.Printer, cmd *cobra.Command) *inputModel { - tokenCustomEndpoint := viper.GetString(config.TokenCustomEndpointKey) - jwksCustomEndpoint := viper.GetString(config.JwksCustomEndpointKey) - model := inputModel{ ServiceAccountToken: flags.FlagToStringValue(p, cmd, serviceAccountTokenFlag), ServiceAccountKeyPath: flags.FlagToStringValue(p, cmd, serviceAccountKeyPathFlag), PrivateKeyPath: flags.FlagToStringValue(p, cmd, privateKeyPathFlag), - TokenCustomEndpoint: tokenCustomEndpoint, - JwksCustomEndpoint: jwksCustomEndpoint, } if p.IsVerbosityDebug() { @@ -126,14 +119,17 @@ func parseInput(p *print.Printer, cmd *cobra.Command) *inputModel { return &model } -func storeFlags(model *inputModel) error { - err := auth.SetAuthField(auth.TOKEN_CUSTOM_ENDPOINT, model.TokenCustomEndpoint) +func storeFlags() (tokenCustomEndpoint, jwksCustomEndpoint string, err error) { + tokenCustomEndpoint = viper.GetString(config.TokenCustomEndpointKey) + jwksCustomEndpoint = viper.GetString(config.JwksCustomEndpointKey) + + err = auth.SetAuthField(auth.TOKEN_CUSTOM_ENDPOINT, tokenCustomEndpoint) if err != nil { - return fmt.Errorf("set %s: %w", auth.TOKEN_CUSTOM_ENDPOINT, err) + return "", "", fmt.Errorf("set %s: %w", auth.TOKEN_CUSTOM_ENDPOINT, err) } - err = auth.SetAuthField(auth.JWKS_CUSTOM_ENDPOINT, model.JwksCustomEndpoint) + err = auth.SetAuthField(auth.JWKS_CUSTOM_ENDPOINT, jwksCustomEndpoint) if err != nil { - return fmt.Errorf("set %s: %w", auth.JWKS_CUSTOM_ENDPOINT, err) + return "", "", fmt.Errorf("set %s: %w", auth.JWKS_CUSTOM_ENDPOINT, err) } - return nil + return tokenCustomEndpoint, jwksCustomEndpoint, nil } diff --git a/internal/cmd/auth/activate-service-account/activate_service_account_test.go b/internal/cmd/auth/activate-service-account/activate_service_account_test.go index c70601ab0..6beadb0d6 100644 --- a/internal/cmd/auth/activate-service-account/activate_service_account_test.go +++ b/internal/cmd/auth/activate-service-account/activate_service_account_test.go @@ -33,8 +33,6 @@ func fixtureInputModel(mods ...func(model *inputModel)) *inputModel { ServiceAccountToken: "token", ServiceAccountKeyPath: "sa_key", PrivateKeyPath: "private_key", - TokenCustomEndpoint: "token_url", - JwksCustomEndpoint: "jwks_url", } for _, mod := range mods { mod(model) @@ -69,8 +67,6 @@ func TestParseInput(t *testing.T) { ServiceAccountToken: "", ServiceAccountKeyPath: "", PrivateKeyPath: "", - TokenCustomEndpoint: "", - JwksCustomEndpoint: "", }, }, { @@ -87,8 +83,6 @@ func TestParseInput(t *testing.T) { ServiceAccountToken: "", ServiceAccountKeyPath: "", PrivateKeyPath: "", - TokenCustomEndpoint: "", - JwksCustomEndpoint: "", }, }, { @@ -119,10 +113,6 @@ func TestParseInput(t *testing.T) { } } - viper.Reset() - viper.Set(config.TokenCustomEndpointKey, tt.tokenCustomEndpoint) - viper.Set(config.JwksCustomEndpointKey, tt.jwksCustomEndpoint) - model := parseInput(p, cmd) if !tt.isValid { @@ -138,14 +128,18 @@ func TestParseInput(t *testing.T) { func TestStoreFlags(t *testing.T) { tests := []struct { - description string - model *inputModel - isValid bool + description string + model *inputModel + tokenCustomEndpoint string + jwksCustomEndpoint string + isValid bool }{ { - description: "base", - model: fixtureInputModel(), - isValid: true, + description: "base", + model: fixtureInputModel(), + tokenCustomEndpoint: testTokenCustomEndpoint, + jwksCustomEndpoint: testJwksCustomEndpoint, + isValid: true, }, { description: "no values", @@ -153,10 +147,10 @@ func TestStoreFlags(t *testing.T) { ServiceAccountToken: "", ServiceAccountKeyPath: "", PrivateKeyPath: "", - TokenCustomEndpoint: "", - JwksCustomEndpoint: "", }, - isValid: true, + tokenCustomEndpoint: "", + jwksCustomEndpoint: "", + isValid: true, }, } @@ -165,7 +159,11 @@ func TestStoreFlags(t *testing.T) { // Initialize an empty keyring keyring.MockInit() - err := storeFlags(tt.model) + viper.Reset() + viper.Set(config.TokenCustomEndpointKey, tt.tokenCustomEndpoint) + viper.Set(config.JwksCustomEndpointKey, tt.jwksCustomEndpoint) + + tokenCustomEndpoint, jwksCustomEndpoint, err := storeFlags() if !tt.isValid { if err == nil { t.Fatalf("did not fail on invalid input") @@ -180,16 +178,16 @@ func TestStoreFlags(t *testing.T) { if err != nil { t.Errorf("Failed to get value of auth field: %v", err) } - if value != tt.model.TokenCustomEndpoint { - t.Errorf("Value of \"%s\" does not match: expected \"%s\", got \"%s\"", auth.TOKEN_CUSTOM_ENDPOINT, tt.model.TokenCustomEndpoint, value) + if value != tokenCustomEndpoint { + t.Errorf("Value of \"%s\" does not match: expected \"%s\", got \"%s\"", auth.TOKEN_CUSTOM_ENDPOINT, tokenCustomEndpoint, value) } value, err = auth.GetAuthField(auth.JWKS_CUSTOM_ENDPOINT) if err != nil { t.Errorf("Failed to get value of auth field: %v", err) } - if value != tt.model.JwksCustomEndpoint { - t.Errorf("Value of \"%s\" does not match: expected \"%s\", got \"%s\"", auth.JWKS_CUSTOM_ENDPOINT, tt.model.TokenCustomEndpoint, value) + if value != jwksCustomEndpoint { + t.Errorf("Value of \"%s\" does not match: expected \"%s\", got \"%s\"", auth.JWKS_CUSTOM_ENDPOINT, jwksCustomEndpoint, value) } }) } From 9e209fa5f96155d11afcba90a73bf6476f565dfd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Go=CC=88kc=CC=A7e=20Go=CC=88k=20Klingel?= Date: Tue, 20 Aug 2024 10:31:46 +0200 Subject: [PATCH 07/10] change default value handling for the allowed url domain --- docs/stackit_config_unset.md | 2 +- internal/cmd/config/unset/unset.go | 4 ++-- internal/pkg/config/config.go | 4 +++- internal/pkg/utils/utils.go | 8 ++------ 4 files changed, 8 insertions(+), 10 deletions(-) diff --git a/docs/stackit_config_unset.md b/docs/stackit_config_unset.md index 70843510d..fc68fe45a 100644 --- a/docs/stackit_config_unset.md +++ b/docs/stackit_config_unset.md @@ -26,7 +26,7 @@ stackit config unset [flags] ### Options ``` - --allowed-url-domain Domain name, used for the verification of the URLs that are given in the IDP endpoint and curl commands + --allowed-url-domain Domain name, used for the verification of the URLs that are given in the IDP endpoint and curl commands. If unset, defaults to stackit.cloud --argus-custom-endpoint Argus API base URL. If unset, uses the default base URL --async Configuration option to run commands asynchronously --authorization-custom-endpoint Authorization API base URL. If unset, uses the default base URL diff --git a/internal/cmd/config/unset/unset.go b/internal/cmd/config/unset/unset.go index b1ef0046e..44526ba4d 100644 --- a/internal/cmd/config/unset/unset.go +++ b/internal/cmd/config/unset/unset.go @@ -129,7 +129,7 @@ func NewCmd(p *print.Printer) *cobra.Command { viper.Set(config.IdentityProviderCustomClientIdKey, "") } if model.AllowedUrlDomain { - viper.Set(config.AllowedUrlDomainKey, "") + viper.Set(config.AllowedUrlDomainKey, config.AllowedUrlDomainDefault) } if model.ArgusCustomEndpoint { @@ -222,7 +222,7 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().Bool(sessionTimeLimitFlag, false, fmt.Sprintf("Maximum time before authentication is required again. If unset, defaults to %s", config.SessionTimeLimitDefault)) cmd.Flags().Bool(identityProviderCustomEndpointFlag, false, "Identity Provider base URL. If unset, uses the default base URL") cmd.Flags().Bool(identityProviderCustomClientIdFlag, false, "Identity Provider client ID, used for user authentication") - cmd.Flags().Bool(allowedUrlDomainFlag, false, "Domain name, used for the verification of the URLs that are given in the IDP endpoint and curl commands") + cmd.Flags().Bool(allowedUrlDomainFlag, false, fmt.Sprintf("Domain name, used for the verification of the URLs that are given in the IDP endpoint and curl commands. If unset, defaults to %s", config.AllowedUrlDomainDefault)) cmd.Flags().Bool(argusCustomEndpointFlag, false, "Argus API base URL. If unset, uses the default base URL") cmd.Flags().Bool(authorizationCustomEndpointFlag, false, "Authorization API base URL. If unset, uses the default base URL") diff --git a/internal/pkg/config/config.go b/internal/pkg/config/config.go index 02823d109..6ca8db9c9 100644 --- a/internal/pkg/config/config.go +++ b/internal/pkg/config/config.go @@ -50,6 +50,8 @@ const ( AsyncDefault = false SessionTimeLimitDefault = "2h" + + AllowedUrlDomainDefault = "stackit.cloud" ) const ( @@ -157,7 +159,7 @@ func setConfigDefaults() { viper.SetDefault(SessionTimeLimitKey, SessionTimeLimitDefault) viper.SetDefault(IdentityProviderCustomEndpointKey, "") viper.SetDefault(IdentityProviderCustomClientIdKey, "") - viper.SetDefault(AllowedUrlDomainKey, "") + viper.SetDefault(AllowedUrlDomainKey, AllowedUrlDomainDefault) viper.SetDefault(DNSCustomEndpointKey, "") viper.SetDefault(ArgusCustomEndpointKey, "") viper.SetDefault(AuthorizationCustomEndpointKey, "") diff --git a/internal/pkg/utils/utils.go b/internal/pkg/utils/utils.go index f3abe7ce0..a30771c36 100644 --- a/internal/pkg/utils/utils.go +++ b/internal/pkg/utils/utils.go @@ -11,10 +11,6 @@ import ( "github.com/stackitcloud/stackit-cli/internal/pkg/config" ) -const ( - defaultAllowedUrlDomain = "stackit.cloud" -) - // Ptr Returns the pointer to any type T func Ptr[T any](v T) *T { return &v @@ -70,11 +66,11 @@ func ValidateURLDomain(value string) error { allowedUrlDomain := viper.GetString(config.AllowedUrlDomainKey) if allowedUrlDomain == "" { - allowedUrlDomain = defaultAllowedUrlDomain + allowedUrlDomain = config.AllowedUrlDomainDefault } if !strings.HasSuffix(urlHost, allowedUrlDomain) { - return fmt.Errorf(`only urls belonging to domain %s are allowed"`, allowedUrlDomain) + return fmt.Errorf(`only urls belonging to domain %s are allowed`, allowedUrlDomain) } return nil } From 6781b50938d2685e1856ec0c6d970a435a6dbf0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Go=CC=88kc=CC=A7e=20Go=CC=88k=20Klingel?= Date: Tue, 20 Aug 2024 13:36:06 +0200 Subject: [PATCH 08/10] accept empty url domains and add warning --- internal/cmd/config/set/set.go | 5 +++++ internal/pkg/utils/utils.go | 4 ---- internal/pkg/utils/utils_test.go | 6 ++++++ 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/internal/cmd/config/set/set.go b/internal/cmd/config/set/set.go index 7aa7ce727..3a22e6663 100644 --- a/internal/cmd/config/set/set.go +++ b/internal/cmd/config/set/set.go @@ -234,6 +234,11 @@ func parseInput(p *print.Printer, cmd *cobra.Command) (*inputModel, error) { projectIdSet = true } + allowedUrlDomainFromFlag := flags.FlagToStringValue(p, cmd, allowedUrlDomainFlag) + if allowedUrlDomainFromFlag == "" { + p.Warn("The allowed URL domain is set to empty. All URLs will be accepted regardless of their domain.\n") + } + model := inputModel{ SessionTimeLimit: sessionTimeLimit, ProjectIdSet: projectIdSet, diff --git a/internal/pkg/utils/utils.go b/internal/pkg/utils/utils.go index a30771c36..205bff1cf 100644 --- a/internal/pkg/utils/utils.go +++ b/internal/pkg/utils/utils.go @@ -65,10 +65,6 @@ func ValidateURLDomain(value string) error { allowedUrlDomain := viper.GetString(config.AllowedUrlDomainKey) - if allowedUrlDomain == "" { - allowedUrlDomain = config.AllowedUrlDomainDefault - } - if !strings.HasSuffix(urlHost, allowedUrlDomain) { return fmt.Errorf(`only urls belonging to domain %s are allowed`, allowedUrlDomain) } diff --git a/internal/pkg/utils/utils_test.go b/internal/pkg/utils/utils_test.go index 6538dea61..6ef165d9b 100644 --- a/internal/pkg/utils/utils_test.go +++ b/internal/pkg/utils/utils_test.go @@ -80,6 +80,12 @@ func TestValidateURLDomain(t *testing.T) { input: "https://www.test.example.com/", isValid: true, }, + { + name: "every URL valid", + allowedUrlDomain: "", + input: "https://www.test.example.com/", + isValid: true, + }, { name: "invalid URL", input: "", From 649238317a4b308aca83a9ec24b2690f6476e289 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Go=CC=88kc=CC=A7e=20Go=CC=88k=20Klingel?= Date: Tue, 20 Aug 2024 14:30:22 +0200 Subject: [PATCH 09/10] fix unit tests --- internal/cmd/curl/curl_test.go | 16 +++++++++++++--- internal/pkg/auth/utils_test.go | 25 +++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/internal/cmd/curl/curl_test.go b/internal/cmd/curl/curl_test.go index b6e9faa30..62d70e607 100644 --- a/internal/cmd/curl/curl_test.go +++ b/internal/cmd/curl/curl_test.go @@ -7,6 +7,8 @@ import ( "net/http" "testing" + "github.com/spf13/viper" + "github.com/stackitcloud/stackit-cli/internal/pkg/config" "github.com/stackitcloud/stackit-cli/internal/pkg/globalflags" "github.com/stackitcloud/stackit-cli/internal/pkg/print" "github.com/stackitcloud/stackit-cli/internal/pkg/utils" @@ -78,6 +80,7 @@ func TestParseInput(t *testing.T) { argValues []string flagValues map[string]string headerFlagValues []string + allowedURLDomain string isValid bool expectedModel *inputModel }{ @@ -123,10 +126,14 @@ func TestParseInput(t *testing.T) { { description: "URL outside STACKIT", argValues: []string{ - "https://www.very-suspicious-website.com/", + "https://www.example.website.com/", }, - flagValues: fixtureFlagValues(), - isValid: false, + flagValues: fixtureFlagValues(), + allowedURLDomain: "", + isValid: true, + expectedModel: fixtureInputModel(func(model *inputModel) { + model.URL = "https://www.example.website.com/" + }), }, { description: "invalid method 1", @@ -213,6 +220,9 @@ func TestParseInput(t *testing.T) { t.Fatalf("configure global flags: %v", err) } + viper.Reset() + viper.Set(config.AllowedUrlDomainKey, tt.allowedURLDomain) + for flag, value := range tt.flagValues { err := cmd.Flags().Set(flag, value) if err != nil { diff --git a/internal/pkg/auth/utils_test.go b/internal/pkg/auth/utils_test.go index c345c4d82..bb9e5a633 100644 --- a/internal/pkg/auth/utils_test.go +++ b/internal/pkg/auth/utils_test.go @@ -11,23 +11,47 @@ func TestGetIDPEndpoint(t *testing.T) { tests := []struct { name string idpCustomEndpoint string + allowedUrlDomain string isValid bool expected string }{ { name: "custom endpoint specified", idpCustomEndpoint: "https://example.stackit.cloud", + allowedUrlDomain: "stackit.cloud", isValid: true, expected: "https://example.stackit.cloud", }, { name: "custom endpoint outside STACKIT", idpCustomEndpoint: "https://www.very-suspicious-website.com/", + allowedUrlDomain: "stackit.cloud", isValid: false, }, + { + name: "non-STACKIT custom endpoint invalid", + idpCustomEndpoint: "https://www.very-suspicious-website.com/", + allowedUrlDomain: "stackit.cloud", + isValid: false, + }, + { + name: "non-STACKIT custom endpoint valid", + idpCustomEndpoint: "https://www.test.example.com/", + allowedUrlDomain: "example.com", + isValid: true, + expected: "https://www.test.example.com/", + }, + { + name: "every URL valid", + idpCustomEndpoint: "https://www.test.example.com/", + allowedUrlDomain: "", + isValid: true, + expected: "https://www.test.example.com/", + }, { name: "custom endpoint not specified", idpCustomEndpoint: "", + allowedUrlDomain: "", isValid: true, expected: defaultIDPEndpoint, }, @@ -36,6 +60,7 @@ func TestGetIDPEndpoint(t *testing.T) { t.Run(tt.name, func(t *testing.T) { viper.Reset() viper.Set(config.IdentityProviderCustomEndpointKey, tt.idpCustomEndpoint) + viper.Set(config.AllowedUrlDomainKey, tt.allowedUrlDomain) got, err := getIDPEndpoint() From 361b3d94cd6411fac75b8be76cae83c28ce825f1 Mon Sep 17 00:00:00 2001 From: GokceGK <161626272+GokceGK@users.noreply.github.com> Date: Wed, 21 Aug 2024 16:49:36 +0200 Subject: [PATCH 10/10] Update internal/cmd/config/set/set.go Co-authored-by: Vicente Pinto --- internal/cmd/config/set/set.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/cmd/config/set/set.go b/internal/cmd/config/set/set.go index 3a22e6663..eb62d1053 100644 --- a/internal/cmd/config/set/set.go +++ b/internal/cmd/config/set/set.go @@ -134,7 +134,7 @@ func configureFlags(cmd *cobra.Command) { cmd.Flags().String(sessionTimeLimitFlag, "", "Maximum time before authentication is required again. After this time, you will be prompted to login again to execute commands that require authentication. Can't be larger than 24h. Requires authentication after being set to take effect. Examples: 3h, 5h30m40s (BETA: currently values greater than 2h have no effect)") cmd.Flags().String(identityProviderCustomEndpointFlag, "", "Identity Provider base URL, used for user authentication") cmd.Flags().String(identityProviderCustomClientIdFlag, "", "Identity Provider client ID, used for user authentication") - cmd.Flags().String(allowedUrlDomainFlag, "", `Domain name, used for the verification of the URLs that are given in the custom identidy provider endpoint and "STACKIT curl" command`) + cmd.Flags().String(allowedUrlDomainFlag, "", `Domain name, used for the verification of the URLs that are given in the custom identity provider endpoint and "STACKIT curl" command`) cmd.Flags().String(argusCustomEndpointFlag, "", "Argus API base URL, used in calls to this API") cmd.Flags().String(authorizationCustomEndpointFlag, "", "Authorization API base URL, used in calls to this API") cmd.Flags().String(dnsCustomEndpointFlag, "", "DNS API base URL, used in calls to this API")