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 8, 2024
1 parent 7c3d605 commit 0064307
Show file tree
Hide file tree
Showing 4 changed files with 152 additions and 12 deletions.
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
4 changes: 4 additions & 0 deletions pkg/auth/fleetshard_authz.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,15 @@ type FleetShardAuthZConfig struct {
Enabled bool
AllowedOrgIDs AllowedOrgIDs
AllowedOrgIDsFile string
AllowedSubject string
}

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

Expand All @@ -40,6 +42,8 @@ func (c *FleetShardAuthZConfig) AddFlags(fs *pflag.FlagSet) {
"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")
fs.StringVar(&c.AllowedSubject, "fleetshard-authz-subject", c.AllowedSubject,
"Fleetshard authZ middleware allowed subject")
}

// ReadFiles ...
Expand Down
62 changes: 55 additions & 7 deletions pkg/auth/fleetshard_authz_middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,42 @@ 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.AllowedSubject)(next)
next = NewRequireIssuerMiddleware().RequireIssuer(iamConfig.DataPlaneOIDCIssuers.URIs, errors.ErrorNotFound)(next)
}

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

func checkAllowedOrgIDs(allowedOrgIDs AllowedOrgIDs) mux.MiddlewareFunc {
Expand Down Expand Up @@ -47,3 +69,29 @@ func checkAllowedOrgIDs(allowedOrgIDs AllowedOrgIDs) mux.MiddlewareFunc {
})
}
}

func checkSubject(expected string) mux.MiddlewareFunc {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
ctx := request.Context()
claims, err := GetClaimsFromContext(ctx)
if err != nil {
// Deliberately return 404 here so that it will appear as the endpoint doesn't exist if requests are
// not authorised. Otherwise, we would leak information about existing cluster IDs, since the path
// of the request is /agent-clusters/<id>.
shared.HandleError(request, writer, errors.NotFound(""))
return
}

s, _ := claims.GetSubject()
if expected == s {
next.ServeHTTP(writer, request)
return
}

glog.Infof("fleetshard subject is not allowed (expected=%s, actual=%s)", expected, s)

shared.HandleError(request, writer, errors.NotFound(""))
})
}
}
95 changes: 92 additions & 3 deletions pkg/auth/fleetshard_authz_middleware_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"net/http/httptest"
"testing"

"github.com/stackrox/acs-fleet-manager/pkg/client/iam"
"github.com/stretchr/testify/assert"

"github.com/golang-jwt/jwt/v4"
Expand Down Expand Up @@ -78,9 +79,16 @@ func TestUseFleetShardAuthorizationMiddleware(t *testing.T) {
return setContextToken(handler, tt.token)
})

UseFleetShardAuthorizationMiddleware(route, validIssuer, &FleetShardAuthZConfig{
AllowedOrgIDs: tt.allowedOrgIDs,
})
UseFleetShardAuthorizationMiddleware(route,
&iam.IAMConfig{
RedhatSSORealm: &iam.IAMRealmConfig{
ValidIssuerURI: validIssuer,
},
DataPlaneOIDCIssuers: &iam.OIDCIssuers{},
},
&FleetShardAuthZConfig{
AllowedOrgIDs: tt.allowedOrgIDs,
})

req := httptest.NewRequest("GET", "http://example.com/agent-clusters/1234", nil)
recorder := httptest.NewRecorder()
Expand Down Expand Up @@ -115,3 +123,84 @@ func TestUseFleetShardAuthorizationMiddleware_NoTokenSet(t *testing.T) {
// We expect the 404 for unauthenticated access. This way we don't potentially leak the cluster ID to a client.
assert.Equal(t, http.StatusNotFound, status)
}

func TestUseFleetShardAuthorizationMiddleware_DataPlaneOIDCIssuers(t *testing.T) {
const validIssuer = "http://localhost"

tests := map[string]struct {
token *jwt.Token
expectedStatusCode int
}{
"should succeed when sub is equal the allowed subject": {
token: &jwt.Token{
Claims: jwt.MapClaims{
"iss": validIssuer,
"sub": "fleetshard-sync",
},
},
expectedStatusCode: http.StatusOK,
},
"should fail when sub is not equal the allowed subject": {
token: &jwt.Token{
Claims: jwt.MapClaims{
"iss": validIssuer,
"sub": "third-party-service",
},
},
expectedStatusCode: http.StatusNotFound,
},
"should fail when sub is not set": {
token: &jwt.Token{
Claims: jwt.MapClaims{},
},
expectedStatusCode: http.StatusNotFound,
},
"should fail when issuer cannot be verified": {
token: &jwt.Token{
Claims: jwt.MapClaims{
"iss": "https://some-other-issuer",
"org_id": "123",
},
},
expectedStatusCode: http.StatusNotFound,
},
"should fail when issuer can be verified but sub is not set": {
token: &jwt.Token{
Claims: jwt.MapClaims{
"iss": validIssuer,
},
},
expectedStatusCode: http.StatusNotFound,
},
}

for name, tt := range tests {
t.Run(name, func(t *testing.T) {
route := mux.NewRouter().PathPrefix("/agent-clusters/{id}").Subrouter()
route.HandleFunc("", func(writer http.ResponseWriter, request *http.Request) {
shared.WriteJSONResponse(writer, http.StatusOK, "")
}).Methods(http.MethodGet)
route.Use(func(handler http.Handler) http.Handler {
return setContextToken(handler, tt.token)
})

UseFleetShardAuthorizationMiddleware(route,
&iam.IAMConfig{
RedhatSSORealm: &iam.IAMRealmConfig{
ValidIssuerURI: "http://rhssorealm.local",
},
DataPlaneOIDCIssuers: &iam.OIDCIssuers{URIs: []string{validIssuer}},
},
&FleetShardAuthZConfig{
AllowedSubject: "fleetshard-sync",
})

req := httptest.NewRequest("GET", "http://example.com/agent-clusters/1234", nil)
recorder := httptest.NewRecorder()
route.ServeHTTP(recorder, req)

status := recorder.Result().StatusCode
assert.Equal(t, tt.expectedStatusCode, status)
})
}
}

0 comments on commit 0064307

Please sign in to comment.