Skip to content

embeddable mode #122

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

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
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
34 changes: 34 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,3 +139,37 @@ export KUBECONFIG=$(pwd)/dev.kubeconfig
kubectx proxy
kubectl --insecure-skip-tls-verify get namespace
```

## Embedded Mode

The proxy supports an embedded mode that allows direct in-process connections without network overhead.
This is useful for applications that want to embed the proxy functionality directly.

In embedded mode:
- No TLS/network layer - requests go directly through handlers
- Authentication via configurable HTTP headers (programmatic configuration only)
- High performance with sub-microsecond latency
- Compatible with standard HTTP clients and kubernetes client-go

Embedded mode is designed for programmatic use when embedding the proxy in Go applications:

```go
// Basic embedded mode setup
opts := proxy.NewOptions(proxy.WithEmbeddedProxy, proxy.WithEmbeddedSpiceDBEndpoint)

// Complete configuration
completedConfig, _ := opts.Complete(ctx)
proxySrv, _ := proxy.NewServer(ctx, completedConfig)

// Get client with automatic authentication headers
client := proxySrv.GetEmbeddedClient(
proxy.WithUser("alice"),
proxy.WithGroups("developers", "admin"),
proxy.WithExtra("department", "engineering"),
)

// Or get a basic client without authentication
basicClient := proxySrv.GetEmbeddedClient()
```

See [docs/embedding.md](docs/embedding.md) for detailed usage examples.
231 changes: 231 additions & 0 deletions docs/embedding.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
# Embedding

The SpiceDB KubeAPI Proxy supports an embedded mode that allows you to integrate the proxy directly into your application without requiring TLS certificates or binding to network ports.
Under the hood, it uses the [`pkg/inmemory`](./pkg/inmemory/) transport package for zero-overhead HTTP communication.

## Usage

### Basic Setup

```go
package main

import (
"context"
"net/http"

"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"

"github.com/authzed/spicedb-kubeapi-proxy/pkg/proxy"
)

func main() {
ctx := context.Background()

// Create options with embedded mode enabled
opts := proxy.NewOptions(proxy.WithEmbeddedProxy, proxy.WithEmbeddedSpiceDBEndpoint)

// Configure your backend Kubernetes cluster
opts.RestConfigFunc = func() (*rest.Config, http.RoundTripper, error) {
// Return your cluster's REST config and transport
return myClusterConfig, myTransport, nil
}

// SpiceDB is already configured for embedded mode via WithEmbeddedSpiceDBEndpoint

// Set up your authorization rules
opts.RuleConfigFile = "rules.yaml"

// Complete configuration
if err := opts.Complete(ctx); err != nil {
panic(err)
}

// Create the proxy server
proxySrv, err := proxy.NewServer(ctx, *opts)
if err != nil {
panic(err)
}

// Get an HTTP client that connects directly to the embedded proxy
// Use functional options to automatically add authentication headers
embeddedClient := proxySrv.GetEmbeddedClient(
proxy.WithUser("my-user"),
proxy.WithGroups("my-group", "admin"),
proxy.WithExtra("department", "engineering"),
)

// Create a Kubernetes client that uses the embedded proxy
k8sClient := createKubernetesClient(embeddedClient)

// Use the client normally - all requests go through SpiceDB authorization
pods, err := k8sClient.CoreV1().Pods("default").List(ctx, metav1.ListOptions{})
if err != nil {
panic(err)
}

fmt.Printf("Found %d pods\n", len(pods.Items))
}

func createKubernetesClient(embeddedClient *http.Client) *kubernetes.Clientset {
restConfig := rest.CopyConfig(proxy.EmbeddedRestConfig)
restConfig.Transport = embeddedClient.Transport

k8sClient, err := kubernetes.NewForConfig(restConfig)
if err != nil {
panic(err)
}

return k8sClient
}
```

### Configuration Options

## Configuration Options

You can configure the proxy with different combinations of embedded options:

### Full Embedded Mode (Proxy + SpiceDB)
```go
// Both proxy and SpiceDB run embedded
opts := proxy.NewOptions(proxy.WithEmbeddedProxy, proxy.WithEmbeddedSpiceDBEndpoint)
```

### Embedded Proxy with Remote SpiceDB
```go
// Proxy runs embedded, but connects to remote SpiceDB
opts := proxy.NewOptions(proxy.WithEmbeddedProxy)
opts.SpiceDBOptions.SpiceDBEndpoint = "localhost:50051"
opts.SpiceDBOptions.SecureSpiceDBTokensBySpace = "your-token"
```

### Regular Proxy with Embedded SpiceDB
```go
// Proxy runs with TLS termination, but uses embedded SpiceDB
opts := proxy.NewOptions(proxy.WithEmbeddedSpiceDBEndpoint)
```

### Example Configuration
```go
opts := proxy.NewOptions(proxy.WithEmbeddedProxy, proxy.WithEmbeddedSpiceDBEndpoint)

