Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: access path for auth endpoints #1403

Merged
merged 6 commits into from
Dec 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 10 additions & 10 deletions server/authz/rbac.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,26 +46,28 @@ const (
)

// CasbinObject is the struct that implements the Authorizer interface.
// It contains the Casbin Enforcer, the current scopes, the default policy and the config reader.
// It contains the Casbin Enforcer, the current scopes, the default policy, the config reader and the route map.
// The config reader is used to watch for changes in the config file.
// The Casbin Enforcer is used to enforce the authorization policy.
// The current scopes are used to determine the user identity token to be used for authorization.
// policyDefault is the default policy to be used when the requested resource is not present in the policy.
// userPermCount is a cache to store the count of permissions for a user. If the user has permissions in the
// policy, we store the count in the cache and return based on the value.
// authRouteMap is a map of routes to their corresponding RouteInfo objects.
type CasbinObject struct {
enforcer *casbin.Enforcer
userPermCount *sync.Map
currentScopes []string
policyDefault string
configReader *viper.Viper
authRouteMap RouteMap
opts *options
rwMutex *sync.RWMutex
}

// NewCasbinObject returns a new CasbinObject. It initializes the Casbin Enforcer with the model and policy.
// It also initializes the config reader to watch for changes in the config file.
func NewCasbinObject(inputOptions ...Option) (*CasbinObject, error) {
func NewCasbinObject(authRouteMap RouteMap, inputOptions ...Option) (*CasbinObject, error) {
// Set the default options.
var opts = DefaultOptions()
// Apply the input options.
Expand Down Expand Up @@ -93,6 +95,7 @@ func NewCasbinObject(inputOptions ...Option) (*CasbinObject, error) {
currentScopes: currentScopes,
policyDefault: policyDefault,
configReader: configReader,
authRouteMap: authRouteMap,
opts: opts,
rwMutex: &sync.RWMutex{},
}
Expand All @@ -116,7 +119,7 @@ func (cas *CasbinObject) Authorize(c *gin.Context, userInfo *authn.UserInfo) boo
scopedList := getSubjectFromScope(currentScopes, userInfo)
// Get the resource, object and action from the request.
resource := extractResource(c)
object := extractObject(c)
object := extractObject(c, cas.authRouteMap)
action := c.Request.Method
userHasPolicies := false
// Check for the given scoped list if the user is authorized using any of the subjects in the list.
Expand Down Expand Up @@ -235,13 +238,10 @@ func extractResource(c *gin.Context) string {
}

// extractObject extracts the object from the request.
func extractObject(c *gin.Context) string {
action := c.Request.Method
// Get the route map from the context. Key is in the format "method:path".
routeMapKey := fmt.Sprintf("%s:%s", action, c.FullPath())
// Return the object from the route map.
if RouteMap[routeMapKey] != nil {
return RouteMap[routeMapKey].Object
func extractObject(c *gin.Context, authRouteMap RouteMap) string {
// Return the object from the route map from context.
if authRouteMap.GetRouteFromContext(c) != nil {
return authRouteMap.GetRouteFromContext(c).Object
}
return emptyString
}
Expand Down
12 changes: 6 additions & 6 deletions server/authz/rbac_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ var (
// TestCreateAuthorizer is a test implementation of the NewCasbinObject function.
// It checks that the authorizer is created correctly and the policies, configs are loaded correctly.
func TestCreateAuthorizer(t *testing.T) {
authorizer, err := NewCasbinObject(WithPolicyMap(testPolicyMapPath), WithPropertyFile(testPropertyFilePath))
authorizer, err := NewCasbinObject(RouteMap{}, WithPolicyMap(testPolicyMapPath), WithPropertyFile(testPropertyFilePath))
assert.NoError(t, err)

// Test that the authorizer is not nil
Expand All @@ -56,7 +56,7 @@ func TestCreateAuthorizer(t *testing.T) {
// TestAuthorize is a test implementation of the Authorize functionality.
// It tests that the user is authorized correctly for the given request.
func TestAuthorize(t *testing.T) {
authorizer, err := NewCasbinObject(WithPolicyMap(testPolicyMapPath), WithPropertyFile(testPropertyFilePath))
authorizer, err := NewCasbinObject(RouteMap{}, WithPolicyMap(testPolicyMapPath), WithPropertyFile(testPropertyFilePath))
assert.NoError(t, err)

// Test that the authorizer is not nil
Expand Down Expand Up @@ -92,7 +92,7 @@ func TestAuthorize(t *testing.T) {
// Additionally, it tests that the default policy is applied correctly when a user is not found in the policy map.
// The default policy is set to "role:readonly" in the test data.
func TestDefaultPolicy(t *testing.T) {
authorizer, err := NewCasbinObject(WithPolicyMap(testPolicyMapPath), WithPropertyFile(testPropertyFilePath))
authorizer, err := NewCasbinObject(RouteMap{}, WithPolicyMap(testPolicyMapPath), WithPropertyFile(testPropertyFilePath))
assert.NoError(t, err)

// Test that the authorizer is not nil
Expand All @@ -119,7 +119,7 @@ func TestDefaultPolicy(t *testing.T) {
// It tests that the scopes are set correctly when the authorizer is created.
// Additionally, it tests that the required scopes are tested for the user.
func TestScopes(t *testing.T) {
authorizer, err := NewCasbinObject(WithPolicyMap(testPolicyMapPath), WithPropertyFile(testPropertyFilePath))
authorizer, err := NewCasbinObject(RouteMap{}, WithPolicyMap(testPolicyMapPath), WithPropertyFile(testPropertyFilePath))
assert.NoError(t, err)

// Test that the authorizer is not nil
Expand Down Expand Up @@ -161,7 +161,7 @@ func TestScopes(t *testing.T) {
// TestNamespaces is a test implementation of the Namespaces based access.
// It tests that a user can access a namespace that is in the policy map.
func TestNamespaces(t *testing.T) {
authorizer, err := NewCasbinObject(WithPolicyMap(testPolicyMapPath), WithPropertyFile(testPropertyFilePath))
authorizer, err := NewCasbinObject(RouteMap{}, WithPolicyMap(testPolicyMapPath), WithPropertyFile(testPropertyFilePath))
assert.NoError(t, err)

// Test that the authorizer is not nil
Expand All @@ -186,7 +186,7 @@ func TestNamespaces(t *testing.T) {
func TestConfigFileReload(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
authorizer, err := NewCasbinObject(WithPolicyMap(testPolicyMapPath), WithPropertyFile(testPropertyReloadFilePath))
authorizer, err := NewCasbinObject(RouteMap{}, WithPolicyMap(testPolicyMapPath), WithPropertyFile(testPropertyReloadFilePath))
assert.NoError(t, err)

// Test that the authorizer is not nil
Expand Down
45 changes: 14 additions & 31 deletions server/authz/route_map.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,49 +30,32 @@ type RouteInfo struct {
RequiresAuthZ bool
}

// newRouteInfo creates a new RouteInfo object.
func newRouteInfo(object string, requiresAuthZ bool) *RouteInfo {
// NewRouteInfo creates a new RouteInfo object.
func NewRouteInfo(object string, requiresAuthZ bool) *RouteInfo {
return &RouteInfo{
Object: object,
RequiresAuthZ: requiresAuthZ,
}
}

// RouteMap is a map of routes to their corresponding RouteInfo objects.
// RouteMap type is a map of routes to their corresponding RouteInfo objects.
// It saves the object corresponding to the route and a boolean to indicate
// whether the route requires authorization.
var RouteMap = map[string]*RouteInfo{
"GET:/api/v1/sysinfo": newRouteInfo(ObjectPipeline, false),
"GET:/api/v1/authinfo": newRouteInfo(ObjectEvents, false),
"GET:/api/v1/namespaces": newRouteInfo(ObjectEvents, false),
"GET:/api/v1/cluster-summary": newRouteInfo(ObjectPipeline, false),
"GET:/api/v1/namespaces/:namespace/pipelines": newRouteInfo(ObjectPipeline, true),
"POST:/api/v1/namespaces/:namespace/pipelines": newRouteInfo(ObjectPipeline, true),
"GET:/api/v1/namespaces/:namespace/pipelines/:pipeline": newRouteInfo(ObjectPipeline, true),
"GET:/api/v1/namespaces/:namespace/pipelines/:pipeline/health": newRouteInfo(ObjectPipeline, true),
"PUT:/api/v1/namespaces/:namespace/pipelines/:pipeline": newRouteInfo(ObjectPipeline, true),
"DELETE:/api/v1/namespaces/:namespace/pipelines/:pipeline": newRouteInfo(ObjectPipeline, true),
"PATCH:/api/v1/namespaces/:namespace/pipelines/:pipeline": newRouteInfo(ObjectPipeline, true),
"POST:/api/v1/namespaces/:namespace/isb-services": newRouteInfo(ObjectISBSvc, true),
"GET:/api/v1/namespaces/:namespace/isb-services": newRouteInfo(ObjectISBSvc, true),
"GET:/api/v1/namespaces/:namespace/isb-services/:isb-service": newRouteInfo(ObjectISBSvc, true),
"PUT:/api/v1/namespaces/:namespace/isb-services/:isb-service": newRouteInfo(ObjectISBSvc, true),
"DELETE:/api/v1/namespaces/:namespace/isb-services/:isb-service": newRouteInfo(ObjectISBSvc, true),
"GET:/api/v1/namespaces/:namespace/pipelines/:pipeline/isbs": newRouteInfo(ObjectPipeline, true),
"GET:/api/v1/namespaces/:namespace/pipelines/:pipeline/watermarks": newRouteInfo(ObjectPipeline, true),
"PUT:/api/v1/namespaces/:namespace/pipelines/:pipeline/vertices/:vertex": newRouteInfo(ObjectPipeline, true),
"GET:/api/v1/namespaces/:namespace/pipelines/:pipeline/vertices/metrics": newRouteInfo(ObjectPipeline, true),
"GET:/api/v1/namespaces/:namespace/pipelines/:pipeline/vertices/:vertex/pods": newRouteInfo(ObjectPipeline, true),
"GET:/api/v1/metrics/namespaces/:namespace/pods": newRouteInfo(ObjectPipeline, true),
"GET:/api/v1/namespaces/:namespace/pods/:pod/logs": newRouteInfo(ObjectPipeline, true),
"GET:/api/v1/namespaces/:namespace/events": newRouteInfo(ObjectEvents, true),
}
type RouteMap map[string]*RouteInfo

// GetRouteMapKey returns the key for the RouteMap.
// GetRouteMapKey returns the key for the AuthRouteMap.
// The key is a combination of the HTTP method and the path.
// The format is "method:path".
// For example, "GET:/api/v1/namespaces", "POST:/api/v1/namespaces".
// This key is used to get the RouteInfo object from the RouteMap.
// This key is used to get the RouteInfo object from the AuthRouteMap.
func GetRouteMapKey(c *gin.Context) string {
return fmt.Sprintf("%s:%s", c.Request.Method, c.FullPath())
}

// GetRouteFromContext returns the RouteInfo object from the AuthRouteMap based on the context.
func (r RouteMap) GetRouteFromContext(c *gin.Context) *RouteInfo {
if routeEntry, ok := r[GetRouteMapKey(c)]; ok {
return routeEntry
}
return nil
}
37 changes: 37 additions & 0 deletions server/cmd/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import (
"github.com/numaproj/numaflow/pkg/shared/logging"
sharedtls "github.com/numaproj/numaflow/pkg/shared/tls"
v1 "github.com/numaproj/numaflow/server/apis/v1"
"github.com/numaproj/numaflow/server/authz"
"github.com/numaproj/numaflow/server/routes"
)

Expand Down Expand Up @@ -66,6 +67,8 @@ func (s *server) Start() {
router := gin.New()
router.Use(gin.LoggerWithConfig(gin.LoggerConfig{SkipPaths: []string{"/livez"}}))
router.RedirectTrailingSlash = true
// sets the route map for authorization with the base href
authRouteMap := CreateAuthRouteMap(s.options.BaseHref)
router.Use(static.Serve(s.options.BaseHref, static.LocalFile("./ui/build", true)))
if s.options.BaseHref != "/" {
router.NoRoute(func(c *gin.Context) {
Expand All @@ -85,6 +88,7 @@ func (s *server) Start() {
ServerAddr: s.options.ServerAddr,
},
s.options.BaseHref,
authRouteMap,
)
router.Use(UrlRewrite(router))
server := http.Server{
Expand Down Expand Up @@ -138,3 +142,36 @@ func UrlRewrite(r *gin.Engine) gin.HandlerFunc {
c.Next()
}
}

// CreateAuthRouteMap creates the route map for authorization.
// The key is a combination of the HTTP method and the path along with the baseHref.
// For example, "GET:/api/v1/namespaces" becomes "GET:/baseHref/api/v1/namespaces".
// The value is a RouteInfo object.
func CreateAuthRouteMap(baseHref string) authz.RouteMap {
return authz.RouteMap{
"GET:" + baseHref + "api/v1/sysinfo": authz.NewRouteInfo(authz.ObjectPipeline, false),
"GET:" + baseHref + "api/v1/authinfo": authz.NewRouteInfo(authz.ObjectEvents, false),
"GET:" + baseHref + "api/v1/namespaces": authz.NewRouteInfo(authz.ObjectEvents, false),
"GET:" + baseHref + "api/v1/cluster-summary": authz.NewRouteInfo(authz.ObjectPipeline, false),
"GET:" + baseHref + "api/v1/namespaces/:namespace/pipelines": authz.NewRouteInfo(authz.ObjectPipeline, true),
"POST:" + baseHref + "api/v1/namespaces/:namespace/pipelines": authz.NewRouteInfo(authz.ObjectPipeline, true),
"GET:" + baseHref + "api/v1/namespaces/:namespace/pipelines/:pipeline": authz.NewRouteInfo(authz.ObjectPipeline, true),
"GET:" + baseHref + "api/v1/namespaces/:namespace/pipelines/:pipeline/health": authz.NewRouteInfo(authz.ObjectPipeline, true),
"PUT:" + baseHref + "api/v1/namespaces/:namespace/pipelines/:pipeline": authz.NewRouteInfo(authz.ObjectPipeline, true),
"DELETE:" + baseHref + "api/v1/namespaces/:namespace/pipelines/:pipeline": authz.NewRouteInfo(authz.ObjectPipeline, true),
"PATCH:" + baseHref + "api/v1/namespaces/:namespace/pipelines/:pipeline": authz.NewRouteInfo(authz.ObjectPipeline, true),
"POST:" + baseHref + "api/v1/namespaces/:namespace/isb-services": authz.NewRouteInfo(authz.ObjectISBSvc, true),
"GET:" + baseHref + "api/v1/namespaces/:namespace/isb-services": authz.NewRouteInfo(authz.ObjectISBSvc, true),
"GET:" + baseHref + "api/v1/namespaces/:namespace/isb-services/:isb-service": authz.NewRouteInfo(authz.ObjectISBSvc, true),
"PUT:" + baseHref + "api/v1/namespaces/:namespace/isb-services/:isb-service": authz.NewRouteInfo(authz.ObjectISBSvc, true),
"DELETE:" + baseHref + "api/v1/namespaces/:namespace/isb-services/:isb-service": authz.NewRouteInfo(authz.ObjectISBSvc, true),
"GET:" + baseHref + "api/v1/namespaces/:namespace/pipelines/:pipeline/isbs": authz.NewRouteInfo(authz.ObjectPipeline, true),
"GET:" + baseHref + "api/v1/namespaces/:namespace/pipelines/:pipeline/watermarks": authz.NewRouteInfo(authz.ObjectPipeline, true),
"PUT:" + baseHref + "api/v1/namespaces/:namespace/pipelines/:pipeline/vertices/:vertex": authz.NewRouteInfo(authz.ObjectPipeline, true),
"GET:" + baseHref + "api/v1/namespaces/:namespace/pipelines/:pipeline/vertices/metrics": authz.NewRouteInfo(authz.ObjectPipeline, true),
"GET:" + baseHref + "api/v1/namespaces/:namespace/pipelines/:pipeline/vertices/:vertex/pods": authz.NewRouteInfo(authz.ObjectPipeline, true),
"GET:" + baseHref + "api/v1/metrics/namespaces/:namespace/pods": authz.NewRouteInfo(authz.ObjectPipeline, true),
"GET:" + baseHref + "api/v1/namespaces/:namespace/pods/:pod/logs": authz.NewRouteInfo(authz.ObjectPipeline, true),
"GET:" + baseHref + "api/v1/namespaces/:namespace/events": authz.NewRouteInfo(authz.ObjectEvents, true),
}
}
18 changes: 8 additions & 10 deletions server/routes/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ type AuthInfo struct {

var logger = logging.NewLogger().Named("server")

func Routes(r *gin.Engine, sysInfo SystemInfo, authInfo AuthInfo, baseHref string) {
func Routes(r *gin.Engine, sysInfo SystemInfo, authInfo AuthInfo, baseHref string, authRouteMap authz.RouteMap) {
r.GET("/livez", func(c *gin.Context) {
c.Status(http.StatusOK)
})
Expand All @@ -51,19 +51,19 @@ func Routes(r *gin.Engine, sysInfo SystemInfo, authInfo AuthInfo, baseHref strin
panic(err)
}
// noAuthGroup is a group of routes that do not require AuthN/AuthZ no matter whether auth is enabled.
noAuthGroup := r.Group("/auth/v1")
noAuthGroup := r.Group(baseHref + "auth/v1")
v1RoutesNoAuth(noAuthGroup, dexObj)

// r1Group is a group of routes that require AuthN/AuthZ when auth is enabled.
// they share the AuthN/AuthZ middleware.
r1Group := r.Group(baseHref + "api/v1")
if !authInfo.DisableAuth {
authorizer, err := authz.NewCasbinObject()
authorizer, err := authz.NewCasbinObject(authRouteMap)
if err != nil {
panic(err)
}
// Add the AuthN/AuthZ middleware to the group.
r1Group.Use(authMiddleware(authorizer, dexObj))
r1Group.Use(authMiddleware(authorizer, dexObj, authRouteMap))
v1Routes(r1Group, dexObj)
} else {
v1Routes(r1Group, nil)
Expand Down Expand Up @@ -144,7 +144,7 @@ func v1Routes(r gin.IRouter, dexObj *v1.DexObject) {
// authMiddleware is the middleware for AuthN/AuthZ.
// it ensures the user is authenticated and authorized
// to execute the requested action before sending the request to the api handler.
func authMiddleware(authorizer authz.Authorizer, authenticator authn.Authenticator) gin.HandlerFunc {
func authMiddleware(authorizer authz.Authorizer, authenticator authn.Authenticator, authRouteMap authz.RouteMap) gin.HandlerFunc {
return func(c *gin.Context) {
// Authenticate the user.
userInfo, err := authenticator.Authenticate(c)
Expand All @@ -154,10 +154,8 @@ func authMiddleware(authorizer authz.Authorizer, authenticator authn.Authenticat
c.Abort()
return
}
// Get the route map from the context. Key is in the format "method:path".
routeMapKey := authz.GetRouteMapKey(c)
// Check if the route requires authorization.
if authz.RouteMap[routeMapKey] != nil && authz.RouteMap[routeMapKey].RequiresAuthZ {
if authRouteMap.GetRouteFromContext(c) != nil && authRouteMap.GetRouteFromContext(c).RequiresAuthZ {
// Check if the user is authorized to execute the requested action.
isAuthorized := authorizer.Authorize(c, userInfo)
if isAuthorized {
Expand All @@ -169,12 +167,12 @@ func authMiddleware(authorizer authz.Authorizer, authenticator authn.Authenticat
c.JSON(http.StatusForbidden, v1.NewNumaflowAPIResponse(&errMsg, nil))
c.Abort()
}
} else if authz.RouteMap[routeMapKey] != nil && !authz.RouteMap[routeMapKey].RequiresAuthZ {
} else if authRouteMap.GetRouteFromContext(c) != nil && !authRouteMap.GetRouteFromContext(c).RequiresAuthZ {
// If the route does not require AuthZ, skip the AuthZ check.
c.Next()
} else {
// If the route is not present in the route map, return an error.
logger.Errorw("route not present in routeMap", "route", routeMapKey)
logger.Errorw("route not present in routeMap", "route", authz.GetRouteMapKey(c))
errMsg := "Invalid route"
c.JSON(http.StatusForbidden, v1.NewNumaflowAPIResponse(&errMsg, nil))
c.Abort()
Expand Down
4 changes: 3 additions & 1 deletion server/routes/routes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ limitations under the License.
package routes

import (
"github.com/numaproj/numaflow/server/authz"
"net/http"
"net/http/httptest"
"testing"
Expand All @@ -41,7 +42,8 @@ func TestRoutes(t *testing.T) {
DisableAuth: false,
DexServerAddr: "test-dex-server-addr",
}
Routes(router, sysInfo, authInfo, "/")
authRouteMap := authz.RouteMap{}
Routes(router, sysInfo, authInfo, "/", authRouteMap)
t.Run("/404", func(t *testing.T) {
w := httptest.NewRecorder()
req, err := http.NewRequest(http.MethodGet, "/404", nil)
Expand Down
3 changes: 2 additions & 1 deletion ui/src/components/common/AccountMenu/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import IconButton from "@mui/material/IconButton";
import { AppContextProps } from "../../../types/declarations/app";
import { AppContext } from "../../../App";
import { useNavigate } from "react-router-dom";
import { getBaseHref } from "../../../utils";
import Chip from "@mui/material/Chip";

export default function AccountMenu() {
Expand All @@ -12,7 +13,7 @@ export default function AccountMenu() {

const handleLogout = useCallback(async () => {
try {
const response = await fetch(`/auth/v1/logout`);
const response = await fetch(`${getBaseHref()}/auth/v1/logout`);
if (response.ok) {
navigate("/login");
}
Expand Down
Loading
Loading