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  }