fxauthorizer

package
v0.0.0-...-eaa8db3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 23, 2026 License: Apache-2.0 Imports: 5 Imported by: 0

README

Authorizer Module

This module adds CEL based middleware for authorizing gRPC and HTTP* requests.

*Soon(tm)

The authorizer allows you to write policies targetting the following parameters:

  • Request headers/metadata
  • TLS client cert information
  • Claims in OIDC IDTokens
  • Grpc service and method name
  • HTTP URI, path and query parameters

Because the CEL program is cached and the parameters are all readily available to request handlers, policy evaluation introduces very little overhead. Benchmarking shows that common policies evaluate in about 1 microsecond.

This provides a flexible environment that allows expressing a wide variety of policies.

Components

  • GrpcServerInterceptors that evaluate the given policy on each request
  • Http Middleware that evaluates the given policy on each request

Configuration file

The module supports the following configuration options:

  • Rule: The CEL expression which will be validated for each request. Required

Request schema

While most parameters are shared between HTTP and gRPC requests, they have been tailored to their respective protocols. For the most up to date definitions check schema/schema.proto.

Example policies

  • Allow healthchecks for everyone, but other requests only for a specific service (using TLS)
    request.service == "grpc.health.v1.Healh" || request.tls.subject.common_name == "special.client"
    
  • Only allow clients from a specific OIDC group
    "dev" in request.jwt.groups
    

Roadmap

  • Hot reloading policies

Documentation

Overview

Example (Grpc)
package main

import (
	"context"
	"fmt"

	sconfig "github.com/exoscale/stelling/config"
	"github.com/exoscale/stelling/fxauthorizer"
	"github.com/exoscale/stelling/fxgrpc"
	"github.com/exoscale/stelling/fxgrpc/health"
	"go.uber.org/fx"
	"go.uber.org/fx/fxevent"
	"go.uber.org/zap"

	pb "google.golang.org/grpc/examples/route_guide/routeguide"

	healthpb "google.golang.org/grpc/health/grpc_health_v1"
	"google.golang.org/grpc/status"
)

type GrpcConfig struct {
	fxgrpc.Server
	fxgrpc.Client
	fxauthorizer.Authorizer
}

type RouteGuideServer struct {
	pb.UnimplementedRouteGuideServer
}

func NewRouteGuideServer() pb.RouteGuideServer {
	return &RouteGuideServer{}
}

func main() {
	conf := &GrpcConfig{}
	rule := "request.service == \"grpc.health.v1.Health\""
	args := []string{"authorizer-test", "--authorizer.rule", rule, "--server.address", "localhost:8080", "--client.endpoint", "localhost:8080", "--client.insecure-connection"}
	if err := sconfig.Load(conf, args); err != nil {
		panic(err)
	}
	opts := fx.Options(
		// Suppressing fx logs to ensure deterministic output
		fx.WithLogger(func() fxevent.Logger { return fxevent.NopLogger }),
		fxgrpc.NewServerModule(conf),
		fxgrpc.NewClientModule(conf),
		health.Module,
		fxauthorizer.NewModule(conf),
		fx.Provide(
			zap.NewNop,
			NewRouteGuideServer,
			pb.NewRouteGuideClient,
			healthpb.NewHealthClient,
		),
		fx.Invoke(
			pb.RegisterRouteGuideServer,
			fxgrpc.StartGrpcServer,
			runGrpc,
		),
	)
	if err := fx.ValidateApp(opts); err != nil {
		panic(err)
	}
	fx.New(opts).Run()

}

func runGrpc(lc fx.Lifecycle, sd fx.Shutdowner, client pb.RouteGuideClient, healthClient healthpb.HealthClient) {
	lc.Append(fx.Hook{
		OnStart: func(ctx context.Context) error {
			go func() {
				defer sd.Shutdown()
				_, err := client.GetFeature(context.Background(), &pb.Point{})
				res, ok := status.FromError(err)
				if !ok {
					panic(fmt.Sprintln("could not extract grpc status code:", err))
				}
				fmt.Println("Endpoint returned status", res.String())

				resp, err := healthClient.Check(context.Background(), &healthpb.HealthCheckRequest{})
				if err != nil {
					panic(fmt.Sprintln("healthcheck request returned error:", err))
				}
				fmt.Println("Healthcheck returned", resp.String())
			}()
			return nil
		},
	})
}
Output:
Endpoint returned status rpc error: code = PermissionDenied desc = policy denied
Healthcheck returned status:SERVING
Example (Http)
package main

