diff --git a/.secrets.baseline b/.secrets.baseline index 2154fd0e55..3bbbb4e1ce 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -377,63 +377,63 @@ "filename": "templates/service-template.yml", "hashed_secret": "13032f402fed753c2248419ea4f69f99931f6dbc", "is_verified": false, - "line_number": 511 + "line_number": 516 }, { "type": "Base64 High Entropy String", "filename": "templates/service-template.yml", "hashed_secret": "30025f80f6e22cdafb85db387d50f90ea884576a", "is_verified": false, - "line_number": 511 + "line_number": 516 }, { "type": "Base64 High Entropy String", "filename": "templates/service-template.yml", "hashed_secret": "355f24fd038bcaf85617abdcaa64af51ed19bbcf", "is_verified": false, - "line_number": 511 + "line_number": 516 }, { "type": "Base64 High Entropy String", "filename": "templates/service-template.yml", "hashed_secret": "3d8a1dcd2c3c765ce35c9a9552d23273cc4ddace", "is_verified": false, - "line_number": 511 + "line_number": 516 }, { "type": "Base64 High Entropy String", "filename": "templates/service-template.yml", "hashed_secret": "4ac7b0522761eba972467942cd5cd7499dd2c361", "is_verified": false, - "line_number": 511 + "line_number": 516 }, { "type": "Base64 High Entropy String", "filename": "templates/service-template.yml", "hashed_secret": "7639ab2a6bcf2ea30a055a99468c9cd844d4c22a", "is_verified": false, - "line_number": 511 + "line_number": 516 }, { "type": "Base64 High Entropy String", "filename": "templates/service-template.yml", "hashed_secret": "b56360daf4793d2a74991a972b34d95bc00fb2da", "is_verified": false, - "line_number": 511 + "line_number": 516 }, { "type": "Base64 High Entropy String", "filename": "templates/service-template.yml", "hashed_secret": "c9a73ef9ee8ce9f38437227801c70bcc6740d1a1", "is_verified": false, - "line_number": 511 + "line_number": 516 }, { "type": "Secret Keyword", "filename": "templates/service-template.yml", "hashed_secret": "4e199b4a1c40b497a95fcd1cd896351733849949", "is_verified": false, - "line_number": 695, + "line_number": 700, "is_secret": false } ], diff --git a/Makefile b/Makefile index c90d88cbba..9e6a1d61a9 100644 --- a/Makefile +++ b/Makefile @@ -52,7 +52,6 @@ DOCKER_CONFIG ?= "${HOME}/.docker" # Default Variables ENABLE_OCM_MOCK ?= true OCM_MOCK_MODE ?= emulate-server -JWKS_URL ?= "https://sso.redhat.com/auth/realms/redhat-external/protocol/openid-connect/certs" GITOPS_CONFIG_FILE ?= ${PROJECT_PATH}/dev/config/gitops-config.yaml DATAPLANE_CLUSTER_CONFIG_FILE ?= ${PROJECT_PATH}/dev/config/dataplane-cluster-configuration.yaml PROVIDERS_CONFIG_FILE ?= ${PROJECT_PATH}/dev/config/provider-configuration.yaml @@ -444,7 +443,9 @@ code/fix: .PHONY: code/fix run: fleet-manager db/migrate - ./fleet-manager serve --dataplane-cluster-config-file $(DATAPLANE_CLUSTER_CONFIG_FILE) + ./fleet-manager serve \ + --dataplane-cluster-config-file $(DATAPLANE_CLUSTER_CONFIG_FILE) \ + --dataplane-oidc-issuers-file ./dev/config/dataplane-oidc-issuers.yaml .PHONY: run # Run Swagger and host the api docs @@ -713,7 +714,7 @@ deploy/db: # deploys the secrets required by the service to an OpenShift cluster deploy/secrets: @oc process -f ./templates/secrets-template.yml --local \ - -p DATABASE_HOST="fleet-manager-db.$(NAMESPACE).svc.cluster.local" \ + -p DATABASE_HOST="fleet-manager-db" \ -p OCM_SERVICE_CLIENT_ID="$(shell ([ -s './secrets/ocm-service.clientId' ] && [ -z '${OCM_SERVICE_CLIENT_ID}' ]) && cat ./secrets/ocm-service.clientId || echo '${OCM_SERVICE_CLIENT_ID}')" \ -p OCM_SERVICE_CLIENT_SECRET="$(shell ([ -s './secrets/ocm-service.clientSecret' ] && [ -z '${OCM_SERVICE_CLIENT_SECRET}' ]) && cat ./secrets/ocm-service.clientSecret || echo '${OCM_SERVICE_CLIENT_SECRET}')" \ -p OCM_SERVICE_TOKEN="$(shell ([ -s './secrets/ocm-service.token' ] && [ -z '${OCM_SERVICE_TOKEN}' ]) && cat ./secrets/ocm-service.token || echo '${OCM_SERVICE_TOKEN}')" \ @@ -763,7 +764,7 @@ endif | oc apply -f - -n $(NAMESPACE) .PHONY: deploy/gitops -# deploy service via templates to an OpenShift cluster +# deploy service via templates to a development Kubernetes/OpenShift cluster deploy/service: FLEET_MANAGER_IMAGE ?= $(SHORT_IMAGE_REF) deploy/service: IMAGE_TAG ?= $(image_tag) deploy/service: FLEET_MANAGER_ENV ?= "development" @@ -773,7 +774,6 @@ deploy/service: ENABLE_CENTRAL_LIFE_SPAN ?= "false" deploy/service: CENTRAL_LIFE_SPAN ?= "48" deploy/service: OCM_URL ?= "https://api.stage.openshift.com" deploy/service: OCM_ADDON_SERVICE_URL ?= "https://api.stage.openshift.com" -deploy/service: TOKEN_ISSUER_URL ?= "https://sso.redhat.com/auth/realms/redhat-external" deploy/service: SERVICE_PUBLIC_HOST_URL ?= "https://api.openshift.com" deploy/service: ENABLE_TERMS_ACCEPTANCE ?= "false" deploy/service: ENABLE_DENY_LIST ?= "false" @@ -813,8 +813,6 @@ endif -p OCM_URL="$(OCM_URL)" \ -p OCM_ADDON_SERVICE_URL="$(OCM_ADDON_SERVICE_URL)" \ -p AMS_URL="${AMS_URL}" \ - -p JWKS_URL="$(JWKS_URL)" \ - -p TOKEN_ISSUER_URL="${TOKEN_ISSUER_URL}" \ -p SERVICE_PUBLIC_HOST_URL="https://$(shell oc get routes/fleet-manager -o jsonpath="{.spec.host}" -n $(NAMESPACE))" \ -p OBSERVATORIUM_RHSSO_GATEWAY="${OBSERVATORIUM_RHSSO_GATEWAY}" \ -p OBSERVATORIUM_RHSSO_REALM="${OBSERVATORIUM_RHSSO_REALM}" \ diff --git a/config/dataplane-oidc-issuers.yaml b/config/dataplane-oidc-issuers.yaml new file mode 100644 index 0000000000..b1679dc753 --- /dev/null +++ b/config/dataplane-oidc-issuers.yaml @@ -0,0 +1,4 @@ +--- +# A list of DataPlane OpenID Connect issuers that should be verified for issued tokens. +# Endpoints in the list are represented by URLs that must match the iss claim in the access token. +[] diff --git a/dev/config/dataplane-oidc-issuers.yaml b/dev/config/dataplane-oidc-issuers.yaml new file mode 100644 index 0000000000..68761d509b --- /dev/null +++ b/dev/config/dataplane-oidc-issuers.yaml @@ -0,0 +1 @@ +- https://127.0.0.1:6443 diff --git a/go.mod b/go.mod index aa80de7940..701958ffe3 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,6 @@ go 1.20 require ( github.com/DATA-DOG/go-sqlmock v1.5.2 github.com/antihax/optional v1.0.0 - github.com/auth0/go-jwt-middleware/v2 v2.2.1 github.com/aws/aws-sdk-go v1.51.15 github.com/bxcodec/faker/v3 v3.8.1 github.com/caarlos0/env/v6 v6.10.1 diff --git a/go.sum b/go.sum index 082b9d18da..c05fc7f143 100644 --- a/go.sum +++ b/go.sum @@ -74,8 +74,6 @@ github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAE github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8/go.mod h1:I0gYDMZ6Z5GRU7l58bNFSkPTFN6Yl12dsUlAZ8xy98g= github.com/antihax/optional v1.0.0 h1:xK2lYat7ZLaVVcIuj82J8kIro4V6kDe0AUDFboUCwcg= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= -github.com/auth0/go-jwt-middleware/v2 v2.2.1 h1:pqxEIwlCztD0T9ZygGfOrw4NK/F9iotnCnPJVADKbkE= -github.com/auth0/go-jwt-middleware/v2 v2.2.1/go.mod h1:CSi0tuu0QrALbWdiQZwqFL8SbBhj4e2MJzkvNfjY0Us= github.com/aws/aws-sdk-go v1.51.15 h1:rxRcn4hmkhxUfIQrmnFfOOW4NQRiRve7GlgQcor13JE= github.com/aws/aws-sdk-go v1.51.15/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk= github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= @@ -1067,7 +1065,6 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/go-jose/go-jose.v2 v2.6.2 h1:Rl5+9rA0kG3vsO1qhncMPRT5eHICihAMQYJkD7u/i4M= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/resty.v1 v1.12.0 h1:CuXP0Pjfw9rOuY6EP+UvtNvt5DSqHpIxILZKT/quCZI= diff --git a/internal/dinosaur/pkg/handlers/authentication.go b/internal/dinosaur/pkg/handlers/authentication.go index 3a8396e2d6..76e71f8460 100644 --- a/internal/dinosaur/pkg/handlers/authentication.go +++ b/internal/dinosaur/pkg/handlers/authentication.go @@ -2,47 +2,106 @@ package handlers import ( "fmt" + "net/http" + "strings" "github.com/golang/glog" sdk "github.com/openshift-online/ocm-sdk-go" "github.com/openshift-online/ocm-sdk-go/authentication" - pkgErrors "github.com/pkg/errors" "github.com/stackrox/acs-fleet-manager/internal/dinosaur/routes" "github.com/stackrox/acs-fleet-manager/pkg/client/iam" "github.com/stackrox/acs-fleet-manager/pkg/errors" - "github.com/stackrox/acs-fleet-manager/pkg/server" ) -// NewAuthenticationBuilder ... -func NewAuthenticationBuilder(ServerConfig *server.ServerConfig, IAMConfig *iam.IAMConfig) (*authentication.HandlerBuilder, error) { +type compositeAuthenticationHandler struct { + defaultHandler http.Handler + privateAPIHandler http.Handler + adminAPIHandler http.Handler +} + +var ( + adminAPIPrefix = fmt.Sprintf("^%s/%s/%s%s", routes.APIEndpoint, routes.FleetManagementAPIPrefix, routes.Version, routes.AdminAPIPrefix) + privateAPIPrefix = fmt.Sprintf("^%s/%s/%s%s", routes.APIEndpoint, routes.FleetManagementAPIPrefix, routes.Version, routes.PrivateAPIPrefix) +) + +func (h *compositeAuthenticationHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if strings.HasPrefix(r.URL.Path, adminAPIPrefix) { + h.adminAPIHandler.ServeHTTP(w, r) + return + } + if strings.HasPrefix(r.URL.Path, privateAPIPrefix) { + h.privateAPIHandler.ServeHTTP(w, r) + return + } + h.defaultHandler.ServeHTTP(w, r) +} +// NewAuthenticationHandler creates a new instance of authentication handler +func NewAuthenticationHandler(IAMConfig *iam.IAMConfig, next http.Handler) (http.Handler, error) { authnLogger, err := sdk.NewGlogLoggerBuilder(). InfoV(glog.Level(1)). DebugV(glog.Level(5)). Build() if err != nil { - return nil, pkgErrors.Wrap(err, "unable to create authentication logger") + return nil, fmt.Errorf("unable to create authentication logger: %w", err) } - authenticationBuilder := authentication.NewHandler() + defaultHandlerBuilder := authentication.NewHandler(). + Logger(authnLogger). + KeysURL(IAMConfig.JwksURL). // ocm JWK JSON web token signing certificates URL + KeysFile(IAMConfig.JwksFile). // ocm JWK backup JSON web token signing certificates + KeysURL(IAMConfig.RedhatSSORealm.JwksEndpointURI). // sso JWK Cert URL + Error(fmt.Sprint(errors.ErrorUnauthenticated)). + Service(errors.ErrorCodePrefix). + Public(fmt.Sprintf("^%s/%s/?$", routes.APIEndpoint, routes.FleetManagementAPIPrefix)). + Public(fmt.Sprintf("^%s/%s/%s/?$", routes.APIEndpoint, routes.FleetManagementAPIPrefix, routes.Version)). + Public(fmt.Sprintf("^%s/%s/%s/openapi/?$", routes.APIEndpoint, routes.FleetManagementAPIPrefix, routes.Version)). + Public(fmt.Sprintf("^%s/%s/%s/errors/?[0-9]*", routes.APIEndpoint, routes.FleetManagementAPIPrefix, routes.Version)) // Add additional JWKS endpoints to the builder if there are any. for _, jwksEndpointURI := range IAMConfig.AdditionalSSOIssuers.JWKSURIs { - authenticationBuilder.KeysURL(jwksEndpointURI) - } - - return authenticationBuilder. - Logger(authnLogger). - KeysURL(ServerConfig.JwksURL). // ocm JWK JSON web token signing certificates URL - KeysFile(ServerConfig.JwksFile). // ocm JWK backup JSON web token signing certificates - KeysURL(IAMConfig.RedhatSSORealm.JwksEndpointURI). // sso JWK Cert URL - KeysURL(IAMConfig.InternalSSORealm.JwksEndpointURI). // internal sso (auth.redhat.com) JWK Cert URL - Error(fmt.Sprint(errors.ErrorUnauthenticated)). - Service(errors.ErrorCodePrefix). - Public(fmt.Sprintf("^%s/%s/?$", routes.APIEndpoint, routes.DinosaursFleetManagementAPIPrefix)). - Public(fmt.Sprintf("^%s/%s/%s/?$", routes.APIEndpoint, routes.DinosaursFleetManagementAPIPrefix, routes.Version)). - Public(fmt.Sprintf("^%s/%s/%s/openapi/?$", routes.APIEndpoint, routes.DinosaursFleetManagementAPIPrefix, routes.Version)). - Public(fmt.Sprintf("^%s/%s/%s/errors/?[0-9]*", routes.APIEndpoint, routes.DinosaursFleetManagementAPIPrefix, routes.Version)), - nil + defaultHandlerBuilder.KeysURL(jwksEndpointURI) + } + + defaultHandler, err := defaultHandlerBuilder.Next(next).Build() + if err != nil { + return nil, fmt.Errorf("unable to create default authN handler: %w", err) + } + + privateAPIHandlerBuilder := authentication.NewHandler(). + Logger(authnLogger). + KeysURL(IAMConfig.RedhatSSORealm.JwksEndpointURI). + Error(fmt.Sprint(errors.ErrorUnauthenticated)). + Service(errors.ErrorCodePrefix) + + // Add additional JWKS endpoints to the builder if there are any. + for _, jwksEndpointURI := range IAMConfig.DataPlaneOIDCIssuers.JWKSURIs { + privateAPIHandlerBuilder.KeysURL(jwksEndpointURI) + } + + privateAPIHandler, err := privateAPIHandlerBuilder.Next(next).Build() + if err != nil { + return nil, fmt.Errorf("unable to create private authN handler: %w", err) + } + + adminAPIHandler, err := authentication.NewHandler(). + Logger(authnLogger). + KeysURL(IAMConfig.JwksURL). // ocm JWK JSON web token signing certificates URL + KeysFile(IAMConfig.JwksFile). // ocm JWK backup JSON web token signing certificates + KeysURL(IAMConfig.InternalSSORealm.JwksEndpointURI). // internal sso (auth.redhat.com) JWK Cert URL + Error(fmt.Sprint(errors.ErrorUnauthenticated)). + Service(errors.ErrorCodePrefix). + Next(next). + Build() + + if err != nil { + return nil, fmt.Errorf("unable to create admin authN handler: %w", err) + } + + return &compositeAuthenticationHandler{ + defaultHandler: defaultHandler, + privateAPIHandler: privateAPIHandler, + adminAPIHandler: adminAPIHandler, + }, nil } diff --git a/internal/dinosaur/pkg/routes/route_loader.go b/internal/dinosaur/pkg/routes/route_loader.go index d5275d29eb..765960d44a 100644 --- a/internal/dinosaur/pkg/routes/route_loader.go +++ b/internal/dinosaur/pkg/routes/route_loader.go @@ -67,7 +67,7 @@ func NewRouteLoader(s options) environments.RouteLoader { // AddRoutes ... func (s *options) AddRoutes(mainRouter *mux.Router) error { - basePath := fmt.Sprintf("%s/%s", routes.APIEndpoint, routes.DinosaursFleetManagementAPIPrefix) + basePath := fmt.Sprintf("%s/%s", routes.APIEndpoint, routes.FleetManagementAPIPrefix) err := s.buildAPIBaseRouter(mainRouter, basePath, "fleet-manager.yaml") if err != nil { return err @@ -93,7 +93,7 @@ func (s *options) buildAPIBaseRouter(mainRouter *mux.Router, basePath string, op authorizeMiddleware := s.AccessControlListMiddleware.Authorize requireOrgID := auth.NewRequireOrgIDMiddleware().RequireOrgID(errors.ErrorUnauthenticated) requireIssuer := auth.NewRequireIssuerMiddleware().RequireIssuer( - append(s.IAMConfig.AdditionalSSOIssuers.GetURIs(), s.ServerConfig.TokenIssuerURL), errors.ErrorUnauthenticated) + append(s.IAMConfig.AdditionalSSOIssuers.GetURIs(), s.IAMConfig.RedhatSSORealm.ValidIssuerURI), errors.ErrorUnauthenticated) requireTermsAcceptance := auth.NewRequireTermsAcceptanceMiddleware().RequireTermsAcceptance(s.ServerConfig.EnableTermsAcceptance, s.AMSClient, errors.ErrorTermsNotAccepted) // base path. @@ -157,8 +157,7 @@ func (s *options) buildAPIBaseRouter(mainRouter *mux.Router, basePath string, op Name(logger.NewLogEvent("get-federate-metrics", "get federate metrics by id").ToString()). Methods(http.MethodGet) apiV1MetricsFederateRouter.Use(auth.NewRequireIssuerMiddleware().RequireIssuer( - append(s.IAMConfig.AdditionalSSOIssuers.GetURIs(), s.ServerConfig.TokenIssuerURL, - s.IAMConfig.RedhatSSORealm.ValidIssuerURI), errors.ErrorUnauthenticated)) + append(s.IAMConfig.AdditionalSSOIssuers.GetURIs(), s.IAMConfig.RedhatSSORealm.ValidIssuerURI), errors.ErrorUnauthenticated)) apiV1MetricsFederateRouter.Use(requireOrgID) apiV1MetricsFederateRouter.Use(authorizeMiddleware) @@ -200,7 +199,7 @@ func (s *options) buildAPIBaseRouter(mainRouter *mux.Router, basePath string, op // /agent-clusters/{id} dataPlaneClusterHandler := handlers.NewDataPlaneClusterHandler(s.DataPlaneCluster) dataPlaneCentralHandler := handlers.NewDataPlaneDinosaurHandler(s.DataPlaneCentralService, s.Central, s.ManagedCentralPresenter, s.GitopsProvider) - apiV1DataPlaneRequestsRouter := apiV1Router.PathPrefix("/agent-clusters").Subrouter() + apiV1DataPlaneRequestsRouter := apiV1Router.PathPrefix(routes.PrivateAPIPrefix).Subrouter() apiV1DataPlaneRequestsRouter.HandleFunc("/{id}", dataPlaneClusterHandler.GetDataPlaneClusterConfig). Name(logger.NewLogEvent("get-dataplane-cluster-config", "get dataplane cluster config by id").ToString()). Methods(http.MethodGet) @@ -225,7 +224,7 @@ func (s *options) buildAPIBaseRouter(mainRouter *mux.Router, basePath string, op s.IAMConfig.RedhatSSORealm.ValidIssuerURI, s.FleetShardAuthZConfig) adminCentralHandler := handlers.NewAdminCentralHandler(s.Central, s.AccountService, s.ProviderConfig, s.Telemetry) - adminRouter := apiV1Router.PathPrefix("/admin").Subrouter() + adminRouter := apiV1Router.PathPrefix(routes.AdminAPIPrefix).Subrouter() adminRouter.Use(auth.NewRequireIssuerMiddleware().RequireIssuer( []string{s.IAMConfig.InternalSSORealm.ValidIssuerURI}, errors.ErrorNotFound)) diff --git a/internal/dinosaur/providers.go b/internal/dinosaur/providers.go index c1add568b5..7d24385ff9 100644 --- a/internal/dinosaur/providers.go +++ b/internal/dinosaur/providers.go @@ -7,7 +7,6 @@ import ( "github.com/stackrox/acs-fleet-manager/internal/dinosaur/pkg/config" "github.com/stackrox/acs-fleet-manager/internal/dinosaur/pkg/environments" "github.com/stackrox/acs-fleet-manager/internal/dinosaur/pkg/gitops" - "github.com/stackrox/acs-fleet-manager/internal/dinosaur/pkg/handlers" "github.com/stackrox/acs-fleet-manager/internal/dinosaur/pkg/migrations" "github.com/stackrox/acs-fleet-manager/internal/dinosaur/pkg/presenters" "github.com/stackrox/acs-fleet-manager/internal/dinosaur/pkg/routes" @@ -63,7 +62,6 @@ func ServiceProviders() di.Option { di.Provide(services.NewClusterPlacementStrategy), di.Provide(services.NewDataPlaneClusterService), di.Provide(services.NewDataPlaneCentralService), - di.Provide(handlers.NewAuthenticationBuilder), di.Provide(clusters.NewDefaultProviderFactory, di.As(new(clusters.ProviderFactory))), di.Provide(routes.NewRouteLoader), di.Provide(quota.NewDefaultQuotaServiceFactory), diff --git a/internal/dinosaur/routes/constants.go b/internal/dinosaur/routes/constants.go index ee6b610dbe..2c16d5de46 100644 --- a/internal/dinosaur/routes/constants.go +++ b/internal/dinosaur/routes/constants.go @@ -3,7 +3,9 @@ package routes // Version ... const ( - Version = "v1" - APIEndpoint = "/api" - DinosaursFleetManagementAPIPrefix = "rhacs" + Version = "v1" + APIEndpoint = "/api" + FleetManagementAPIPrefix = "rhacs" + PrivateAPIPrefix = "/agent-clusters" + AdminAPIPrefix = "/admin" ) diff --git a/pkg/acl/access_control_list_middleware_test.go b/pkg/acl/access_control_list_middleware_test.go index a08b4126a3..a717477f1b 100644 --- a/pkg/acl/access_control_list_middleware_test.go +++ b/pkg/acl/access_control_list_middleware_test.go @@ -12,10 +12,9 @@ import ( "github.com/golang/glog" "github.com/stackrox/acs-fleet-manager/internal/dinosaur" "github.com/stackrox/acs-fleet-manager/pkg/acl" - "github.com/stackrox/acs-fleet-manager/pkg/server" - "github.com/stackrox/acs-fleet-manager/pkg/auth" "github.com/stackrox/acs-fleet-manager/pkg/environments" + "github.com/stackrox/acs-fleet-manager/pkg/server" . "github.com/onsi/gomega" ) @@ -42,7 +41,7 @@ func TestMain(m *testing.M) { func Test_AccessControlListMiddleware_UserHasNoAccess(t *testing.T) { RegisterTestingT(t) - authHelper, err := auth.NewAuthHelper(jwtKeyFile, jwtCAFile, serverConfig.TokenIssuerURL) + authHelper, err := auth.NewAuthHelper(jwtKeyFile, jwtCAFile, "") Expect(err).NotTo(HaveOccurred()) tests := []struct { diff --git a/pkg/client/iam/config.go b/pkg/client/iam/config.go index 1dc7bf279b..f7f85d5f11 100644 --- a/pkg/client/iam/config.go +++ b/pkg/client/iam/config.go @@ -6,35 +6,47 @@ import ( "io" "io/fs" "net/http" + "net/url" + "os" + "path/filepath" "strings" "time" "github.com/golang/glog" "github.com/pkg/errors" - "github.com/stackrox/acs-fleet-manager/pkg/shared" - "github.com/spf13/pflag" + "github.com/stackrox/acs-fleet-manager/pkg/shared" + "github.com/stackrox/rox/pkg/netutil" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" ) // IAMConfig ... type IAMConfig struct { - SsoBaseURL string `json:"sso_base_url"` - InternalSsoBaseURL string `json:"internal_sso_base_url"` - RedhatSSORealm *IAMRealmConfig `json:"redhat_sso_config"` - InternalSSORealm *IAMRealmConfig `json:"internal_sso_config"` - AdditionalSSOIssuers *AdditionalSSOIssuers `json:"-"` + JwksURL string + JwksFile string + SsoBaseURL string + InternalSsoBaseURL string + RedhatSSORealm *IAMRealmConfig + InternalSSORealm *IAMRealmConfig + AdditionalSSOIssuers *OIDCIssuers + DataPlaneOIDCIssuers *OIDCIssuers } -// AdditionalSSOIssuers ... -type AdditionalSSOIssuers struct { - URIs []string +// OIDCIssuers is a list of issuers that the Fleet Manager server trusts. +type OIDCIssuers struct { + // URIs the list of issuer uris + URIs []string + // JWKSURIs the list of JWKSs uris derived from URIs JWKSURIs []string - File string - Enabled bool + // File location of the file from which the URIs will be read + File string + // Enabled add to the server configuration if true + Enabled bool } // GetURIs returns copy of URIs to protect config from modifications. -func (a *AdditionalSSOIssuers) GetURIs() []string { +func (a *OIDCIssuers) GetURIs() []string { uris := make([]string, 0, len(a.URIs)) copy(uris, a.URIs) return uris @@ -94,6 +106,8 @@ func (c *IAMRealmConfig) validateConfiguration() error { // NewIAMConfig ... func NewIAMConfig() *IAMConfig { kc := &IAMConfig{ + JwksURL: "https://sso.redhat.com/auth/realms/redhat-external/protocol/openid-connect/certs", + JwksFile: "config/jwks-file.json", SsoBaseURL: "https://sso.redhat.com", RedhatSSORealm: &IAMRealmConfig{ APIEndpointURI: "/auth/realms/redhat-external", @@ -107,23 +121,32 @@ func NewIAMConfig() *IAMConfig { Realm: "EmployeeIDP", }, InternalSsoBaseURL: "https://auth.redhat.com", - AdditionalSSOIssuers: &AdditionalSSOIssuers{}, + AdditionalSSOIssuers: &OIDCIssuers{}, + DataPlaneOIDCIssuers: &OIDCIssuers{ + Enabled: true, + File: "config/dataplane-oidc-issuers.yaml", + }, } return kc } // AddFlags ... func (ic *IAMConfig) AddFlags(fs *pflag.FlagSet) { + fs.StringVar(&ic.JwksURL, "jwks-url", ic.JwksURL, "The URL of the JSON web token signing certificates.") + fs.StringVar(&ic.JwksFile, "jwks-file", ic.JwksFile, "File containing the the JSON web token signing certificates.") fs.StringVar(&ic.RedhatSSORealm.ClientIDFile, "redhat-sso-client-id-file", ic.RedhatSSORealm.ClientIDFile, "File containing IAM privileged account client-id that has access to the OSD Cluster IDP realm") fs.StringVar(&ic.RedhatSSORealm.ClientSecretFile, "redhat-sso-client-secret-file", ic.RedhatSSORealm.ClientSecretFile, "File containing IAM privileged account client-secret that has access to the OSD Cluster IDP realm") fs.StringVar(&ic.SsoBaseURL, "redhat-sso-base-url", ic.SsoBaseURL, "The base URL of the SSO, integration by default") + fs.StringVar(&ic.InternalSsoBaseURL, "internal-sso-base-url", ic.InternalSsoBaseURL, "The base URL of the internal SSO, production by default") fs.BoolVar(&ic.AdditionalSSOIssuers.Enabled, "enable-additional-sso-issuers", ic.AdditionalSSOIssuers.Enabled, "Enable additional SSO issuer URIs for verifying tokens") fs.StringVar(&ic.AdditionalSSOIssuers.File, "additional-sso-issuers-file", ic.AdditionalSSOIssuers.File, "File containing a list of SSO issuer URIs to include for verifying tokens") - fs.StringVar(&ic.InternalSsoBaseURL, "internal-sso-base-url", ic.InternalSsoBaseURL, "The base URL of the internal SSO, production by default") + fs.StringVar(&ic.DataPlaneOIDCIssuers.File, "dataplane-oidc-issuers-file", ic.DataPlaneOIDCIssuers.File, "File containing a list of OIDC issuer URIs to include for verifying tokens") } // ReadFiles ... func (ic *IAMConfig) ReadFiles() error { + ic.JwksFile = shared.BuildFullFilePath(ic.JwksFile) + err := shared.ReadFileValueString(ic.RedhatSSORealm.ClientIDFile, &ic.RedhatSSORealm.ClientID) if err != nil { return fmt.Errorf("reading Red Hat SSO Realm ClientID file %q: %w", ic.RedhatSSORealm.ClientIDFile, err) @@ -146,7 +169,7 @@ func (ic *IAMConfig) ReadFiles() error { // Read the additional issuers file. This will add additional SSO issuer URIs which shall be used as valid issuers // for tokens, i.e. sso.stage.redhat.com. if ic.AdditionalSSOIssuers.Enabled { - err = readAdditionalIssuersFile(ic.AdditionalSSOIssuers.File, ic.AdditionalSSOIssuers) + err = readIssuersFile(ic.AdditionalSSOIssuers.File, ic.AdditionalSSOIssuers) if err != nil { if errors.Is(err, fs.ErrNotExist) { glog.V(10).Infof("Specified additional SSO issuers file %q does not exist. "+ @@ -159,24 +182,30 @@ func (ic *IAMConfig) ReadFiles() error { return err } } - - return nil + if err := readIssuersFile(ic.DataPlaneOIDCIssuers.File, ic.DataPlaneOIDCIssuers); err != nil { + return err + } + return ic.DataPlaneOIDCIssuers.resolveURIs() } const ( openidConfigurationPath = "/.well-known/openid-configuration" + kubernetesIssuer = "https://kubernetes.default.svc" ) type openIDConfiguration struct { JwksURI string `json:"jwks_uri"` } -// setJWKSURIs will set the jwks URIs by taking the issuer URI and fetching the openid-configuration, setting the +// resolveURIs will set the jwks URIs by taking the issuer URI and fetching the openid-configuration, setting the // jwks URI dynamically -func (a *AdditionalSSOIssuers) resolveURIs() error { - client := http.Client{Timeout: time.Minute} +func (a *OIDCIssuers) resolveURIs() error { jwksURIs := make([]string, 0, len(a.URIs)) for _, issuerURI := range a.URIs { + client, err := createHTTPClient(issuerURI) + if err != nil { + return err + } cfg, err := getOpenIDConfiguration(client, issuerURI) if err != nil { return errors.Wrapf(err, "retrieving open-id configuration for %q", issuerURI) @@ -190,7 +219,51 @@ func (a *AdditionalSSOIssuers) resolveURIs() error { return nil } -func getOpenIDConfiguration(c http.Client, baseURL string) (*openIDConfiguration, error) { +func createHTTPClient(url string) (*http.Client, error) { + // Special case for dev/test environments: Fleet Manager runs on the Data Plane cluster + if url == kubernetesIssuer { + config, err := rest.InClusterConfig() + if err != nil { + return nil, fmt.Errorf("create in-cluster k8s config: %w", err) + } + client, err := rest.HTTPClientFor(config) + if err != nil { + return nil, fmt.Errorf("create http client for in-cluster k8s issuer: %w", err) + } + return client, nil + } + // Special case for local dev environments: Fleet Manager manages a local cluster, assuming kubeconfig exists + if isLocalCluster(url) { + kubeconfig := os.Getenv("KUBECONFIG") + if len(kubeconfig) == 0 { + kubeconfig = filepath.Join(os.Getenv("HOME"), ".kube/config") + } + config, err := clientcmd.BuildConfigFromFlags(url, kubeconfig) + if err != nil { + return nil, fmt.Errorf("create local cluster k8s config: %w", err) + } + client, err := rest.HTTPClientFor(config) + if err != nil { + return nil, fmt.Errorf("create http client for local k8s issuer") + } + return client, nil + } + // default client + return &http.Client{ + Timeout: time.Minute, + }, nil +} + +func isLocalCluster(uri string) bool { + url, err := url.Parse(uri) + if err != nil { + glog.V(10).Infof("Unable to parse the issuer URI %v, consider it a non-local cluster", uri) + return false + } + return netutil.IsLocalHost(url.Hostname()) +} + +func getOpenIDConfiguration(c *http.Client, baseURL string) (*openIDConfiguration, error) { url := strings.TrimRight(baseURL, "/") + openidConfigurationPath resp, err := c.Get(url) if err != nil { @@ -214,7 +287,7 @@ func getOpenIDConfiguration(c http.Client, baseURL string) (*openIDConfiguration return &cfg, nil } -func readAdditionalIssuersFile(file string, endpoints *AdditionalSSOIssuers) error { +func readIssuersFile(file string, endpoints *OIDCIssuers) error { var issuers []string if err := shared.ReadYamlFile(file, &issuers); err != nil { return fmt.Errorf("reading from yaml file: %w", err) diff --git a/pkg/server/api_server.go b/pkg/server/api_server.go index def648efc0..fb59ae1cc8 100644 --- a/pkg/server/api_server.go +++ b/pkg/server/api_server.go @@ -8,20 +8,13 @@ import ( "net/http" "time" + "github.com/goava/di" + "github.com/stackrox/acs-fleet-manager/internal/dinosaur/pkg/handlers" "github.com/stackrox/acs-fleet-manager/pkg/client/iam" "github.com/stackrox/acs-fleet-manager/pkg/environments" - - "github.com/goava/di" "github.com/stackrox/acs-fleet-manager/pkg/server/logging" "github.com/stackrox/acs-fleet-manager/pkg/services/sentry" - "github.com/openshift-online/ocm-sdk-go/authentication" - - // TODO why is this imported? - _ "github.com/auth0/go-jwt-middleware/v2" - // TODO why is this imported? - _ "github.com/golang-jwt/jwt/v4" - sentryhttp "github.com/getsentry/sentry-go/http" "github.com/golang/glog" gorillahandlers "github.com/gorilla/handlers" @@ -97,11 +90,9 @@ func NewAPIServer(options ServerOptions) *APIServer { // referring to the router as type http.Handler allows us to add middleware via more handlers var mainHandler http.Handler = mainRouter - var builder *authentication.HandlerBuilder - options.Env.MustResolve(&builder) var err error - mainHandler, err = builder.Next(mainHandler).Build() + mainHandler, err = handlers.NewAuthenticationHandler(options.IAMConfig, mainHandler) check(err, "Unable to create authentication handler", options.SentryConfig.Timeout) mainHandler = gorillahandlers.CORS( diff --git a/pkg/server/server_config.go b/pkg/server/server_config.go index 70a45cc4cd..27d02dc52b 100644 --- a/pkg/server/server_config.go +++ b/pkg/server/server_config.go @@ -2,18 +2,14 @@ package server import ( "github.com/spf13/pflag" - "github.com/stackrox/acs-fleet-manager/pkg/shared" ) // ServerConfig ... type ServerConfig struct { - BindAddress string `json:"bind_address"` - HTTPSCertFile string `json:"https_cert_file"` - HTTPSKeyFile string `json:"https_key_file"` - EnableHTTPS bool `json:"enable_https"` - JwksURL string `json:"jwks_url"` - JwksFile string `json:"jwks_file"` - TokenIssuerURL string `json:"jwt_token_issuer_url"` + BindAddress string `json:"bind_address"` + HTTPSCertFile string `json:"https_cert_file"` + HTTPSKeyFile string `json:"https_key_file"` + EnableHTTPS bool `json:"enable_https"` // The public http host URL to access the service // For staging it is "https://api.stage.openshift.com" // For production it is "https://api.openshift.com" @@ -27,9 +23,6 @@ func NewServerConfig() *ServerConfig { return &ServerConfig{ BindAddress: "localhost:8000", EnableHTTPS: false, - JwksURL: "https://sso.redhat.com/auth/realms/redhat-external/protocol/openid-connect/certs", - JwksFile: "config/jwks-file.json", - TokenIssuerURL: "https://sso.redhat.com/auth/realms/redhat-external", HTTPSCertFile: "", HTTPSKeyFile: "", PublicHostURL: "http://localhost", @@ -44,16 +37,11 @@ func (s *ServerConfig) AddFlags(fs *pflag.FlagSet) { fs.StringVar(&s.HTTPSKeyFile, "https-key-file", s.HTTPSKeyFile, "The path to the tls.key file.") fs.BoolVar(&s.EnableHTTPS, "enable-https", s.EnableHTTPS, "Enable HTTPS rather than HTTP") fs.BoolVar(&s.EnableTermsAcceptance, "enable-terms-acceptance", s.EnableTermsAcceptance, "Enable terms acceptance check") - fs.StringVar(&s.JwksURL, "jwks-url", s.JwksURL, "The URL of the JSON web token signing certificates.") - fs.StringVar(&s.JwksFile, "jwks-file", s.JwksFile, "File containing the the JSON web token signing certificates.") - fs.StringVar(&s.TokenIssuerURL, "token-issuer-url", s.TokenIssuerURL, "A token issuer URL. Used to validate if a JWT token used for public endpoints was issued from the given URL.") fs.StringVar(&s.PublicHostURL, "public-host-url", s.PublicHostURL, "Public http host URL of the service") fs.BoolVar(&s.EnableLeaderElection, "enable-leader-election", s.EnableLeaderElection, "Enable leader election") } // ReadFiles ... func (s *ServerConfig) ReadFiles() error { - s.JwksFile = shared.BuildFullFilePath(s.JwksFile) - return nil } diff --git a/templates/service-template.yml b/templates/service-template.yml index 6d47d91c5f..ead953f640 100644 --- a/templates/service-template.yml +++ b/templates/service-template.yml @@ -78,9 +78,6 @@ parameters: description: CPU limit for the envoy sidecar container. value: "500m" -- name: JWKS_URL - displayName: JWK Token Certificate URL - - name: OCM_URL displayName: OCM API Base URL description: Base path for all OCM APIs @@ -359,11 +356,6 @@ parameters: description: image pull policy value: "IfNotPresent" -- name: TOKEN_ISSUER_URL - displayName: A token issuer url - description: A token issuer url used to validate if JWT token used are coming from the given issuer - value: "https://sso.redhat.com/auth/realms/redhat-external" - - name: DATAPLANE_CLUSTER_CONFIG_FILE displayName: This file contains properties for manually configuring OSD cluster description: This file contains properties for manually configuring OSD cluster, and it's passed to the fleet manager executable in the flag --dataplane-cluster-config-file @@ -411,6 +403,10 @@ parameters: description: Sets the provided fleetshard image tag if the addon parameter value is 'inherit'" value: "true" +- name: DATAPLANE_OIDC_ISSUERS + description: List of DataPlane OpenID Connect issuers that should be verified for issued tokens. + value: "[]" + objects: - kind: ConfigMap apiVersion: v1 @@ -449,6 +445,15 @@ objects: data: additional-sso-issuers.yaml: |- - https://sso.stage.redhat.com/auth/realms/redhat-external + - kind: ConfigMap + apiVersion: v1 + metadata: + name: fleet-manager-dataplane-oidc-issuers-config + annotations: + qontract.recycle: "true" + data: + dataplane-oidc-issuers.yaml: |- + ${DATAPLANE_OIDC_ISSUERS} - kind: ConfigMap apiVersion: v1 metadata: @@ -791,6 +796,9 @@ objects: - name: fleet-manager-additional-sso-issuers-config configMap: name: fleet-manager-additional-sso-issuers-config + - name: fleet-manager-dataplane-oidc-issuers-config + configMap: + name: fleet-manager-dataplane-oidc-issuers-config - name: fleet-manager-read-only-user-list configMap: name: fleet-manager-read-only-user-list @@ -898,6 +906,9 @@ objects: - name: fleet-manager-additional-sso-issuers-config mountPath: /config/additional-sso-issuers.yaml subPath: additional-sso-issuers.yaml + - name: fleet-manager-dataplane-oidc-issuers-config + mountPath: /config/dataplane-oidc-issuers.yaml + subPath: dataplane-oidc-issuers.yaml - name: fleet-manager-read-only-user-list mountPath: /config/read-only-user-list.yaml subPath: read-only-user-list.yaml @@ -999,8 +1010,6 @@ objects: - --ocm-debug=${OCM_DEBUG} - --https-cert-file=/secrets/tls/tls.crt - --https-key-file=/secrets/tls/tls.key - - --jwks-url=${JWKS_URL} - - --token-issuer-url=${TOKEN_ISSUER_URL} - --enable-https=${ENABLE_HTTPS} - --api-server-bindaddress=${API_SERVER_BINDADDRESS} - --metrics-server-bindaddress=${METRICS_SERVER_BINDADDRESS} diff --git a/test/helper.go b/test/helper.go index 5a70a9b676..f2321f0640 100644 --- a/test/helper.go +++ b/test/helper.go @@ -11,9 +11,8 @@ import ( "testing" "time" - "github.com/stackrox/acs-fleet-manager/pkg/shared/testutils" - "github.com/stackrox/acs-fleet-manager/internal/dinosaur/pkg/config" + "github.com/stackrox/acs-fleet-manager/pkg/shared/testutils" "github.com/stackrox/acs-fleet-manager/pkg/client/iam" ocm "github.com/stackrox/acs-fleet-manager/pkg/client/ocm/impl" @@ -108,14 +107,13 @@ func NewHelperWithHooks(t *testing.T, httpServer *httptest.Server, configuration parseCommandLineFlags(env) var ocmConfig *ocm.OCMConfig - var serverConfig *server.ServerConfig var iamConfig *iam.IAMConfig var centralConfig *config.CentralConfig - env.MustResolveAll(&ocmConfig, &serverConfig, &iamConfig, ¢ralConfig) + env.MustResolveAll(&ocmConfig, &iamConfig, ¢ralConfig) // Create a new helper - authHelper, err := auth.NewAuthHelper(jwtKeyFile, jwtCAFile, serverConfig.TokenIssuerURL) + authHelper, err := auth.NewAuthHelper(jwtKeyFile, jwtCAFile, iamConfig.RedhatSSORealm.ValidIssuerURI) if err != nil { t.Fatalf("failed to create a new auth helper %s", err.Error()) } @@ -132,7 +130,8 @@ func NewHelperWithHooks(t *testing.T, httpServer *httptest.Server, configuration } jwkURL, stopJWKMockServer := h.StartJWKCertServerMock() - serverConfig.JwksURL = jwkURL + iamConfig.JwksURL = jwkURL + iamConfig.DataPlaneOIDCIssuers.JWKSURIs = []string{jwkURL} file := testutils.CreateNonEmptyFile(t) defer os.Remove(file.Name())