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 }