// Backend Kubernetes cluster configuration
opts.RestConfigFunc = func() (*rest.Config, http.RoundTripper, error) {
// Your cluster configuration
}

// SpiceDB configuration is already set to embedded via WithEmbeddedSpiceDBEndpoint
// For remote SpiceDB, you would instead use:
// opts.SpiceDBOptions.SpiceDBEndpoint = "localhost:50051"
// opts.SpiceDBOptions.SecureSpiceDBTokensBySpace = "your-token"

// Authorization rules
opts.RuleConfigFile = "path/to/rules.yaml"
```

### Authentication Headers

In embedded mode, authentication is handled via HTTP headers:

The embedded proxy has a dedicated `EmbeddedAuthentication` configuration that is designed for programmatic use only. When embedding the proxy in your Go application, you can configure the header names through the `opts.Authentication.Embedded` struct:

- `opts.Authentication.Embedded.UsernameHeaders`
- `opts.Authentication.Embedded.GroupHeaders`
- `opts.Authentication.Embedded.ExtraHeaderPrefixes`

**Default Headers:**
- `X-Remote-User`: The username (required)
- `X-Remote-Group`: Group membership (can be specified multiple times)
- `X-Remote-Extra-*`: Extra user attributes (e.g., `X-Remote-Extra-Department: engineering`)

**Example with default headers:**

```
X-Remote-User: alice
X-Remote-Group: developers
X-Remote-Group: admin
X-Remote-Extra-Department: engineering
X-Remote-Extra-Team: platform
```

**Example with custom headers (programmatic configuration):**

```go
opts := proxy.NewOptions(proxy.WithEmbeddedProxy, proxy.WithEmbeddedSpiceDBEndpoint)

// Configure custom header names
opts.Authentication.Embedded.UsernameHeaders = []string{"Custom-User"}
opts.Authentication.Embedded.GroupHeaders = []string{"Custom-Groups"}
opts.Authentication.Embedded.ExtraHeaderPrefixes = []string{"Custom-Extra-"}

// Complete and create the proxy server
completedConfig, _ := opts.Complete(ctx)
proxySrv, _ := proxy.NewServer(ctx, completedConfig)

// The client will automatically use the custom header names
embeddedClient := proxySrv.GetEmbeddedClient(
proxy.WithUser("alice"),
proxy.WithGroups("developers", "admin"),
proxy.WithExtra("department", "engineering"),
)
// Headers will be: Custom-User: alice, Custom-Groups: developers, etc.
```

This is similar to Kubernetes' request header authentication, but uses a separate dedicated `EmbeddedAuthentication` type for embedded mode and doesn't require client certificate configuration (the requests are trusted because the server is embedded).

### Functional Options for GetEmbeddedClient

The `GetEmbeddedClient()` method supports functional options that automatically add authentication headers based on your configured header names. This eliminates the need to manually add headers to each request:

```go
// Basic client without authentication
client := proxySrv.GetEmbeddedClient()

// Client with user authentication
client := proxySrv.GetEmbeddedClient(
proxy.WithUser("alice"),
)

// Client with user and groups
client := proxySrv.GetEmbeddedClient(
proxy.WithUser("alice"),
proxy.WithGroups("developers", "admin", "reviewers"),
)

// Client with user, groups, and extra attributes
client := proxySrv.GetEmbeddedClient(
proxy.WithUser("alice"),
proxy.WithGroups("developers", "admin"),
proxy.WithExtra("department", "engineering"),
proxy.WithExtra("team", "platform"),
proxy.WithExtra("location", "remote"),
)
```

The functional options automatically use the header names you've configured in `opts.Authentication.Embedded`. For example, if you've configured custom header names:

```go
opts.Authentication.Embedded.UsernameHeaders = []string{"My-User"}
opts.Authentication.Embedded.GroupHeaders = []string{"My-Groups"}
opts.Authentication.Embedded.ExtraHeaderPrefixes = []string{"My-Extra-"}

