Skip to content

Commit

Permalink
ROX-23709: Trust Data Plane OAuth issuers in fleetshard authorization…
Browse files Browse the repository at this point in the history
… middleware
  • Loading branch information
kovayur committed May 15, 2024
1 parent 9353319 commit 44ede5a
Show file tree
Hide file tree
Showing 17 changed files with 255 additions and 69 deletions.
4 changes: 2 additions & 2 deletions .secrets.baseline
Original file line number Diff line number Diff line change
Expand Up @@ -433,7 +433,7 @@
"filename": "templates/service-template.yml",
"hashed_secret": "4e199b4a1c40b497a95fcd1cd896351733849949",
"is_verified": false,
"line_number": 700,
"line_number": 710,
"is_secret": false
}
],
Expand Down Expand Up @@ -463,5 +463,5 @@
}
]
},
"generated_at": "2024-05-08T19:37:20Z"
"generated_at": "2024-05-13T18:42:38Z"
}
15 changes: 15 additions & 0 deletions config/fleetshard-authz-development.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
# A list of org_id's that allow access to the internal API of the fleet manager.
allowed_org_ids:
# RH ACS Organization (returned for personal tokens obtained by ocm token).
- "11009103"
# RHACS Managed Service Org for staging (Entry `RHACS Managed Service Org for AWS Billing` in Bitwarden).
- "16064548"
# ACS RH SSO Org Prod
- "16134752"
# ACS RH SSO ORG Development
- "16155304"
allowed_subjects:
- "system:serviceaccount:rhacs:fleetshard-sync"
allowed_audiences:
- "acs-fleet-manager-private-api"
10 changes: 0 additions & 10 deletions config/fleetshard-authz-org-ids-development.yaml

This file was deleted.

4 changes: 0 additions & 4 deletions config/fleetshard-authz-org-ids-prod.yaml

This file was deleted.

9 changes: 9 additions & 0 deletions config/fleetshard-authz-prod.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
# A list of org_id's that allow access to the internal API of the fleet manager.
allowed_org_ids:
# ACS RH SSO Org Prod
- "16134752"
allowed_subjects:
- "system:serviceaccount:rhacs:fleetshard-sync"
allowed_audiences:
- "acs-fleet-manager-private-api"
2 changes: 1 addition & 1 deletion internal/dinosaur/pkg/environments/development.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ func NewDevelopmentEnvLoader() environments.EnvLoader {
"enable-additional-sso-issuers": "true",
"additional-sso-issuers-file": "config/additional-sso-issuers.yaml",
"jwks-file": "config/jwks-file-static.json",
"fleetshard-authz-config-file": "config/fleetshard-authz-org-ids-development.yaml",
"fleetshard-authz-config-file": "config/fleetshard-authz-development.yaml",
"central-idp-client-id": "rhacs-ms-dev",
"central-idp-issuer": "https://sso.stage.redhat.com/auth/realms/redhat-external",
"admin-authz-config-file": "config/admin-authz-roles-dev.yaml",
Expand Down
1 change: 1 addition & 0 deletions internal/dinosaur/pkg/environments/integration.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ func (b IntegrationEnvLoader) Defaults() map[string]string {
"quota-type": "quota-management-list",
"enable-deletion-of-expired-central": "true",
"dataplane-cluster-scaling-type": "auto", // need to set this to 'auto' for integration environment as some tests rely on this
"fleetshard-authz-config-file": "config/fleetshard-authz-development.yaml",
}
}

