github.com/authzed/spicedb@v1.32.1-0.20240520085336-ebda56537386/cmd/spicedb/servetesting_integration_test.go (about) 1 //go:build docker && image 2 // +build docker,image 3 4 package main 5 6 import ( 7 "context" 8 "fmt" 9 "io" 10 "io/ioutil" 11 "log" 12 "net/http" 13 "strings" 14 "testing" 15 "time" 16 17 v1 "github.com/authzed/authzed-go/proto/authzed/api/v1" 18 "github.com/authzed/grpcutil" 19 "github.com/google/uuid" 20 "github.com/ory/dockertest/v3" 21 "github.com/stretchr/testify/require" 22 "google.golang.org/grpc" 23 "google.golang.org/grpc/codes" 24 "google.golang.org/grpc/credentials/insecure" 25 healthpb "google.golang.org/grpc/health/grpc_health_v1" 26 "google.golang.org/grpc/status" 27 28 "github.com/authzed/spicedb/pkg/tuple" 29 ) 30 31 func TestTestServer(t *testing.T) { 32 require := require.New(t) 33 key := uuid.NewString() 34 tester, err := newTester(t, 35 &dockertest.RunOptions{ 36 Repository: "authzed/spicedb", 37 Tag: "ci", 38 Cmd: []string{ 39 "serve-testing", 40 "--log-level", "debug", 41 "--http-addr", ":8443", 42 "--readonly-http-addr", ":8444", 43 "--http-enabled", 44 "--readonly-http-enabled", 45 }, 46 ExposedPorts: []string{"50051/tcp", "50052/tcp", "8443/tcp", "8444/tcp"}, 47 }, 48 key, 49 false, 50 ) 51 require.NoError(err) 52 defer tester.cleanup() 53 54 options := []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials()), grpcutil.WithInsecureBearerToken(key)} 55 conn, err := grpc.Dial(fmt.Sprintf("localhost:%s", tester.port), options...) 56 require.NoError(err) 57 defer conn.Close() 58 59 roConn, err := grpc.Dial(fmt.Sprintf("localhost:%s", tester.readonlyPort), options...) 60 require.NoError(err) 61 defer roConn.Close() 62 63 require.Eventually(func() bool { 64 resp, err := healthpb.NewHealthClient(conn).Check(context.Background(), &healthpb.HealthCheckRequest{Service: "authzed.api.v1.SchemaService"}) 65 if err != nil || resp.GetStatus() != healthpb.HealthCheckResponse_SERVING { 66 return false 67 } 68 69 resp, err = healthpb.NewHealthClient(roConn).Check(context.Background(), &healthpb.HealthCheckRequest{Service: "authzed.api.v1.SchemaService"}) 70 if err != nil || resp.GetStatus() != healthpb.HealthCheckResponse_SERVING { 71 return false 72 } 73 74 return true 75 }, 5*time.Second, 5*time.Millisecond, "was unable to connect to running service(s)") 76 77 v1client := v1.NewPermissionsServiceClient(conn) 78 rov1client := v1.NewPermissionsServiceClient(roConn) 79 80 relationship := tuple.MustParse("resource:someresource#reader@user:somegal") 81 82 // Try writing a simple relationship against readonly and ensure it fails. 83 _, err = rov1client.WriteRelationships(context.Background(), &v1.WriteRelationshipsRequest{ 84 Updates: []*v1.RelationshipUpdate{ 85 tuple.UpdateToRelationshipUpdate(tuple.Create(relationship)), 86 }, 87 }) 88 require.Equal("rpc error: code = Unavailable desc = service read-only", err.Error()) 89 90 // Write a simple relationship. 91 _, err = v1client.WriteRelationships(context.Background(), &v1.WriteRelationshipsRequest{ 92 Updates: []*v1.RelationshipUpdate{ 93 tuple.UpdateToRelationshipUpdate(tuple.Create(relationship)), 94 }, 95 }) 96 require.NoError(err) 97 98 // Ensure the check succeeds. 99 checkReq := &v1.CheckPermissionRequest{ 100 Consistency: &v1.Consistency{ 101 Requirement: &v1.Consistency_FullyConsistent{FullyConsistent: true}, 102 }, 103 Resource: &v1.ObjectReference{ 104 ObjectType: "resource", 105 ObjectId: "someresource", 106 }, 107 Permission: "view", 108 Subject: &v1.SubjectReference{ 109 Object: &v1.ObjectReference{ 110 ObjectType: "user", 111 ObjectId: "somegal", 112 }, 113 }, 114 } 115 116 v1Resp, err := v1client.CheckPermission(context.Background(), checkReq) 117 require.NoError(err) 118 require.Equal(v1.CheckPermissionResponse_PERMISSIONSHIP_HAS_PERMISSION, v1Resp.Permissionship) 119 120 // Ensure check against readonly works as well. 121 v1Resp, err = rov1client.CheckPermission(context.Background(), checkReq) 122 require.NoError(err) 123 require.Equal(v1.CheckPermissionResponse_PERMISSIONSHIP_HAS_PERMISSION, v1Resp.Permissionship) 124 125 // Try a call with a different auth header and ensure it fails. 126 authedConn, err := grpc.Dial(fmt.Sprintf("localhost:%s", tester.readonlyPort), grpc.WithTransportCredentials(insecure.NewCredentials()), grpcutil.WithInsecureBearerToken("someothertoken")) 127 require.NoError(err) 128 defer authedConn.Close() 129 130 require.Eventually(func() bool { 131 resp, err := healthpb.NewHealthClient(authedConn).Check(context.Background(), &healthpb.HealthCheckRequest{Service: "authzed.api.v1.SchemaService"}) 132 if err != nil || resp.GetStatus() != healthpb.HealthCheckResponse_SERVING { 133 return false 134 } 135 136 return true 137 }, 5*time.Second, 5*time.Millisecond, "was unable to connect to running service(s)") 138 139 authedv1client := v1.NewPermissionsServiceClient(authedConn) 140 _, err = authedv1client.CheckPermission(context.Background(), checkReq) 141 s, ok := status.FromError(err) 142 require.True(ok) 143 require.Equal(codes.FailedPrecondition, s.Code()) 144 145 // Make an HTTP call and ensure it succeeds. 146 readUrl := fmt.Sprintf("http://localhost:%s/v1/schema/read", tester.httpPort) 147 req, err := http.NewRequest("POST", readUrl, nil) 148 req.Header.Add("Authorization", "Bearer "+key) 149 hresp, err := http.DefaultClient.Do(req) 150 require.NoError(err) 151 152 body, err := io.ReadAll(hresp.Body) 153 require.NoError(err) 154 155 require.Equal(200, hresp.StatusCode) 156 require.Contains(string(body), "schemaText") 157 require.Contains(string(body), "definition resource") 158 159 // Attempt to write to the read only HTTP and ensure it fails. 160 writeUrl := fmt.Sprintf("http://localhost:%s/v1/schema/write", tester.readonlyHttpPort) 161 wresp, err := http.Post(writeUrl, "application/json", strings.NewReader(`{ 162 "schemaText": "definition user {}\ndefinition resource {\nrelation reader: user\nrelation writer: user\nrelation foobar: user\n}" 163 }`)) 164 require.NoError(err) 165 require.Equal(503, wresp.StatusCode) 166 167 body, err = ioutil.ReadAll(wresp.Body) 168 require.NoError(err) 169 require.Contains(string(body), "SERVICE_READ_ONLY") 170 } 171 172 type spicedbHandle struct { 173 port string 174 readonlyPort string 175 httpPort string 176 readonlyHttpPort string 177 cleanup func() 178 } 179 180 const retryCount = 5 181 182 func newTester(t *testing.T, containerOpts *dockertest.RunOptions, token string, withExistingSchema bool) (*spicedbHandle, error) { 183 for i := 0; i < retryCount; i++ { 184 pool, err := dockertest.NewPool("") 185 if err != nil { 186 return nil, fmt.Errorf("could not connect to docker: %w", err) 187 } 188 189 pool.MaxWait = 30 * time.Second 190 191 resource, err := pool.RunWithOptions(containerOpts) 192 if err != nil { 193 return nil, fmt.Errorf("could not start resource: %w", err) 194 } 195 196 port := resource.GetPort("50051/tcp") 197 readonlyPort := resource.GetPort("50052/tcp") 198 httpPort := resource.GetPort("8443/tcp") 199 readonlyHttpPort := resource.GetPort("8444/tcp") 200 201 cleanup := func() { 202 // When you're done, kill and remove the container 203 if err = pool.Purge(resource); err != nil { 204 log.Fatalf("Could not purge resource: %s", err) 205 } 206 } 207 208 // Give the service time to boot. 209 err = pool.Retry(func() error { 210 conn, err := grpc.Dial( 211 fmt.Sprintf("localhost:%s", port), 212 grpc.WithTransportCredentials(insecure.NewCredentials()), 213 grpcutil.WithInsecureBearerToken(token), 214 ) 215 if err != nil { 216 return err 217 } 218 219 client := v1.NewSchemaServiceClient(conn) 220 221 if withExistingSchema { 222 _, err = client.ReadSchema(context.Background(), &v1.ReadSchemaRequest{}) 223 return err 224 } 225 226 // Write a basic schema. 227 _, err = client.WriteSchema(context.Background(), &v1.WriteSchemaRequest{ 228 Schema: ` 229 definition user {} 230 231 definition resource { 232 relation reader: user 233 relation writer: user 234 235 permission view = reader + writer 236 } 237 `, 238 }) 239 240 return err 241 }) 242 if err != nil { 243 fmt.Printf("got error on startup: %v\n", err) 244 cleanup() 245 continue 246 } 247 248 return &spicedbHandle{ 249 port: port, 250 readonlyPort: readonlyPort, 251 httpPort: httpPort, 252 readonlyHttpPort: readonlyHttpPort, 253 cleanup: cleanup, 254 }, nil 255 } 256 257 return nil, fmt.Errorf("hit maximum retries when trying to spawn test server") 258 }