// This client will automatically add:
// My-User: alice
// My-Groups: developers
// My-Groups: admin
// My-Extra-department: engineering
client := proxySrv.GetEmbeddedClient(
proxy.WithUser("alice"),
proxy.WithGroups("developers", "admin"),
proxy.WithExtra("department", "engineering"),
)
```

Available functional options:
- `WithUser(username string)`: Sets the username
- `WithGroups(groups ...string)`: Sets group memberships
- `WithExtra(key, value string)`: Sets extra user attributes (can be called multiple times)

This approach provides a clean, type-safe way to configure authentication without manually managing headers.
45 changes: 4 additions & 41 deletions e2e/e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,12 @@ import (
"os"
"path"
"path/filepath"
goruntime "runtime"

"testing"
"time"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/spf13/afero"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/discovery/cached/disk"
"k8s.io/client-go/informers"
Expand All @@ -40,11 +39,6 @@ import (
"k8s.io/kubernetes/pkg/controller/garbagecollector"
"sigs.k8s.io/controller-runtime/pkg/envtest"
"sigs.k8s.io/controller-runtime/pkg/log/zap"
"sigs.k8s.io/controller-runtime/tools/setup-envtest/env"
"sigs.k8s.io/controller-runtime/tools/setup-envtest/remote"
"sigs.k8s.io/controller-runtime/tools/setup-envtest/store"
"sigs.k8s.io/controller-runtime/tools/setup-envtest/versions"
"sigs.k8s.io/controller-runtime/tools/setup-envtest/workflows"

"github.com/authzed/spicedb-kubeapi-proxy/pkg/authz/distributedtx"
"github.com/authzed/spicedb-kubeapi-proxy/pkg/proxy"
Expand Down Expand Up @@ -118,7 +112,7 @@ var _ = SynchronizedBeforeSuite(func() []byte {
Expect(err).To(Succeed())
clientCA = GenerateClientCA(port)

opts := proxy.NewOptions()
opts := proxy.NewOptions(proxy.WithEmbeddedSpiceDBEndpoint)
opts.RestConfigFunc = func() (*rest.Config, http.RoundTripper, error) {
conf, err := clientcmd.NewDefaultClientConfig(*backendCfg, nil).ClientConfig()
if err != nil {
Expand All @@ -130,7 +124,6 @@ var _ = SynchronizedBeforeSuite(func() []byte {
}
opts.RuleConfigFile = "rules.yaml"
opts.SecureServing.BindPort = port
opts.SpiceDBOptions.SpiceDBEndpoint = proxy.EmbeddedSpiceDBEndpoint
opts.SecureServing.BindAddress = net.ParseIP("127.0.0.1")
opts.Authentication.BuiltInOptions.ClientCert.ClientCA = clientCA.Path()

Expand All @@ -154,39 +147,9 @@ var _ = SynchronizedBeforeSuite(func() []byte {
func ConfigureApiserver() {
log := zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))

e := &env.Env{
Log: log,
Client: &remote.HTTPClient{
Log: log,
IndexURL: remote.DefaultIndexURL,
},
Version: versions.Spec{
Selector: versions.TildeSelector{},
CheckLatest: false,
},
VerifySum: true,
ForceDownload: false,
Platform: versions.PlatformItem{
Platform: versions.Platform{
OS: goruntime.GOOS,
Arch: goruntime.GOARCH,
},
},
FS: afero.Afero{Fs: afero.NewOsFs()},
Store: store.NewAt("../testbin"),
Out: os.Stdout,
}
var err error
e.Version, err = versions.FromExpr("~1.33.0")
Expect(err).To(Succeed())

workflows.Use{
UseEnv: true,
PrintFormat: env.PrintOverview,
AssetsPath: "../testbin",
}.Do(e)
assetsPath := setupEnvtest(log)

Expect(os.Setenv("KUBEBUILDER_ASSETS", fmt.Sprintf("../testbin/k8s/%s-%s-%s", e.Version.AsConcrete(), e.Platform.OS, e.Platform.Arch))).To(Succeed())
Expect(os.Setenv("KUBEBUILDER_ASSETS", assetsPath)).To(Succeed())
DeferCleanup(os.Unsetenv, "KUBEBUILDER_ASSETS")
}

Expand Down
Loading
Loading