Implement JWT token auth with transparent auto-renewal.
Replace per-call SSH signing with a two-layer auth system: Server: AuthInterceptor verifies JWT tokens (HMAC-SHA256 signed with repo-local jwt.key). Authenticate RPC accepts SSH-signed challenges and issues 30-day JWTs. Expired-but-valid tokens return a ReauthChallenge in error details (server-provided nonce for fast re-auth). Authenticate RPC is exempt from token requirement. Client: TokenCredentials replaces SSHCredentials as the primary PerRPCCredentials. NewWithAuth creates clients with auto-renewal — EnsureAuth obtains initial token, retryOnAuth catches Unauthenticated errors and re-authenticates transparently. Token cached at $XDG_STATE_HOME/sgard/token (0600). CLI: dialRemote() helper handles token loading, connection setup, and initial auth. Push/pull/prune commands simplified to use it. Proto: Added Authenticate RPC, AuthenticateRequest/Response, ReauthChallenge messages. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
198
client/auth.go
198
client/auth.go
@@ -7,59 +7,148 @@ import (
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"strconv"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/kisom/sgard/sgardpb"
|
||||
"golang.org/x/crypto/ssh"
|
||||
"golang.org/x/crypto/ssh/agent"
|
||||
"google.golang.org/grpc/credentials"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
// SSHCredentials implements grpc.PerRPCCredentials using an SSH signer.
|
||||
type SSHCredentials struct {
|
||||
signer ssh.Signer
|
||||
// TokenCredentials implements grpc.PerRPCCredentials using a cached JWT token.
|
||||
// It is safe for concurrent use.
|
||||
type TokenCredentials struct {
|
||||
mu sync.RWMutex
|
||||
token string
|
||||
}
|
||||
|
||||
// NewSSHCredentials creates credentials from an SSH signer.
|
||||
func NewSSHCredentials(signer ssh.Signer) *SSHCredentials {
|
||||
return &SSHCredentials{signer: signer}
|
||||
// NewTokenCredentials creates credentials with an initial token (may be empty).
|
||||
func NewTokenCredentials(token string) *TokenCredentials {
|
||||
return &TokenCredentials{token: token}
|
||||
}
|
||||
|
||||
// GetRequestMetadata signs a nonce+timestamp and returns auth metadata.
|
||||
func (c *SSHCredentials) GetRequestMetadata(_ context.Context, _ ...string) (map[string]string, error) {
|
||||
nonce := make([]byte, 32)
|
||||
if _, err := rand.Read(nonce); err != nil {
|
||||
return nil, fmt.Errorf("generating nonce: %w", err)
|
||||
// SetToken updates the cached token.
|
||||
func (c *TokenCredentials) SetToken(token string) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.token = token
|
||||
}
|
||||
|
||||
// GetRequestMetadata returns the token as gRPC metadata.
|
||||
func (c *TokenCredentials) GetRequestMetadata(_ context.Context, _ ...string) (map[string]string, error) {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
if c.token == "" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
tsUnix := time.Now().Unix()
|
||||
payload := buildPayload(nonce, tsUnix)
|
||||
|
||||
sig, err := c.signer.Sign(rand.Reader, payload)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("signing payload: %w", err)
|
||||
}
|
||||
|
||||
pubkey := c.signer.PublicKey()
|
||||
pubkeyStr := strings.TrimSpace(string(ssh.MarshalAuthorizedKey(pubkey)))
|
||||
|
||||
return map[string]string{
|
||||
"x-sgard-auth-nonce": base64.StdEncoding.EncodeToString(nonce),
|
||||
"x-sgard-auth-timestamp": strconv.FormatInt(tsUnix, 10),
|
||||
"x-sgard-auth-signature": base64.StdEncoding.EncodeToString(ssh.Marshal(sig)),
|
||||
"x-sgard-auth-pubkey": pubkeyStr,
|
||||
}, nil
|
||||
return map[string]string{"x-sgard-auth-token": c.token}, nil
|
||||
}
|
||||
|
||||
// RequireTransportSecurity returns false — auth is via SSH signatures,
|
||||
// not TLS. Transport security can be added separately.
|
||||
func (c *SSHCredentials) RequireTransportSecurity() bool {
|
||||
// RequireTransportSecurity returns false.
|
||||
func (c *TokenCredentials) RequireTransportSecurity() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// Verify that SSHCredentials implements the interface.
|
||||
var _ credentials.PerRPCCredentials = (*SSHCredentials)(nil)
|
||||
var _ credentials.PerRPCCredentials = (*TokenCredentials)(nil)
|
||||
|
||||
// TokenPath returns the XDG-compliant path for the token cache.
|
||||
// Uses $XDG_STATE_HOME/sgard/token, falling back to ~/.local/state/sgard/token.
|
||||
func TokenPath() (string, error) {
|
||||
stateHome := os.Getenv("XDG_STATE_HOME")
|
||||
if stateHome == "" {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("determining home directory: %w", err)
|
||||
}
|
||||
stateHome = filepath.Join(home, ".local", "state")
|
||||
}
|
||||
return filepath.Join(stateHome, "sgard", "token"), nil
|
||||
}
|
||||
|
||||
// LoadCachedToken reads the token from the XDG state path.
|
||||
// Returns empty string if the file doesn't exist.
|
||||
func LoadCachedToken() string {
|
||||
path, err := TokenPath()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(string(data))
|
||||
}
|
||||
|
||||
// SaveToken writes the token to the XDG state path with 0600 permissions.
|
||||
func SaveToken(token string) error {
|
||||
path, err := TokenPath()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
|
||||
return fmt.Errorf("creating token directory: %w", err)
|
||||
}
|
||||
return os.WriteFile(path, []byte(token+"\n"), 0o600)
|
||||
}
|
||||
|
||||
// Authenticate calls the server's Authenticate RPC with an SSH-signed challenge.
|
||||
// If challenge is non-nil (reauth fast path), uses the server-provided nonce.
|
||||
// Otherwise generates a fresh nonce.
|
||||
func Authenticate(ctx context.Context, rpc sgardpb.GardenSyncClient, signer ssh.Signer, challenge *sgardpb.ReauthChallenge) (string, error) {
|
||||
var nonce []byte
|
||||
var tsUnix int64
|
||||
|
||||
if challenge != nil {
|
||||
nonce = challenge.GetNonce()
|
||||
tsUnix = challenge.GetTimestamp()
|
||||
} else {
|
||||
var err error
|
||||
nonce = make([]byte, 32)
|
||||
if _, err = rand.Read(nonce); err != nil {
|
||||
return "", fmt.Errorf("generating nonce: %w", err)
|
||||
}
|
||||
tsUnix = time.Now().Unix()
|
||||
}
|
||||
|
||||
payload := buildPayload(nonce, tsUnix)
|
||||
sig, err := signer.Sign(rand.Reader, payload)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("signing challenge: %w", err)
|
||||
}
|
||||
|
||||
pubkeyStr := strings.TrimSpace(string(ssh.MarshalAuthorizedKey(signer.PublicKey())))
|
||||
|
||||
resp, err := rpc.Authenticate(ctx, &sgardpb.AuthenticateRequest{
|
||||
Nonce: nonce,
|
||||
Timestamp: tsUnix,
|
||||
Signature: ssh.Marshal(sig),
|
||||
PublicKey: pubkeyStr,
|
||||
})
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("authenticate RPC: %w", err)
|
||||
}
|
||||
|
||||
return resp.GetToken(), nil
|
||||
}
|
||||
|
||||
// ExtractReauthChallenge extracts a ReauthChallenge from a gRPC error's
|
||||
// details, if present. Returns nil if not found.
|
||||
func ExtractReauthChallenge(err error) *sgardpb.ReauthChallenge {
|
||||
st, ok := status.FromError(err)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
for _, detail := range st.Details() {
|
||||
if challenge, ok := detail.(*sgardpb.ReauthChallenge); ok {
|
||||
return challenge
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// buildPayload constructs nonce || timestamp (big-endian int64).
|
||||
func buildPayload(nonce []byte, tsUnix int64) []byte {
|
||||
@@ -81,7 +170,6 @@ func LoadSigner(keyPath string) (ssh.Signer, error) {
|
||||
return loadSignerFromFile(keyPath)
|
||||
}
|
||||
|
||||
// Try ssh-agent.
|
||||
if sock := os.Getenv("SSH_AUTH_SOCK"); sock != "" {
|
||||
conn, err := net.Dial("unix", sock)
|
||||
if err == nil {
|
||||
@@ -94,7 +182,6 @@ func LoadSigner(keyPath string) (ssh.Signer, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// Try default key paths.
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("no SSH key found: %w", err)
|
||||
@@ -122,3 +209,40 @@ func loadSignerFromFile(path string) (ssh.Signer, error) {
|
||||
}
|
||||
return signer, nil
|
||||
}
|
||||
|
||||
// SSHCredentials is kept for backward compatibility in tests.
|
||||
// It signs every request with SSH (the old approach).
|
||||
type SSHCredentials struct {
|
||||
signer ssh.Signer
|
||||
}
|
||||
|
||||
func NewSSHCredentials(signer ssh.Signer) *SSHCredentials {
|
||||
return &SSHCredentials{signer: signer}
|
||||
}
|
||||
|
||||
func (c *SSHCredentials) GetRequestMetadata(_ context.Context, _ ...string) (map[string]string, error) {
|
||||
nonce := make([]byte, 32)
|
||||
if _, err := rand.Read(nonce); err != nil {
|
||||
return nil, fmt.Errorf("generating nonce: %w", err)
|
||||
}
|
||||
tsUnix := time.Now().Unix()
|
||||
payload := buildPayload(nonce, tsUnix)
|
||||
sig, err := c.signer.Sign(rand.Reader, payload)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("signing: %w", err)
|
||||
}
|
||||
pubkeyStr := strings.TrimSpace(string(ssh.MarshalAuthorizedKey(c.signer.PublicKey())))
|
||||
|
||||
// Send as both token-style metadata (won't work) AND the old SSH fields
|
||||
// for the Authenticate RPC. But this is only used in legacy tests.
|
||||
return map[string]string{
|
||||
"x-sgard-auth-nonce": base64.StdEncoding.EncodeToString(nonce),
|
||||
"x-sgard-auth-timestamp": fmt.Sprintf("%d", tsUnix),
|
||||
"x-sgard-auth-signature": base64.StdEncoding.EncodeToString(ssh.Marshal(sig)),
|
||||
"x-sgard-auth-pubkey": pubkeyStr,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *SSHCredentials) RequireTransportSecurity() bool { return false }
|
||||
|
||||
var _ credentials.PerRPCCredentials = (*SSHCredentials)(nil)
|
||||
|
||||
112
client/client.go
112
client/client.go
@@ -10,28 +10,103 @@ import (
|
||||
"github.com/kisom/sgard/garden"
|
||||
"github.com/kisom/sgard/server"
|
||||
"github.com/kisom/sgard/sgardpb"
|
||||
"golang.org/x/crypto/ssh"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
const chunkSize = 64 * 1024 // 64 KiB
|
||||
|
||||
// Client wraps a gRPC connection to a GardenSync server.
|
||||
type Client struct {
|
||||
rpc sgardpb.GardenSyncClient
|
||||
rpc sgardpb.GardenSyncClient
|
||||
creds *TokenCredentials // may be nil (no auth)
|
||||
signer ssh.Signer // may be nil (no auth)
|
||||
}
|
||||
|
||||
// New creates a Client from an existing gRPC connection.
|
||||
// New creates a Client from an existing gRPC connection (no auth).
|
||||
func New(conn grpc.ClientConnInterface) *Client {
|
||||
return &Client{rpc: sgardpb.NewGardenSyncClient(conn)}
|
||||
}
|
||||
|
||||
// NewWithAuth creates a Client with token-based auth and auto-renewal.
|
||||
// Loads any cached token automatically.
|
||||
func NewWithAuth(conn grpc.ClientConnInterface, creds *TokenCredentials, signer ssh.Signer) *Client {
|
||||
return &Client{
|
||||
rpc: sgardpb.NewGardenSyncClient(conn),
|
||||
creds: creds,
|
||||
signer: signer,
|
||||
}
|
||||
}
|
||||
|
||||
// EnsureAuth ensures the client has a valid token. If no token is cached,
|
||||
// authenticates with the server using the SSH signer.
|
||||
func (c *Client) EnsureAuth(ctx context.Context) error {
|
||||
if c.creds == nil || c.signer == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// If we already have a token, assume it's valid until the server says otherwise.
|
||||
md, _ := c.creds.GetRequestMetadata(ctx)
|
||||
if md != nil && md["x-sgard-auth-token"] != "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// No token — do full auth.
|
||||
return c.authenticate(ctx, nil)
|
||||
}
|
||||
|
||||
// authenticate calls the Authenticate RPC and caches the resulting token.
|
||||
func (c *Client) authenticate(ctx context.Context, challenge *sgardpb.ReauthChallenge) error {
|
||||
token, err := Authenticate(ctx, c.rpc, c.signer, challenge)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.creds.SetToken(token)
|
||||
_ = SaveToken(token)
|
||||
return nil
|
||||
}
|
||||
|
||||
// retryOnAuth retries a function once after re-authenticating if it fails
|
||||
// with Unauthenticated.
|
||||
func (c *Client) retryOnAuth(ctx context.Context, fn func() error) error {
|
||||
err := fn()
|
||||
if err == nil || c.signer == nil {
|
||||
return err
|
||||
}
|
||||
|
||||
st, ok := status.FromError(err)
|
||||
if !ok || st.Code() != codes.Unauthenticated {
|
||||
return err
|
||||
}
|
||||
|
||||
// Extract reauth challenge if present (fast path).
|
||||
challenge := ExtractReauthChallenge(err)
|
||||
if authErr := c.authenticate(ctx, challenge); authErr != nil {
|
||||
return fmt.Errorf("re-authentication failed: %w", authErr)
|
||||
}
|
||||
|
||||
// Retry the original call.
|
||||
return fn()
|
||||
}
|
||||
|
||||
// Push sends the local manifest and any missing blobs to the server.
|
||||
// Returns the number of blobs sent, or an error. If the server is newer,
|
||||
// returns ErrServerNewer.
|
||||
// returns ErrServerNewer. Automatically re-authenticates if the token expires.
|
||||
func (c *Client) Push(ctx context.Context, g *garden.Garden) (int, error) {
|
||||
var result int
|
||||
err := c.retryOnAuth(ctx, func() error {
|
||||
n, err := c.doPush(ctx, g)
|
||||
result = n
|
||||
return err
|
||||
})
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (c *Client) doPush(ctx context.Context, g *garden.Garden) (int, error) {
|
||||
localManifest := g.GetManifest()
|
||||
|
||||
// Step 1: send manifest, get decision.
|
||||
resp, err := c.rpc.PushManifest(ctx, &sgardpb.PushManifestRequest{
|
||||
Manifest: server.ManifestToProto(localManifest),
|
||||
})
|
||||
@@ -110,9 +185,18 @@ func (c *Client) Push(ctx context.Context, g *garden.Garden) (int, error) {
|
||||
|
||||
// Pull downloads the server's manifest and any missing blobs to the local garden.
|
||||
// Returns the number of blobs received, or an error. If the local manifest is
|
||||
// newer or equal, returns 0 with no error.
|
||||
// newer or equal, returns 0 with no error. Automatically re-authenticates if needed.
|
||||
func (c *Client) Pull(ctx context.Context, g *garden.Garden) (int, error) {
|
||||
// Step 1: get server manifest.
|
||||
var result int
|
||||
err := c.retryOnAuth(ctx, func() error {
|
||||
n, err := c.doPull(ctx, g)
|
||||
result = n
|
||||
return err
|
||||
})
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (c *Client) doPull(ctx context.Context, g *garden.Garden) (int, error) {
|
||||
pullResp, err := c.rpc.PullManifest(ctx, &sgardpb.PullManifestRequest{})
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("pull manifest: %w", err)
|
||||
@@ -190,12 +274,18 @@ func (c *Client) Pull(ctx context.Context, g *garden.Garden) (int, error) {
|
||||
}
|
||||
|
||||
// Prune requests the server to remove orphaned blobs. Returns the count removed.
|
||||
// Automatically re-authenticates if needed.
|
||||
func (c *Client) Prune(ctx context.Context) (int, error) {
|
||||
resp, err := c.rpc.Prune(ctx, &sgardpb.PruneRequest{})
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("prune: %w", err)
|
||||
}
|
||||
return int(resp.BlobsRemoved), nil
|
||||
var result int
|
||||
err := c.retryOnAuth(ctx, func() error {
|
||||
resp, err := c.rpc.Prune(ctx, &sgardpb.PruneRequest{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("prune: %w", err)
|
||||
}
|
||||
result = int(resp.BlobsRemoved)
|
||||
return nil
|
||||
})
|
||||
return result, err
|
||||
}
|
||||
|
||||
func writeAndVerify(g *garden.Garden, expectedHash string, data []byte) error {
|
||||
|
||||
@@ -210,8 +210,9 @@ func TestPrune(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthIntegration(t *testing.T) {
|
||||
// Generate an ed25519 key pair.
|
||||
var testJWTKey = []byte("test-jwt-secret-key-32-bytes!!")
|
||||
|
||||
func TestTokenAuthIntegration(t *testing.T) {
|
||||
_, priv, err := ed25519.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
t.Fatalf("generating key: %v", err)
|
||||
@@ -227,19 +228,18 @@ func TestAuthIntegration(t *testing.T) {
|
||||
t.Fatalf("init server garden: %v", err)
|
||||
}
|
||||
|
||||
// Set up server with auth interceptor.
|
||||
auth := server.NewAuthInterceptorFromKeys([]ssh.PublicKey{signer.PublicKey()})
|
||||
auth := server.NewAuthInterceptorFromKeys([]ssh.PublicKey{signer.PublicKey()}, testJWTKey)
|
||||
lis := bufconn.Listen(bufSize)
|
||||
srv := grpc.NewServer(
|
||||
grpc.UnaryInterceptor(auth.UnaryInterceptor()),
|
||||
grpc.StreamInterceptor(auth.StreamInterceptor()),
|
||||
)
|
||||
sgardpb.RegisterGardenSyncServer(srv, server.New(serverGarden))
|
||||
sgardpb.RegisterGardenSyncServer(srv, server.NewWithAuth(serverGarden, auth))
|
||||
t.Cleanup(func() { srv.Stop() })
|
||||
go func() { _ = srv.Serve(lis) }()
|
||||
|
||||
// Client with SSH credentials.
|
||||
creds := NewSSHCredentials(signer)
|
||||
// Client with token auth + auto-renewal.
|
||||
creds := NewTokenCredentials("")
|
||||
conn, err := grpc.NewClient("passthrough:///bufconn",
|
||||
grpc.WithContextDialer(func(context.Context, string) (net.Conn, error) {
|
||||
return lis.Dial()
|
||||
@@ -252,16 +252,22 @@ func TestAuthIntegration(t *testing.T) {
|
||||
}
|
||||
t.Cleanup(func() { _ = conn.Close() })
|
||||
|
||||
c := New(conn)
|
||||
c := NewWithAuth(conn, creds, signer)
|
||||
|
||||
// Authenticated request should succeed.
|
||||
_, err = c.Pull(context.Background(), serverGarden)
|
||||
// No token yet — EnsureAuth should authenticate via SSH.
|
||||
ctx := context.Background()
|
||||
if err := c.EnsureAuth(ctx); err != nil {
|
||||
t.Fatalf("EnsureAuth: %v", err)
|
||||
}
|
||||
|
||||
// Now requests should work.
|
||||
_, err = c.Pull(ctx, serverGarden)
|
||||
if err != nil {
|
||||
t.Fatalf("authenticated Pull should succeed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthIntegrationRejectsUnauthenticated(t *testing.T) {
|
||||
func TestAuthRejectsUnauthenticated(t *testing.T) {
|
||||
_, priv, err := ed25519.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
t.Fatalf("generating key: %v", err)
|
||||
@@ -277,17 +283,17 @@ func TestAuthIntegrationRejectsUnauthenticated(t *testing.T) {
|
||||
t.Fatalf("init server garden: %v", err)
|
||||
}
|
||||
|
||||
auth := server.NewAuthInterceptorFromKeys([]ssh.PublicKey{signer.PublicKey()})
|
||||
auth := server.NewAuthInterceptorFromKeys([]ssh.PublicKey{signer.PublicKey()}, testJWTKey)
|
||||
lis := bufconn.Listen(bufSize)
|
||||
srv := grpc.NewServer(
|
||||
grpc.UnaryInterceptor(auth.UnaryInterceptor()),
|
||||
grpc.StreamInterceptor(auth.StreamInterceptor()),
|
||||
)
|
||||
sgardpb.RegisterGardenSyncServer(srv, server.New(serverGarden))
|
||||
sgardpb.RegisterGardenSyncServer(srv, server.NewWithAuth(serverGarden, auth))
|
||||
t.Cleanup(func() { srv.Stop() })
|
||||
go func() { _ = srv.Serve(lis) }()
|
||||
|
||||
// Client WITHOUT credentials.
|
||||
// Client WITHOUT credentials — no token, no signer.
|
||||
conn, err := grpc.NewClient("passthrough:///bufconn",
|
||||
grpc.WithContextDialer(func(context.Context, string) (net.Conn, error) {
|
||||
return lis.Dial()
|
||||
|
||||
@@ -39,19 +39,20 @@ func TestE2EPushPullCycle(t *testing.T) {
|
||||
t.Fatalf("init server: %v", err)
|
||||
}
|
||||
|
||||
auth := server.NewAuthInterceptorFromKeys([]ssh.PublicKey{signer.PublicKey()})
|
||||
jwtKey := []byte("e2e-test-jwt-secret-key-32bytes!")
|
||||
auth := server.NewAuthInterceptorFromKeys([]ssh.PublicKey{signer.PublicKey()}, jwtKey)
|
||||
lis := bufconn.Listen(bufSize)
|
||||
srv := grpc.NewServer(
|
||||
grpc.UnaryInterceptor(auth.UnaryInterceptor()),
|
||||
grpc.StreamInterceptor(auth.StreamInterceptor()),
|
||||
)
|
||||
sgardpb.RegisterGardenSyncServer(srv, server.New(serverGarden))
|
||||
sgardpb.RegisterGardenSyncServer(srv, server.NewWithAuth(serverGarden, auth))
|
||||
t.Cleanup(func() { srv.Stop() })
|
||||
go func() { _ = srv.Serve(lis) }()
|
||||
|
||||
dial := func(t *testing.T) *Client {
|
||||
t.Helper()
|
||||
creds := NewSSHCredentials(signer)
|
||||
creds := NewTokenCredentials("")
|
||||
conn, err := grpc.NewClient("passthrough:///bufconn",
|
||||
grpc.WithContextDialer(func(context.Context, string) (net.Conn, error) {
|
||||
return lis.Dial()
|
||||
@@ -63,7 +64,11 @@ func TestE2EPushPullCycle(t *testing.T) {
|
||||
t.Fatalf("dial: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = conn.Close() })
|
||||
return New(conn)
|
||||
c := NewWithAuth(conn, creds, signer)
|
||||
if err := c.EnsureAuth(context.Background()); err != nil {
|
||||
t.Fatalf("EnsureAuth: %v", err)
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
Reference in New Issue
Block a user