import (
	"context"
	"fmt"
	"net/http"

	sconfig "github.com/exoscale/stelling/config"
	"github.com/exoscale/stelling/fxauthorizer"
	"github.com/exoscale/stelling/fxhttp"
	"go.uber.org/fx"
	"go.uber.org/fx/fxevent"
	"go.uber.org/zap"
)

type HttpConfig struct {
	fxhttp.Server
	fxauthorizer.Authorizer
}

func main() {
	conf := &HttpConfig{}
	rule := "request.path == \"/health\""
	args := []string{"authorizer-test", "--authorizer.rule", rule, "--server.address", "localhost:8081"}
	if err := sconfig.Load(conf, args); err != nil {
		panic(err)
	}

	opts := fx.Options(
		// Suppressing fx logs to ensure deterministic output
		fx.WithLogger(func() fxevent.Logger { return fxevent.NopLogger }),
		fxhttp.NewModule(conf),
		fxauthorizer.NewModule(conf),
		fx.Provide(
			zap.NewNop,
			newMux,
		),
		fx.Invoke(
			fxhttp.StartHttpServer,
			runHttp,
		),
	)
	if err := fx.ValidateApp(opts); err != nil {
		panic(err)
	}
	fx.New(opts).Run()

}

func newMux() http.Handler {
	mux := http.NewServeMux()
	mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) })
	return mux
}

func runHttp(lc fx.Lifecycle, sd fx.Shutdowner) {
	lc.Append(fx.Hook{
		OnStart: func(ctx context.Context) error {
			go func() {
				defer sd.Shutdown()
				req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "http://localhost:8081/foobar", nil)
				if err != nil {
					panic(err)
				}
				resp, err := http.DefaultClient.Do(req)
				if err != nil {
					panic(fmt.Sprintln("failed to do http request", err))
				}
				defer resp.Body.Close()
				fmt.Println("Request /foobar returned status", resp.Status)

				req2, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "http://localhost:8081/health", nil)
				if err != nil {
					panic(err)
				}
				resp2, err := http.DefaultClient.Do(req2)
				if err != nil {
					panic(fmt.Sprintln("failed to do http request", err))
				}
				defer resp2.Body.Close()
				fmt.Println("Request /health returned status", resp2.Status)

			}()
			return nil
		},
	})
}
Output:
Request /foobar returned status 403 Forbidden
Request /health returned status 200 OK

Index

Examples

Constants

View Source
const GrpcInterceptorWeight uint = 70

Setting this late in the chain so observability interceptors can monitor requests that fail authorization

Variables

This section is empty.

Functions

func NewGrpcAuthorizer

func NewGrpcAuthorizer(conf AuthorizerConfig) (interceptor.GrpcAuthorizer, error)

func NewHttpAuthorizer

func NewHttpAuthorizer(conf AuthorizerConfig) (interceptor.HttpAuthorizer, error)

func NewModule

func NewModule(conf AuthorizerConfig) fx.Option

NewModule provides authorization middleware to the system: * Grpc server interceptors * Http server middleware (TODO) Keep in mind that the Authorizer components for Grpc and Http are distinct, but share the same config. If you need different rules for either protocol, you must supply 2 different configurations with proper annotations to your system

Types

type Authorizer

type Authorizer struct {
	// The CEL expression that will be evaluated for each request made to the server
	Rule string `validate:"required"`
}

Logging contains the configuration options for the authorizer module

func (*Authorizer) AuthorizerConfig

func (a *Authorizer) AuthorizerConfig() *Authorizer

type AuthorizerConfig

type AuthorizerConfig interface {
	AuthorizerConfig() *Authorizer
}

type HttpMiddlewareResult

type HttpMiddlewareResult struct {
	fx.Out

	Middleware *fxhttp.Middleware `group:"http_middleware"`
}

func NewHttpMiddleware

func NewHttpMiddleware(authorizer interceptor.HttpAuthorizer) HttpMiddlewareResult

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL