RBAC(Role Base Access Control) with gRPC-Gateway generated RESTful API

136 views Asked by At

I use gRPC-Gateway for RESTful API and gRPC server. Here is my code:

func main() {
    ctx := context.Background()
    ctx, cancel := context.WithCancel(ctx)
    defer cancel()

    // rest server
    go func() {
        mux := runtime.NewServeMux()

        opts := []grpc.DialOption{grpc.WithInsecure()}
        err := pb.RegisterXYZHandlerFromEndpoint(ctx, mux, *grpcServerEndpoint, opts)
        if err != nil {
            // Err
        }

        if err := http.ListenAndServe(":8080", mux); err != nil {
            // Err
        }
    }()

    // grpc server
    l, err := net.Listen("tcp", ":9090")
    if err != nil {
        // Err
    }

    xyzSvc := server.Init()
    s := grpc.NewServer(
        grpc.UnaryInterceptor(
            server.AuthenticateInterceptor(xyzSvc),
        ),
    )

    pb.RegisterXYZServiceServer(s, xyzSvc)

    fmt.Println("GRPC on port 9090, REST Server on port 8080")
    if err := s.Serve(l); err != nil {
        // Err
    }
}

Here line server.AuthenticateInterceptor(xyzSvc) added authentication which works fine.

But the problem is I want to add RBAC(Role Base Access Control) with different endpoint. Something like:

mux.HandlePath("GET", "/v1/users", AuthMiddleware(RBACMiddleware("user")))
mux.HandlePath("POST", "/v1/users", AuthMiddleware(RBACMiddleware("admin")))

I know I can't do it because my endpoints are already generated by gRPC-Gateway according to *.proto file.

Is there any idea to add RBAC with gRPC-Gateway?

1

There are 1 answers

1
VonC On BEST ANSWER

To implement RBAC (Role-Based Access Control) with a gRPC-Gateway generated RESTful API, you would need to integrate your RBAC logic within the gRPC server's interceptors, as the gRPC-Gateway acts as a reverse proxy translating RESTful HTTP APIs into gRPC calls.
By handling RBAC at the gRPC level, you make sure access control is enforced regardless of whether the client is using REST or gRPC directly.

That would involve to:

  • clearly define the roles within your system and what permissions each role has.
  • create middleware functions for both authentication and authorization (RBAC). The authentication middleware should authenticate the user and attach the user's role to the context. The RBAC middleware should check the user's role against the required role for the endpoint.
  • use gRPC interceptors to apply your RBAC logic, since your REST endpoints are automatically generated and you cannot directly attach middleware as you would in a typical HTTP server.

An RBAC interceptor in your gRPC server setup would look like (a bit like NguyenTrungTin/go-grpc-boilerplate pkg/interceptor/auth.go):

Assuming Go 1.20+ and the generic slices.Contains() availability:

package server

import (
    "context"
    "google.golang.org/grpc"
    "google.golang.org/grpc/codes"
    "google.golang.org/grpc/metadata"
    "google.golang.org/grpc/status"
    "slices"
)

// AuthenticateInterceptor authenticates requests
func AuthenticateInterceptor(svc *XYZService) grpc.UnaryServerInterceptor {
    return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
        // Authentication logic here
        // Assume we extract user info and roles from the request and add it to the context
        ctx = context.WithValue(ctx, "roles", []string{"user", "admin"}) // Example: Add roles to context
        return handler(ctx, req)
    }
}

// RBACInterceptor checks if the user has the required role to access the endpoint
func RBACInterceptor(requiredRole string) grpc.UnaryServerInterceptor {
    return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) {
        roles, ok := ctx.Value("roles").([]string)
        if !ok || !slices.Contains(roles, requiredRole) {
            return nil, status.Errorf(codes.PermissionDenied, "access denied")
        }
        return handler(ctx, req)
    }
}

I'm not bothered about gRPC but I wanted to add RBAC with router ("GET", "/v1/users", RBACMiddleware("user")), and it seems to me it's not possible as long as I use grpc-gateway.

As mentioned before, the grpc-gateway serves as a bridge between HTTP/REST and gRPC by generating HTTP handlers from your gRPC services defined in the .proto files. It does not directly support attaching middleware to individual routes because it dynamically translates HTTP requests to gRPC and vice versa, without exposing direct control over the HTTP routing layer in the same way a traditional HTTP server framework might.

As an alternative approach (to achieve RBAC at the HTTP level with grpc-gateway), you might consider applying middleware at the global level to your HTTP server. That middleware can inspect incoming requests and apply RBAC checks based on the request path and method. That would allow you to enforce RBAC but requires custom logic to map HTTP routes to roles.

func RBACMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // Custom logic to determine the required role based on r.URL.Path and r.Method
        requiredRole := determineRole(r.URL.Path, r.Method)

        // Perform RBAC check (pseudo-code)
        if !userHasRole(r.Context(), requiredRole) {
            http.Error(w, "Forbidden", http.StatusForbidden)
            return
        }

        next.ServeHTTP(w, r)
    })
}

// Wrap the gRPC-Gateway mux with the RBACMiddleware
mux := runtime.NewServeMux()
http.ListenAndServe(":8080", RBACMiddleware(mux))

Or, as another option, you would create a custom HTTP handler that wraps the grpc-gateway mux (a bit like what is mentioned for tracing). That handler would intercept all HTTP requests, perform RBAC checks for specific routes, and then delegate to the grpc-gateway mux for routes that pass the RBAC check.

func customHandler(mux *runtime.ServeMux) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // Apply RBAC checks based on the request path and method
        if r.URL.Path == "/v1/users" && r.Method == "GET" {
            if !userHasRole(r.Context(), "user") {
                http.Error(w, "Forbidden", http.StatusForbidden)
                return
            }
        }

        // Delegate to the grpc-gateway mux for requests that pass RBAC checks
        mux.ServeHTTP(w, r)
    })
}

// Use the custom handler when starting your HTTP server
mux := runtime.NewServeMux()
http.ListenAndServe(":8080", customHandler(mux))

Both approaches require you to implement the logic for determineRole and userHasRole, which would typically involve extracting the user's role from the request context, likely populated by an earlier authentication middleware.

It is still not a direct route-specific middleware application that you want, because of the design of grpc-gateway, which focuses on simplicity and uniformity in exposing gRPC services as RESTful APIs, rather than offering granular control over HTTP request handling.