diff --git a/README.md b/README.md index b7683a8..15231ed 100644 --- a/README.md +++ b/README.md @@ -106,7 +106,7 @@ Once the Kong admin API is secured a gateway service for it must be added to Kon - Basic authentication - API Key authentication -- OAuth2 authentication +- OAuth2 authentication (currently, Kong returns an Internal Server Error if securing the admin api with OAuth2. The plugin can be created in Kong, but further requests will not work when receiving the token. The Agent is also configured to (as of now) not work with OAuth2) #### Specification discovery methods @@ -180,7 +180,7 @@ The Kong agents are delivered as containers, kong_discovery_agent and kong_trace Before beginning to deploy the agents following information will need to be gathered in addition to the details that were noted in setup. -- The full URL to connect to the Kong admin API, `KONG_ADMIN_URL` +- The full URL to connect to the Kong admin API, `KONG_ADMIN_URL`. Note that if secured by kong, the URL should look like: https://host:port/secured-route-from-kong - The host the agent will use when setting the endpoint of a discovered API, (`KONG_PROXY_HOST`) - The HTTP `KONG_PROXY_PORTS_HTTP` and HTTPs `KONG_PROXY_PORTS_HTTPS` ports the agent will use with the endpoint above - The URL paths, hosted by the gateway service, to query for spec files, `KONG_SPEC_URLPATHS` @@ -392,13 +392,15 @@ Finally, when a Marketplace user requests a credential, within the Kong environm All Kong specific environment variables available are listed below -| Name | Description | -| --------------------------------- | ------------------------------------------------------------------------------------- | -| **KONG_ADMIN_URL** | The Kong admin API URL that the agent will query against | -| **KONG_ADMIN_AUTH_APIKEY_HEADER** | The API Key header name the agent will use when authenticating | -| **KONG_ADMIN_AUTH_APIKEY_VALUE** | The API Key value the agent will use when authenticating | -| **KONG_PROXY_HOST** | The proxy endpoint that the agent will use in API Services for discovered Kong routes | -| **KONG_PROXY_PORTS_HTTP** | The HTTP port number that the agent will set for discovered APIS | -| **KONG_PROXY_PORTS_HTTPS** | The HTTPs port number that the agent will set for discovered APIS | -| **KONG_SPEC_LOCALPATH** | The local path that the agent will look in for API definitions | -| **KONG_SPEC_URLPATHS** | The URL paths that the agent will query on the gateway service for API definitions | +| Name | Description | +| -------------------------------------- | ------------------------------------------------------------------------------------- | +| **KONG_ADMIN_URL** | The Kong admin API URL that the agent will query against | +| **KONG_ADMIN_AUTH_APIKEY_HEADER** | The API Key header name the agent will use when authenticating | +| **KONG_ADMIN_AUTH_APIKEY_VALUE** | The API Key value the agent will use when authenticating | +| **KONG_ADMIN_AUTH_BASICAUTH_USERNAME** | The HTTP Basic username that the agent will use when authenticating | +| **KONG_ADMIN_AUTH_BASICAUTH_PASSWORD** | The HTTP Basic password that the agent will use when authenticating | +| **KONG_PROXY_HOST** | The proxy endpoint that the agent will use in API Services for discovered Kong routes | +| **KONG_PROXY_PORTS_HTTP** | The HTTP port number that the agent will set for discovered APIS | +| **KONG_PROXY_PORTS_HTTPS** | The HTTPs port number that the agent will set for discovered APIS | +| **KONG_SPEC_LOCALPATH** | The local path that the agent will look in for API definitions | +| **KONG_SPEC_URLPATHS** | The URL paths that the agent will query on the gateway service for API definitions | diff --git a/helm/kong-agents/templates/discovery-deployment.yaml b/helm/kong-agents/templates/discovery-deployment.yaml index ba1d01f..d6c07f9 100644 --- a/helm/kong-agents/templates/discovery-deployment.yaml +++ b/helm/kong-agents/templates/discovery-deployment.yaml @@ -66,14 +66,14 @@ spec: {{- end }} {{- end }} env: - - name: KONG_ADMIN_URL - value: "{{ .Values.kong.admin.url }}" - name: KONG_PROXY_HOST value: "{{ .Values.kong.proxy.host }}" - name: KONG_PROXY_PORTS_HTTP value: "{{ .Values.kong.proxy.ports.http }}" - name: KONG_PROXY_PORTS_HTTPS value: "{{ .Values.kong.proxy.ports.https }}" + - name: KONG_ADMIN_URL + value: "{{ .Values.kong.admin.url }}" {{- if (include "kong-agents.specDownloadPathsString" .) }} - name: KONG_SPEC_URLPATHS value: {{ include "kong-agents.specDownloadPathsString" . }} @@ -98,6 +98,18 @@ spec: name: kong-admin-auth-apikey key: header {{- end }} + {{- if .Values.kong.admin.auth.basicAuth.username }} + - name: KONG_ADMIN_AUTH_BASICAUTH_USERNAME + valueFrom: + secretKeyRef: + name: kong-admin-auth-basicauth + key: username + - name: KONG_ADMIN_AUTH_BASICAUTH_PASSWORD + valueFrom: + secretKeyRef: + name: kong-admin-auth-basicauth + key: password + {{- end }} {{- with .Values.env }} {{- range $key, $value := . }} {{- if and (not (eq (toString $value) "")) diff --git a/helm/kong-agents/templates/kong-admin-auth-basicauth.yaml b/helm/kong-agents/templates/kong-admin-auth-basicauth.yaml new file mode 100644 index 0000000..fde0aa5 --- /dev/null +++ b/helm/kong-agents/templates/kong-admin-auth-basicauth.yaml @@ -0,0 +1,10 @@ +{{- if .Values.kong.admin.auth.basicAuth.username }} +apiVersion: v1 +kind: Secret +metadata: + name: kong-admin-auth-basicauth +type: Opaque +stringData: + username: "{{ .Values.kong.admin.auth.basicAuth.username }}" + password: "{{ .Values.kong.admin.auth.basicAuth.password }}" +{{- end }} \ No newline at end of file diff --git a/helm/kong-agents/values.yaml b/helm/kong-agents/values.yaml index 6cb7209..43c19d1 100644 --- a/helm/kong-agents/values.yaml +++ b/helm/kong-agents/values.yaml @@ -29,8 +29,11 @@ kong: apikey: header: value: + basicAuth: + username: + password: proxy: - host: + host: ports: http: 8000 https: 8443 diff --git a/pkg/cmd/discovery/cmd.go b/pkg/cmd/discovery/cmd.go index 21f3ef8..4730c90 100644 --- a/pkg/cmd/discovery/cmd.go +++ b/pkg/cmd/discovery/cmd.go @@ -62,14 +62,9 @@ func run() error { // and passed to the callback allowing the agent code to access the central config func initConfig(centralConfig corecfg.CentralConfig) (interface{}, error) { rootProps := DiscoveryCmd.GetProperties() - agentConfig = config.AgentConfig{ CentralCfg: centralConfig, KongGatewayCfg: config.ParseProperties(rootProps), } return agentConfig, nil } - -func GetAgentConfig() config.AgentConfig { - return agentConfig -} diff --git a/pkg/config/discovery/config.go b/pkg/config/discovery/config.go index 3bd7034..398dc26 100644 --- a/pkg/config/discovery/config.go +++ b/pkg/config/discovery/config.go @@ -2,30 +2,37 @@ package config import ( "fmt" + "net/url" + "strings" "github.com/Axway/agent-sdk/pkg/cmd/properties" corecfg "github.com/Axway/agent-sdk/pkg/config" + "github.com/Axway/agent-sdk/pkg/util/log" ) const ( - cfgKongAdminURL = "kong.admin.url" - cfgKongAdminAPIKey = "kong.admin.auth.apikey.value" - cfgKongAdminAPIKeyHeader = "kong.admin.auth.apikey.header" cfgKongProxyHost = "kong.proxy.host" - cfgKongProxyPortHttp = "kong.proxy.port.http" - cfgKongProxyPortHttps = "kong.proxy.port.https" + cfgKongAdminUrl = "kong.admin.url" + cfgKongAdminAPIKey = "kong.admin.auth.apiKey.value" + cfgKongAdminAPIKeyHeader = "kong.admin.auth.apiKey.header" + cfgKongAdminUsername = "kong.admin.auth.basicauth.username" + cfgKongAdminPassword = "kong.admin.auth.basicauth.password" + cfgKongProxyPortHttp = "kong.proxy.ports.http" + cfgKongProxyPortHttps = "kong.proxy.ports.https" cfgKongSpecURLPaths = "kong.spec.urlPaths" cfgKongSpecLocalPath = "kong.spec.localPath" cfgKongSpecFilter = "kong.spec.filter" ) func AddKongProperties(rootProps properties.Properties) { - rootProps.AddStringProperty(cfgKongAdminURL, "", "The Kong admin endpoint") + rootProps.AddStringProperty(cfgKongAdminUrl, "", "The Admin API url") rootProps.AddStringProperty(cfgKongAdminAPIKey, "", "API Key value to authenticate with Kong Gateway") rootProps.AddStringProperty(cfgKongAdminAPIKeyHeader, "", "API Key header to authenticate with Kong Gateway") + rootProps.AddStringProperty(cfgKongAdminUsername, "", "Username for basic auth to authenticate with Kong Admin API") + rootProps.AddStringProperty(cfgKongAdminPassword, "", "Password for basic auth to authenticate with Kong Admin API") rootProps.AddStringProperty(cfgKongProxyHost, "", "The Kong proxy endpoint") - rootProps.AddIntProperty(cfgKongProxyPortHttp, 0, "The Kong proxy http port") - rootProps.AddIntProperty(cfgKongProxyPortHttps, 0, "The Kong proxy https port") + rootProps.AddIntProperty(cfgKongProxyPortHttp, 80, "The Kong proxy http port") + rootProps.AddIntProperty(cfgKongProxyPortHttps, 443, "The Kong proxy https port") rootProps.AddStringSliceProperty(cfgKongSpecURLPaths, []string{}, "URL paths that the agent will look in for spec files") rootProps.AddStringProperty(cfgKongSpecLocalPath, "", "Local paths where the agent will look for spec files") rootProps.AddStringProperty(cfgKongSpecFilter, "", "SDK Filter format. Empty means filters are ignored.") @@ -38,12 +45,18 @@ type AgentConfig struct { } type KongAdminConfig struct { - URL string `config:"url"` + Url string `config:"url"` Auth KongAdminAuthConfig `config:"auth"` } type KongAdminAuthConfig struct { - APIKey KongAdminAuthAPIKeyConfig `config:"apikey"` + APIKey KongAdminAuthAPIKeyConfig `config:"apiKey"` + BasicAuth KongAdminBasicAuthConfig `config:"basicAuth"` +} + +type KongAdminBasicAuthConfig struct { + Username string `config:"username"` + Password string `config:"password"` } type KongAdminAuthAPIKeyConfig struct { @@ -52,11 +65,11 @@ type KongAdminAuthAPIKeyConfig struct { } type KongProxyConfig struct { - Host string `config:"host"` - Port KongProxyPortConfig `config:"port"` + Host string `config:"host"` + Ports KongPortConfig `config:"ports"` } -type KongProxyPortConfig struct { +type KongPortConfig struct { HTTP int `config:"http"` HTTPS int `config:"https"` } @@ -76,35 +89,89 @@ type KongGatewayConfig struct { Spec KongSpecConfig `config:"spec"` } +const ( + hostErr = "kong host must be provided." + proxyPortErr = "both proxy port values of http https are required" + invalidUrlErr = "invalid Admin API url provided. Must contain protocol + hostname + port." + + "Examples: , " + credentialConfigErr = "invalid authorization configuration provided. " + + "If provided, (Username and Password) or (ClientID and ClientSecret) must be non-empty" +) + // ValidateCfg - Validates the gateway config func (c *KongGatewayConfig) ValidateCfg() (err error) { - if c.Admin.URL == "" { - return fmt.Errorf("error: admin url is required") - } + logger := log.NewFieldLogger().WithPackage("config").WithComponent("ValidateConfig") if c.Proxy.Host == "" { - return fmt.Errorf("error: proxy host is required") + return fmt.Errorf(hostErr) } - if c.Proxy.Port.HTTP == 0 && c.Proxy.Port.HTTPS == 0 { - return fmt.Errorf("error: at least one proxy port value of either http or https is required") + if c.Proxy.Ports.HTTP == 0 || c.Proxy.Ports.HTTPS == 0 { + return fmt.Errorf(proxyPortErr) + } + if invalidAdminUrl(c.Admin.Url) { + return fmt.Errorf(invalidUrlErr) + } + if noCredentialsProvided(c) { + logger.Warn("No credentials provided. Assuming Kong Admin API requires no authorization.") + } + if invalidCredentialConfig(c) { + return fmt.Errorf(credentialConfigErr) } return } +func noCredentialsProvided(c *KongGatewayConfig) bool { + apiKey := c.Admin.Auth.APIKey.Value + user := c.Admin.Auth.BasicAuth.Username + pass := c.Admin.Auth.BasicAuth.Password + + if apiKey == "" && user == "" && pass == "" { + return true + } + return false +} + +func invalidAdminUrl(u string) bool { + parsedUrl, err := url.Parse(u) + if err != nil { + return true + } + if parsedUrl.Port() == "" || + strings.HasPrefix(parsedUrl.Host, "http://") || strings.HasPrefix(parsedUrl.Host, "https://") { + return true + } + return false +} + +func invalidCredentialConfig(c *KongGatewayConfig) bool { + user := c.Admin.Auth.BasicAuth.Username + pass := c.Admin.Auth.BasicAuth.Password + + if (user == "" && pass != "") || + (user != "" && pass == "") { + return true + } + return false +} + func ParseProperties(rootProps properties.Properties) *KongGatewayConfig { // Parse the config from bound properties and setup gateway config return &KongGatewayConfig{ Admin: KongAdminConfig{ - URL: rootProps.StringPropertyValue(cfgKongAdminURL), + Url: rootProps.StringPropertyValue(cfgKongAdminUrl), Auth: KongAdminAuthConfig{ APIKey: KongAdminAuthAPIKeyConfig{ Value: rootProps.StringPropertyValue(cfgKongAdminAPIKey), Header: rootProps.StringPropertyValue(cfgKongAdminAPIKeyHeader), }, + BasicAuth: KongAdminBasicAuthConfig{ + Username: rootProps.StringPropertyValue(cfgKongAdminUsername), + Password: rootProps.StringPropertyValue(cfgKongAdminPassword), + }, }, }, Proxy: KongProxyConfig{ Host: rootProps.StringPropertyValue(cfgKongProxyHost), - Port: KongProxyPortConfig{ + Ports: KongPortConfig{ HTTP: rootProps.IntPropertyValue(cfgKongProxyPortHttp), HTTPS: rootProps.IntPropertyValue(cfgKongProxyPortHttps), }, diff --git a/pkg/config/discovery/config_test.go b/pkg/config/discovery/config_test.go new file mode 100644 index 0000000..64c5e44 --- /dev/null +++ b/pkg/config/discovery/config_test.go @@ -0,0 +1,47 @@ +package config + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestKongGateCfg(t *testing.T) { + cfg := &KongGatewayConfig{} + + err := cfg.ValidateCfg() + assert.Equal(t, hostErr, err.Error()) + + cfg.Proxy.Host = "localhost" + err = cfg.ValidateCfg() + assert.Equal(t, proxyPortErr, err.Error()) + + cfg.Proxy.Ports.HTTP = 8000 + cfg.Proxy.Ports.HTTPS = 8443 + err = cfg.ValidateCfg() + assert.Equal(t, invalidUrlErr, err.Error()) + + cfg.Admin.Url = "sdl.com:8000" + err = cfg.ValidateCfg() + assert.Equal(t, invalidUrlErr, err.Error()) + + cfg.Admin.Url = "http://sdl.com" + err = cfg.ValidateCfg() + assert.Equal(t, invalidUrlErr, err.Error()) + + cfg.Admin.Url = "https://sds.com:8000" + cfg.Admin.Auth.BasicAuth.Username = "test" + err = cfg.ValidateCfg() + assert.Equal(t, credentialConfigErr, err.Error()) + + cfg.Admin.Auth.BasicAuth.Username = "" + cfg.Admin.Auth.BasicAuth.Password = "sas" + err = cfg.ValidateCfg() + assert.Equal(t, credentialConfigErr, err.Error()) + + cfg.Admin.Auth.BasicAuth.Password = "" + + err = cfg.ValidateCfg() + assert.Equal(t, nil, err) + +} diff --git a/pkg/gateway/client.go b/pkg/gateway/client.go index ae17b73..25de0ab 100644 --- a/pkg/gateway/client.go +++ b/pkg/gateway/client.go @@ -157,9 +157,10 @@ func (gc *Client) processSingleKongService(ctx context.Context, service *klib.Se func (gc *Client) specPreparation(ctx context.Context, route *klib.Route, service *klib.Service, spec *Openapi) { log := gc.logger.WithField(common.AttrRouteID, *route.ID). WithField(common.AttrServiceID, *service.ID) + proxyHost := gc.kongGatewayCfg.Proxy.Host - httpPort := gc.kongGatewayCfg.Proxy.Port.HTTP - httpsPort := gc.kongGatewayCfg.Proxy.Port.HTTPS + httpPort := gc.kongGatewayCfg.Proxy.Ports.HTTP + httpsPort := gc.kongGatewayCfg.Proxy.Ports.HTTPS apiPlugins, err := gc.plugins.GetEffectivePlugins(*route.ID, *service.ID) if err != nil { diff --git a/pkg/gateway/client_test.go b/pkg/gateway/client_test.go index f7c44f1..b6ba3f5 100644 --- a/pkg/gateway/client_test.go +++ b/pkg/gateway/client_test.go @@ -8,14 +8,7 @@ import ( ) func TestKongClient(t *testing.T) { - gatewayConfig := &config.KongGatewayConfig{ - Admin: config.KongAdminConfig{ - URL: "http://localhost", - }, - Proxy: config.KongProxyConfig{ - Host: "localhost", - }, - } + gatewayConfig := &config.KongGatewayConfig{} _ = config.AgentConfig{ CentralCfg: corecfg.NewCentralConfig(corecfg.DiscoveryAgent), KongGatewayCfg: gatewayConfig, diff --git a/pkg/kong/kongclient.go b/pkg/kong/kongclient.go index f2ecfe5..d0fc823 100644 --- a/pkg/kong/kongclient.go +++ b/pkg/kong/kongclient.go @@ -3,6 +3,7 @@ package kong import ( "context" "crypto/tls" + "encoding/base64" "encoding/json" "errors" "fmt" @@ -67,29 +68,34 @@ type KongClient struct { } func NewKongClient(baseClient *http.Client, kongConfig *config.KongGatewayConfig) (*KongClient, error) { - if kongConfig.Admin.Auth.APIKey.Value != "" { - defaultTransport := http.DefaultTransport.(*http.Transport) - http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true} - baseClient.Transport = defaultTransport + headers := make(http.Header) + var kongEndpoint string + defaultTransport := http.DefaultTransport.(*http.Transport) + http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{InsecureSkipVerify: true} + baseClient.Transport = defaultTransport + kongEndpoint = kongConfig.Admin.Url - headers := make(http.Header) + if kongConfig.Admin.Auth.APIKey.Value != "" { headers.Set(kongConfig.Admin.Auth.APIKey.Header, kongConfig.Admin.Auth.APIKey.Value) - client := klib.HTTPClientWithHeaders(baseClient, headers) - baseClient = client } + if kongConfig.Admin.Auth.BasicAuth.Username != "" { + headers.Set("Authorization", "Basic "+basicAuth(kongConfig.Admin.Auth.BasicAuth.Username, kongConfig.Admin.Auth.BasicAuth.Password)) + } + headers.Set("Host", kongConfig.Proxy.Host) + baseClient = klib.HTTPClientWithHeaders(baseClient, headers) logger := log.NewFieldLogger().WithComponent("client").WithPackage("kong") - - baseKongClient, err := klib.NewClient(&kongConfig.Admin.URL, baseClient) + baseKongClient, err := klib.NewClient(&kongEndpoint, baseClient) if err != nil { logger.WithError(err).Error("failed to create kong client") return nil, err } + return &KongClient{ Client: baseKongClient, logger: log.NewFieldLogger().WithComponent("KongClient").WithPackage("kong"), baseClient: baseClient, - kongAdminEndpoint: kongConfig.Admin.URL, + kongAdminEndpoint: kongEndpoint, specURLPaths: kongConfig.Spec.URLPaths, specLocalPath: kongConfig.Spec.LocalPath, devPortalEnabled: kongConfig.Spec.DevPortalEnabled, @@ -284,3 +290,8 @@ func (k KongClient) getSpec(ctx context.Context, endpoint string, fromDevPortal func (k KongClient) GetKongPlugins() *Plugins { return &Plugins{PluginLister: k.Plugins} } + +func basicAuth(username, password string) string { + auth := username + ":" + password + return base64.StdEncoding.EncodeToString([]byte(auth)) +} diff --git a/pkg/kong/provisioning_test.go b/pkg/kong/provisioning_test.go index fcccc07..9903c7f 100644 --- a/pkg/kong/provisioning_test.go +++ b/pkg/kong/provisioning_test.go @@ -6,6 +6,8 @@ import ( "fmt" "net/http" "net/http/httptest" + "net/url" + "strconv" "testing" klib "github.com/kong/go-kong/kong" @@ -86,11 +88,23 @@ func createClient(responses map[string]response) KongAPIClient { return } })) + u, _ := url.Parse(s.URL) + port, _ := strconv.Atoi(u.Port()) cfg := &config.KongGatewayConfig{ + Proxy: config.KongProxyConfig{ + Host: u.Hostname(), + Ports: config.KongPortConfig{ + HTTP: port, + HTTPS: port, + }, + }, Admin: config.KongAdminConfig{ - URL: s.URL, + Url: s.URL, }, } + if err := cfg.ValidateCfg(); err != nil { + panic(err) + } client, _ := NewKongClient(&http.Client{}, cfg) return client } diff --git a/sample_env_vars.env b/sample_env_vars.env new file mode 100644 index 0000000..d30e9f2 --- /dev/null +++ b/sample_env_vars.env @@ -0,0 +1,26 @@ +CENTRAL_ENVIRONMENT="kong-env-name", +CENTRAL_ORGANIZATIONID="28123456789", +CENTRAL_PLATFORM_URL="central_platform_url", +CENTRAL_POLLINTERVAL="30m", +CENTRAL_TEAM="Default Team", +CENTRAL_URL="central_url", +CENTRAL_GRPC_ENABLED="true", +CENTRAL_AUTH_CLIENTID="central_auth_clientid", +CENTRAL_AUTH_PRIVATEKEY="/path/to/private_key.pem", +CENTRAL_AUTH_PUBLICKEY="/path/to/public_key.pem", +CENTRAL_AUTH_URL="central_auth_url", +LOG_LEVEL="info", +LOG_FORMAT="json", +LOG_OUTPUT="stdout", +LOG_PATH="logs", +KONG_PROXY_HOST="kong.host.name.where.gateway.lives", +KONG_PROXY_PORTS_HTTP="8000", +KONG_PROXY_PORTS_HTTPS="8443", +KONG_ADMIN_ROUTEPATH="/admin-api", +KONG_ADMIN_AUTH_BASICAUTH_USERNAME="gh", +KONG_ADMIN_AUTH_BASICAUTH_PASSWORD="12", +KONG_ADMIN_AUTH_APIKEY_HEADER="apikey", +KONG_ADMIN_AUTH_APIKEY_VALUE="12", +KONG_SPEC_LOCALPATH="/local/path/to/specs", +KONG_SPEC_URLPATHS="openapi.json,swagger.json", +AGENTFEATURES_MARKETPLACEPROVISIONING="true" \ No newline at end of file