Expand Down
2 changes: 1 addition & 1 deletion internal/dinosaur/pkg/environments/stage.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ func NewStageEnvLoader() environments.EnvLoader {
"enable-additional-sso-issuers": "true",
"additional-sso-issuers-file": "config/additional-sso-issuers.yaml",
"jwks-file": "config/jwks-file-static.json",
"fleetshard-authz-config-file": "config/fleetshard-authz-org-ids-development.yaml",
"fleetshard-authz-config-file": "config/fleetshard-authz-development.yaml",
"admin-authz-config-file": "config/admin-authz-roles-dev.yaml",
}
}
3 changes: 1 addition & 2 deletions internal/dinosaur/pkg/routes/route_loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -220,8 +220,7 @@ func (s *options) buildAPIBaseRouter(mainRouter *mux.Router, basePath string, op
Methods(http.MethodGet)

// deliberately returns 404 here if the request doesn't have the required role, so that it will appear as if the endpoint doesn't exist
auth.UseFleetShardAuthorizationMiddleware(apiV1DataPlaneRequestsRouter,
s.IAMConfig.RedhatSSORealm.ValidIssuerURI, s.FleetShardAuthZConfig)
auth.UseFleetShardAuthorizationMiddleware(apiV1DataPlaneRequestsRouter, s.IAMConfig, s.FleetShardAuthZConfig)

adminCentralHandler := handlers.NewAdminCentralHandler(s.Central, s.AccountService, s.ProviderConfig, s.Telemetry)
adminRouter := apiV1Router.PathPrefix(routes.AdminAPIPrefix).Subrouter()
Expand Down
9 changes: 9 additions & 0 deletions pkg/auth/acs_claims.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,15 @@ func (c *ACSClaims) GetSubject() (string, error) {
return "", fmt.Errorf("can't find %q attribute in claims", tenantSubClaim)
}

// GetAudience returns the audience claim of the token. It identifies the token consumer.
func (c *ACSClaims) GetAudience() (string, error) {
if sub, ok := (*c)[audienceClaim].(string); ok {
return sub, nil
}

return "", fmt.Errorf("can't find %q attribute in claims", audienceClaim)
}

// IsOrgAdmin ...
func (c *ACSClaims) IsOrgAdmin() bool {
isOrgAdmin, _ := (*c)[tenantOrgAdminClaim].(bool)
Expand Down
2 changes: 2 additions & 0 deletions pkg/auth/context_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ var (

// Sub claim in old format.
alternateSubClaim = "deprecated_sub"

audienceClaim = "aud"
)

// ContextConfig ...
Expand Down
32 changes: 17 additions & 15 deletions pkg/auth/fleetshard_authz.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,34 +9,36 @@ import (
"gopkg.in/yaml.v2"
)

// AllowedOrgIDs ...
type AllowedOrgIDs []string
// ClaimValues a list of claim values that a fleetshard access token may contain
type ClaimValues []string

// IsOrgIDAllowed ...
func (allowedOrgIDs AllowedOrgIDs) IsOrgIDAllowed(orgID string) bool {
return arrays.FindFirstString(allowedOrgIDs, func(allowedOrgID string) bool {
return orgID == allowedOrgID
// Contains returns true if the specified value is present in the list
func (v ClaimValues) Contains(value string) bool {
return arrays.FindFirstString(v, func(allowedValue string) bool {
return value == allowedValue
}) != -1
}

// FleetShardAuthZConfig ...
type FleetShardAuthZConfig struct {
Enabled bool
AllowedOrgIDs AllowedOrgIDs
AllowedOrgIDsFile string
Enabled bool `yaml:"-"`
File string `yaml:"-"`
AllowedOrgIDs ClaimValues `yaml:"allowed_org_ids"`
AllowedSubjects ClaimValues `yaml:"allowed_subjects"`
AllowedAudiences ClaimValues `yaml:"allowed_audiences"`
}

// NewFleetShardAuthZConfig ...
func NewFleetShardAuthZConfig() *FleetShardAuthZConfig {
return &FleetShardAuthZConfig{
Enabled: true,
AllowedOrgIDsFile: "config/fleetshard-authz-org-ids-prod.yaml",
Enabled: true,
File: "config/fleetshard-authz-prod.yaml",
}
}

// AddFlags ...
func (c *FleetShardAuthZConfig) AddFlags(fs *pflag.FlagSet) {
fs.StringVar(&c.AllowedOrgIDsFile, "fleetshard-authz-config-file", c.AllowedOrgIDsFile,
fs.StringVar(&c.File, "fleetshard-authz-config-file", c.File,
"Fleetshard authZ middleware configuration file containing a list of allowed org IDs")
fs.BoolVar(&c.Enabled, "enable-fleetshard-authz", c.Enabled, "Enable fleetshard authZ "+
"via the list of allowed org IDs")
Expand All @@ -45,19 +47,19 @@ func (c *FleetShardAuthZConfig) AddFlags(fs *pflag.FlagSet) {
// ReadFiles ...
func (c *FleetShardAuthZConfig) ReadFiles() error {
if c.Enabled {
return readFleetShardAuthZConfigFile(c.AllowedOrgIDsFile, &c.AllowedOrgIDs)
return readFleetShardAuthZConfigFile(c.File, c)
}

return nil
}

func readFleetShardAuthZConfigFile(file string, val *AllowedOrgIDs) error {
func readFleetShardAuthZConfigFile(file string, config *FleetShardAuthZConfig) error {
fileContents, err := shared.ReadFile(file)
if err != nil {
return fmt.Errorf("reading FleedShard AuthZ config: %w", err)
}

err = yaml.UnmarshalStrict([]byte(fileContents), val)
err = yaml.UnmarshalStrict([]byte(fileContents), config)
if err != nil {
return fmt.Errorf("unmarshalling FleedShard AuthZ config: %w", err)
}
Expand Down
61 changes: 49 additions & 12 deletions pkg/auth/fleetshard_authz_middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,60 @@ import (
"strings"

"github.com/golang/glog"

"github.com/gorilla/mux"
"github.com/stackrox/acs-fleet-manager/pkg/client/iam"
"github.com/stackrox/acs-fleet-manager/pkg/errors"
"github.com/stackrox/acs-fleet-manager/pkg/shared"
)

// UseFleetShardAuthorizationMiddleware ...
func UseFleetShardAuthorizationMiddleware(router *mux.Router, jwkValidIssuerURI string,
func UseFleetShardAuthorizationMiddleware(router *mux.Router, iamConfig *iam.IAMConfig,
fleetShardAuthZConfig *FleetShardAuthZConfig) {
router.Use(
NewRequireOrgIDMiddleware().RequireOrgID(errors.ErrorNotFound),
checkAllowedOrgIDs(fleetShardAuthZConfig.AllowedOrgIDs),
NewRequireIssuerMiddleware().RequireIssuer([]string{jwkValidIssuerURI}, errors.ErrorNotFound),
)
router.Use(fleetShardAuthorizationMiddleware(iamConfig, fleetShardAuthZConfig))
}

func fleetShardAuthorizationMiddleware(iamConfig *iam.IAMConfig, fleetShardAuthZConfig *FleetShardAuthZConfig) mux.MiddlewareFunc {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
ctx := req.Context()
claims, err := GetClaimsFromContext(ctx)
if err != nil {
serviceErr := errors.New(errors.ErrorNotFound, "")
shared.HandleError(req, w, serviceErr)
return
}

if claims.VerifyIssuer(iamConfig.RedhatSSORealm.ValidIssuerURI, true) {
// middlewares must be applied in REVERSE order (last comes first)
next = checkAllowedOrgIDs(fleetShardAuthZConfig.AllowedOrgIDs)(next)
next = NewRequireOrgIDMiddleware().RequireOrgID(errors.ErrorNotFound)(next)
} else {
// middlewares must be applied in REVERSE order (last comes first)
next = checkSubject(fleetShardAuthZConfig.AllowedSubjects)(next)
next = checkAudience(fleetShardAuthZConfig.AllowedAudiences)(next)
next = NewRequireIssuerMiddleware().RequireIssuer(iamConfig.DataPlaneOIDCIssuers.URIs, errors.ErrorNotFound)(next)
}

next.ServeHTTP(w, req)
})
}
}

func checkAllowedOrgIDs(allowedOrgIDs []string) mux.MiddlewareFunc {
return checkClaim(alternateTenantIDClaim, (*ACSClaims).GetOrgID, allowedOrgIDs)
}

func checkAllowedOrgIDs(allowedOrgIDs AllowedOrgIDs) mux.MiddlewareFunc {
func checkSubject(subjects []string) mux.MiddlewareFunc {
return checkClaim(tenantSubClaim, (*ACSClaims).GetSubject, subjects)
}

func checkAudience(audiences []string) mux.MiddlewareFunc {
return checkClaim(audienceClaim, (*ACSClaims).GetAudience, audiences)
}

type claimsGetter func(*ACSClaims) (string, error)

func checkClaim(claimName string, getter claimsGetter, allowedValues ClaimValues) mux.MiddlewareFunc {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
ctx := request.Context()
Expand All @@ -34,14 +71,14 @@ func checkAllowedOrgIDs(allowedOrgIDs AllowedOrgIDs) mux.MiddlewareFunc {
return
}

orgID, _ := claims.GetOrgID()
if allowedOrgIDs.IsOrgIDAllowed(orgID) {
claimValue, _ := getter(&claims)
if allowedValues.Contains(claimValue) {
next.ServeHTTP(writer, request)
return
}

glog.Infof("org_id %q is not in the list of allowed org IDs [%s]",
orgID, strings.Join(allowedOrgIDs, ","))
glog.Infof("%s %q is not in the list of allowed values [%s]",
claimName, claimValue, strings.Join(allowedValues, ","))

shared.HandleError(request, writer, errors.NotFound(""))
})
Expand Down
Loading

0 comments on commit 44ede5a

Please sign in to comment.