github.com/authzed/spicedb@v1.32.1-0.20240520085336-ebda56537386/internal/datastore/crdb/crdb_test.go (about)

     1  //go:build ci && docker
     2  // +build ci,docker
     3  
     4  package crdb
     5  
     6  import (
     7  	"context"
     8  	"crypto/ecdsa"
     9  	"crypto/elliptic"
    10  	"crypto/rand"
    11  	"crypto/x509"
    12  	"crypto/x509/pkix"
    13  	"encoding/pem"
    14  	"fmt"
    15  	"math/big"
    16  	"net"
    17  	"os"
    18  	"path/filepath"
    19  	"testing"
    20  	"time"
    21  
    22  	"github.com/jackc/pgx/v5"
    23  	"github.com/jackc/pgx/v5/pgconn"
    24  	"github.com/jackc/pgx/v5/pgxpool"
    25  	"github.com/ory/dockertest/v3"
    26  	"github.com/stretchr/testify/require"
    27  
    28  	crdbmigrations "github.com/authzed/spicedb/internal/datastore/crdb/migrations"
    29  	"github.com/authzed/spicedb/internal/datastore/crdb/pool"
    30  	"github.com/authzed/spicedb/internal/datastore/revisions"
    31  	testdatastore "github.com/authzed/spicedb/internal/testserver/datastore"
    32  	"github.com/authzed/spicedb/pkg/datastore"
    33  	"github.com/authzed/spicedb/pkg/datastore/test"
    34  	"github.com/authzed/spicedb/pkg/migrate"
    35  )
    36  
    37  // Implement the TestableDatastore interface
    38  func (cds *crdbDatastore) ExampleRetryableError() error {
    39  	return &pgconn.PgError{
    40  		Code: pool.CrdbRetryErrCode,
    41  	}
    42  }
    43  
    44  func TestCRDBDatastore(t *testing.T) {
    45  	b := testdatastore.RunCRDBForTesting(t, "")
    46  	test.All(t, test.DatastoreTesterFunc(func(revisionQuantization, gcInterval, gcWindow time.Duration, watchBufferLength uint16) (datastore.Datastore, error) {
    47  		ctx := context.Background()
    48  		ds := b.NewDatastore(t, func(engine, uri string) datastore.Datastore {
    49  			ds, err := NewCRDBDatastore(
    50  				ctx,
    51  				uri,
    52  				GCWindow(gcWindow),
    53  				RevisionQuantization(revisionQuantization),
    54  				WatchBufferLength(watchBufferLength),
    55  				OverlapStrategy(overlapStrategyPrefix),
    56  				DebugAnalyzeBeforeStatistics(),
    57  			)
    58  			require.NoError(t, err)
    59  			return ds
    60  		})
    61  
    62  		return ds, nil
    63  	}))
    64  }
    65  
    66  func TestCRDBDatastoreWithFollowerReads(t *testing.T) {
    67  	followerReadDelay := time.Duration(4.8 * float64(time.Second))
    68  	gcWindow := 100 * time.Second
    69  
    70  	engine := testdatastore.RunCRDBForTesting(t, "")
    71  
    72  	quantizationDurations := []time.Duration{
    73  		0 * time.Second,
    74  		100 * time.Millisecond,
    75  	}
    76  	for _, quantization := range quantizationDurations {
    77  		t.Run(fmt.Sprintf("Quantization%s", quantization), func(t *testing.T) {
    78  			require := require.New(t)
    79  			ctx := context.Background()
    80  
    81  			ds := engine.NewDatastore(t, func(engine, uri string) datastore.Datastore {
    82  				ds, err := NewCRDBDatastore(
    83  					ctx,
    84  					uri,
    85  					GCWindow(gcWindow),
    86  					RevisionQuantization(quantization),
    87  					FollowerReadDelay(followerReadDelay),
    88  					DebugAnalyzeBeforeStatistics(),
    89  				)
    90  				require.NoError(err)
    91  				return ds
    92  			})
    93  			defer ds.Close()
    94  
    95  			r, err := ds.ReadyState(ctx)
    96  			require.NoError(err)
    97  			require.True(r.IsReady)
    98  
    99  			// Revisions should be at least the follower read delay amount in the past
   100  			for start := time.Now(); time.Since(start) < 50*time.Millisecond; {
   101  				testRevision, err := ds.OptimizedRevision(ctx)
   102  				require.NoError(err)
   103  
   104  				nowRevision, err := ds.HeadRevision(ctx)
   105  				require.NoError(err)
   106  
   107  				diff := nowRevision.(revisions.HLCRevision).TimestampNanoSec() - testRevision.(revisions.HLCRevision).TimestampNanoSec()
   108  				require.True(diff > followerReadDelay.Nanoseconds())
   109  			}
   110  		})
   111  	}
   112  }
   113  
   114  func TestWatchFeatureDetection(t *testing.T) {
   115  	pool, err := dockertest.NewPool("")
   116  	require.NoError(t, err)
   117  	cases := []struct {
   118  		name          string
   119  		postInit      func(ctx context.Context, adminConn *pgx.Conn)
   120  		expectEnabled bool
   121  		expectMessage string
   122  	}{
   123  		{
   124  			name: "rangefeeds disabled",
   125  			postInit: func(ctx context.Context, adminConn *pgx.Conn) {
   126  				_, err = adminConn.Exec(ctx, `SET CLUSTER SETTING kv.rangefeed.enabled = false;`)
   127  				require.NoError(t, err)
   128  			},
   129  			expectEnabled: false,
   130  			expectMessage: "Range feeds must be enabled in CockroachDB and the user must have permission to create them in order to enable the Watch API: ERROR: rangefeeds require the kv.rangefeed.enabled setting. See",
   131  		},
   132  		{
   133  			name: "rangefeeds enabled, user doesn't have permission",
   134  			postInit: func(ctx context.Context, adminConn *pgx.Conn) {
   135  				_, err = adminConn.Exec(ctx, `SET CLUSTER SETTING kv.rangefeed.enabled = true;`)
   136  				require.NoError(t, err)
   137  			},
   138  			expectEnabled: false,
   139  			expectMessage: "(SQLSTATE 42501)",
   140  		},
   141  		{
   142  			name: "rangefeeds enabled, user has permission",
   143  			postInit: func(ctx context.Context, adminConn *pgx.Conn) {
   144  				_, err = adminConn.Exec(ctx, `SET CLUSTER SETTING kv.rangefeed.enabled = true;`)
   145  				require.NoError(t, err)
   146  
   147  				_, err = adminConn.Exec(ctx, fmt.Sprintf(`GRANT CHANGEFEED ON TABLE testspicedb.%s TO unprivileged;`, tableTuple))
   148  				require.NoError(t, err)
   149  
   150  				_, err = adminConn.Exec(ctx, fmt.Sprintf(`GRANT SELECT ON TABLE testspicedb.%s TO unprivileged;`, tableTuple))
   151  				require.NoError(t, err)
   152  			},
   153  			expectEnabled: true,
   154  		},
   155  	}
   156  	for _, tt := range cases {
   157  		t.Run(tt.name, func(t *testing.T) {
   158  			ctx, cancel := context.WithCancel(context.Background())
   159  			t.Cleanup(cancel)
   160  			adminConn, connStrings := newCRDBWithUser(t, pool)
   161  			require.NoError(t, err)
   162  
   163  			migrationDriver, err := crdbmigrations.NewCRDBDriver(connStrings[testuser])
   164  			require.NoError(t, err)
   165  			require.NoError(t, crdbmigrations.CRDBMigrations.Run(ctx, migrationDriver, migrate.Head, migrate.LiveRun))
   166  
   167  			tt.postInit(ctx, adminConn)
   168  
   169  			ds, err := NewCRDBDatastore(ctx, connStrings[unprivileged])
   170  			require.NoError(t, err)
   171  
   172  			features, err := ds.Features(ctx)
   173  			require.NoError(t, err)
   174  			require.Equal(t, tt.expectEnabled, features.Watch.Enabled)
   175  			require.Contains(t, features.Watch.Reason, tt.expectMessage)
   176  
   177  			if !features.Watch.Enabled {
   178  				headRevision, err := ds.HeadRevision(ctx)
   179  				require.NoError(t, err)
   180  
   181  				_, errChan := ds.Watch(ctx, headRevision, datastore.WatchJustRelationships())
   182  				err = <-errChan
   183  				require.NotNil(t, err)
   184  				require.Contains(t, err.Error(), "watch is currently disabled")
   185  			}
   186  		})
   187  	}
   188  }
   189  
   190  type provisionedUser string
   191  
   192  const (
   193  	testuser     provisionedUser = "testuser"
   194  	unprivileged provisionedUser = "unprivileged"
   195  )
   196  
   197  func newCRDBWithUser(t *testing.T, pool *dockertest.Pool) (adminConn *pgx.Conn, connStrings map[provisionedUser]string) {
   198  	// in order to create users, cockroach must be running with
   199  	// real certs, and the root user must be authenticated with
   200  	// client certs.
   201  	certDir := t.TempDir()
   202  
   203  	ca := &x509.Certificate{
   204  		NotBefore:             time.Now(),
   205  		NotAfter:              time.Now().Add(1 * time.Hour),
   206  		SerialNumber:          big.NewInt(0),
   207  		Subject:               pkix.Name{Organization: []string{"Cockroach"}, CommonName: "Cockroach CA"},
   208  		IsCA:                  true,
   209  		ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
   210  		KeyUsage:              x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
   211  		BasicConstraintsValid: true,
   212  	}
   213  	caPrivateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
   214  	require.NoError(t, err)
   215  	caPublicKey := &caPrivateKey.PublicKey
   216  	caCertBytes, err := x509.CreateCertificate(rand.Reader, ca, ca, caPublicKey, caPrivateKey)
   217  	require.NoError(t, err)
   218  	caCert, err := x509.ParseCertificate(caCertBytes)
   219  	require.NoError(t, err)
   220  	caFile, err := os.Create(filepath.Join(certDir, "ca.crt"))
   221  	require.NoError(t, err)
   222  	t.Cleanup(func() {
   223  		caFile.Close()
   224  	})
   225  	require.NoError(t, pem.Encode(caFile, &pem.Block{
   226  		Type:  "CERTIFICATE",
   227  		Bytes: caCert.Raw,
   228  	}))
   229  
   230  	certData := &x509.Certificate{
   231  		SerialNumber:          big.NewInt(1),
   232  		NotBefore:             time.Now(),
   233  		NotAfter:              time.Now().Add(1 * time.Hour),
   234  		ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth},
   235  		KeyUsage:              x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
   236  		BasicConstraintsValid: true,
   237  		DNSNames:              []string{"localhost", "node"},
   238  		IPAddresses:           []net.IP{net.ParseIP("127.0.0.1")},
   239  	}
   240  	certPrivateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
   241  	require.NoError(t, err)
   242  	certPublicKey := &certPrivateKey.PublicKey
   243  	certBytes, err := x509.CreateCertificate(rand.Reader, certData, caCert, certPublicKey, caPrivateKey)
   244  	require.NoError(t, err)
   245  	cert, err := x509.ParseCertificate(certBytes)
   246  	require.NoError(t, err)
   247  
   248  	keyFile, err := os.OpenFile(filepath.Join(certDir, "node.key"), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o600)
   249  	require.NoError(t, err)
   250  	keyBytes, err := x509.MarshalECPrivateKey(certPrivateKey)
   251  	require.NoError(t, err)
   252  	require.NoError(t, pem.Encode(keyFile, &pem.Block{
   253  		Type:  "EC PRIVATE KEY",
   254  		Bytes: keyBytes,
   255  	}))
   256  	require.NoError(t, keyFile.Close())
   257  
   258  	certFile, err := os.OpenFile(filepath.Join(certDir, "node.crt"), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o600)
   259  	require.NoError(t, err)
   260  	require.NoError(t, pem.Encode(certFile, &pem.Block{
   261  		Type:  "CERTIFICATE",
   262  		Bytes: cert.Raw,
   263  	}))
   264  	require.NoError(t, certFile.Close())
   265  
   266  	rootUserCertData := &x509.Certificate{
   267  		SerialNumber: big.NewInt(1),
   268  		Subject: pkix.Name{
   269  			Organization: []string{"Cockroach"},
   270  			CommonName:   "root",
   271  		},
   272  		NotBefore:             time.Now(),
   273  		NotAfter:              time.Now().Add(1 * time.Hour),
   274  		ExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
   275  		KeyUsage:              x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
   276  		BasicConstraintsValid: true,
   277  		DNSNames:              []string{"root"},
   278  	}
   279  	rootUserPrivateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
   280  	require.NoError(t, err)
   281  	rootUserPublicKey := &rootUserPrivateKey.PublicKey
   282  	rootUserCertBytes, err := x509.CreateCertificate(rand.Reader, rootUserCertData, caCert, rootUserPublicKey, caPrivateKey)
   283  	require.NoError(t, err)
   284  	rootUserCert, err := x509.ParseCertificate(rootUserCertBytes)
   285  	require.NoError(t, err)
   286  
   287  	rootKeyFile, err := os.OpenFile(filepath.Join(certDir, "client.root.key"), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0o600)
   288  	require.NoError(t, err)
   289  	rootKeyBytes, err := x509.MarshalECPrivateKey(rootUserPrivateKey)
   290  	require.NoError(t, err)
   291  	require.NoError(t, pem.Encode(rootKeyFile, &pem.Block{
   292  		Type:  "EC PRIVATE KEY",
   293  		Bytes: rootKeyBytes,
   294  	}))
   295  	require.NoError(t, rootKeyFile.Close())
   296  
   297  	rootCertFile, err := os.Create(filepath.Join(certDir, "client.root.crt"))
   298  	require.NoError(t, err)
   299  	require.NoError(t, pem.Encode(rootCertFile, &pem.Block{
   300  		Type:  "CERTIFICATE",
   301  		Bytes: rootUserCert.Raw,
   302  	}))
   303  	require.NoError(t, rootCertFile.Close())
   304  
   305  	resource, err := pool.RunWithOptions(&dockertest.RunOptions{
   306  		Repository: "mirror.gcr.io/cockroachdb/cockroach",
   307  		Tag:        testdatastore.CRDBTestVersionTag,
   308  		Cmd:        []string{"start-single-node", "--certs-dir", "/certs", "--accept-sql-without-tls"},
   309  		Mounts:     []string{certDir + ":/certs"},
   310  	})
   311  	require.NoError(t, err)
   312  	t.Cleanup(func() {
   313  		require.NoError(t, pool.Purge(resource))
   314  	})
   315  
   316  	port := resource.GetPort(fmt.Sprintf("%d/tcp", 26257))
   317  	require.NoError(t, pool.Retry(func() error {
   318  		var err error
   319  		_, err = pgxpool.New(context.Background(), fmt.Sprintf("postgres://root@localhost:%[1]s/defaultdb?sslmode=verify-full&sslrootcert=%[2]s/ca.crt&sslcert=%[2]s/client.root.crt&sslkey=%[2]s/client.root.key", port, certDir))
   320  		if err != nil {
   321  			t.Log(err)
   322  			return err
   323  		}
   324  		return nil
   325  	}))
   326  
   327  	ctx, cancel := context.WithCancel(context.Background())
   328  	t.Cleanup(cancel)
   329  	adminConnString := fmt.Sprintf("postgresql://root:unused@localhost:%[1]s?sslmode=require&sslrootcert=%[2]s/ca.crt&sslcert=%[2]s/client.root.crt&sslkey=%[2]s/client.root.key", port, certDir)
   330  
   331  	require.Eventually(t, func() bool {
   332  		adminConn, err = pgx.Connect(ctx, adminConnString)
   333  		return err == nil
   334  	}, 30*time.Second, 1*time.Second)
   335  
   336  	// create a non-admin user
   337  	_, err = adminConn.Exec(ctx, `
   338  		CREATE DATABASE testspicedb;
   339  		CREATE USER testuser WITH PASSWORD 'testpass';
   340  		CREATE USER unprivileged WITH PASSWORD 'testpass2';
   341  	`)
   342  	require.NoError(t, err)
   343  
   344  	connStrings = map[provisionedUser]string{
   345  		testuser:     fmt.Sprintf("postgresql://testuser:testpass@localhost:%[1]s/testspicedb?sslmode=require&sslrootcert=%[2]s/ca.crt", port, certDir),
   346  		unprivileged: fmt.Sprintf("postgresql://unprivileged:testpass2@localhost:%[1]s/testspicedb?sslmode=require&sslrootcert=%[2]s/ca.crt", port, certDir),
   347  	}
   348  
   349  	return
   350  }