Skip to content

Commit ba40c86

Browse files
committed
ING-1329: Add support for testing client cert auth
1 parent 1d4680a commit ba40c86

File tree

9 files changed

+363
-19
lines changed

9 files changed

+363
-19
lines changed

.github/actions/start-couchbase-cluster/action.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ runs:
2424
kv-memory: 2048
2525
index-memory: 1024
2626
fts-memory: 1024
27+
use-dino-certs: true
2728
run: |
2829
CBDC_ID=$(cbdinocluster -v alloc --def="${CLUSTERCONFIG}")
2930
cbdinocluster -v buckets add ${CBDC_ID} default --ram-quota-mb=100 --flush-enabled=true --num-replicas=2
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
name: Run Client Cert Auth Tests
2+
permissions:
3+
contents: read
4+
packages: read
5+
6+
on:
7+
push:
8+
tags:
9+
- v*
10+
branches:
11+
- master
12+
pull_request:
13+
jobs:
14+
test:
15+
name: Test
16+
strategy:
17+
matrix:
18+
server:
19+
- 8.0.0-3534
20+
- 7.6.5
21+
- 7.2.2
22+
23+
runs-on: ubuntu-latest
24+
steps:
25+
- uses: actions/checkout@v4
26+
with:
27+
submodules: recursive
28+
- name: Install cbdinocluster
29+
uses: ./.github/actions/install-cbdinocluster
30+
with:
31+
github-token: ${{ secrets.GITHUB_TOKEN }}
32+
- name: Start couchbase cluster
33+
id: start-cluster
34+
uses: ./.github/actions/start-couchbase-cluster
35+
- uses: actions/setup-go@v5
36+
with:
37+
go-version: 1.24
38+
- uses: arduino/setup-protoc@v3
39+
with:
40+
version: 31.1
41+
repo-token: ${{ secrets.GITHUB_TOKEN }}
42+
- name: Install Tools
43+
run: |
44+
go install google.golang.org/protobuf/cmd/[email protected]
45+
go install google.golang.org/grpc/cmd/[email protected]
46+
go install github.com/matryer/[email protected]
47+
- name: Install Dependencies
48+
run: go get ./...
49+
- name: Generate Files
50+
run: |
51+
go generate ./...
52+
- name: Run Test
53+
env:
54+
SGTEST_CBCONNSTR: ${{ steps.start-cluster.outputs.node-ip }}
55+
SGTEST_DINOID: ${{ steps.start-cluster.outputs.dino-id }}
56+
run: go test ./gateway/test -run TestGatewayOps -v -testify.m TestClientCertAuth
57+
58+
- name: Collect couchbase logs
59+
timeout-minutes: 10
60+
if: failure()
61+
run: |
62+
mkdir -p ./client-cert-auth-logs
63+
cbdinocluster -v collect-logs ${{ steps.start-cluster.outputs.dino-id }} ./client-cert-auth-logs
64+
- name: Upload couchbase logs
65+
if: failure()
66+
uses: actions/upload-artifact@v4
67+
with:
68+
name: cbcollect-logs-${{ matrix.server }}
69+
path: ./client-cert-auth-logs/*
70+
retention-days: 5

cmd/gateway/main.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -446,13 +446,19 @@ func startGateway() {
446446

447447
var selfSignedCert *tls.Certificate
448448
if config.selfSign {
449-
generatedCert, err := selfsignedcert.GenerateCertificate()
449+
generatedCert, generatedKey, err := selfsignedcert.GenerateCertificate()
450450
if err != nil {
451451
logger.Error("failed to generate a self-signed certificate")
452452
os.Exit(1)
453453
}
454454

455-
selfSignedCert = generatedCert
455+
tlsCert, err := selfsignedcert.ConstructTlsCert(generatedCert, generatedKey)
456+
if err != nil {
457+
logger.Error("failed to generate a self-signed certificate")
458+
os.Exit(1)
459+
}
460+
461+
selfSignedCert = tlsCert
456462
}
457463

458464
var grpcCertificate tls.Certificate

gateway/test/dapi_graceful_shutdown_test.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,16 @@ import (
1919
func (s *GatewayOpsTestSuite) TestGracefulShutdown() {
2020
s.T().Logf("setting up new instance of stellar gateway...")
2121

22-
gwCert, err := selfsignedcert.GenerateCertificate()
22+
cert, key, err := selfsignedcert.GenerateCertificate()
2323
if err != nil {
2424
s.T().Fatalf("failed to create testing certificate: %s", err)
2525
}
2626

27+
gwCert, err := selfsignedcert.ConstructTlsCert(cert, key)
28+
if err != nil {
29+
s.T().Fatalf("failed to construct testing certificate: %s", err)
30+
}
31+
2732
logger, err := zap.NewDevelopment()
2833
if err != nil {
2934
s.T().Fatalf("failed to initialize test logging: %s", err)

gateway/test/mtls_test.go

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
package test
2+
3+
import (
4+
"context"
5+
"crypto/tls"
6+
"time"
7+
8+
"github.com/couchbase/goprotostellar/genproto/kv_v1"
9+
"github.com/couchbase/stellar-gateway/testutils"
10+
"github.com/couchbase/stellar-gateway/utils/certificates"
11+
"github.com/stretchr/testify/assert"
12+
"google.golang.org/grpc"
13+
"google.golang.org/grpc/codes"
14+
"google.golang.org/grpc/credentials"
15+
)
16+
17+
func (s *GatewayOpsTestSuite) TestClientCertAuth() {
18+
testutils.SkipIfNoDinoCluster(s.T())
19+
20+
s.Run("KvService", s.KvService)
21+
}
22+
23+
func (s *GatewayOpsTestSuite) KvService() {
24+
dino := testutils.StartDinoTesting(s.T(), false)
25+
26+
username := "kvUser"
27+
28+
clientCert, err := certificates.GenerateSignedClientCert(s.caCert, s.caKey, username)
29+
assert.NoError(s.T(), err)
30+
31+
conn, err := grpc.NewClient(s.gwConnAddr,
32+
grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{
33+
RootCAs: s.clientCaCertPool,
34+
Certificates: []tls.Certificate{*clientCert},
35+
InsecureSkipVerify: false,
36+
})))
37+
if err != nil {
38+
s.T().Fatalf("failed to connect to test gateway: %s", err)
39+
}
40+
41+
kvClient := kv_v1.NewKvServiceClient(conn)
42+
43+
s.Run("UserMissing", func() {
44+
_, err := kvClient.Get(context.Background(), &kv_v1.GetRequest{
45+
BucketName: s.bucketName,
46+
ScopeName: s.scopeName,
47+
CollectionName: s.collectionName,
48+
Key: s.testDocId(),
49+
})
50+
assertRpcStatus(s.T(), err, codes.PermissionDenied)
51+
assert.Contains(s.T(), err.Error(), "Your certificate is invalid")
52+
})
53+
54+
// Create a user without any permissions
55+
dino.AddUser(username, false, false)
56+
57+
s.T().Cleanup(func() {
58+
dino.RemoveUser(username)
59+
})
60+
61+
s.Run("NoReadPermission", func() {
62+
_, err := kvClient.Get(context.Background(), &kv_v1.GetRequest{
63+
BucketName: s.bucketName,
64+
ScopeName: s.scopeName,
65+
CollectionName: s.collectionName,
66+
Key: s.testDocId(),
67+
})
68+
assertRpcStatus(s.T(), err, codes.PermissionDenied)
69+
assert.Contains(s.T(), err.Error(), "No permissions to read documents")
70+
})
71+
72+
// Add read permissions to the user and wait for this to take effect
73+
dino.AddUser(username, true, false)
74+
time.Sleep(time.Second * 5)
75+
76+
s.Run("ReadSuccess", func() {
77+
resp, err := kvClient.Get(context.Background(), &kv_v1.GetRequest{
78+
BucketName: s.bucketName,
79+
ScopeName: s.scopeName,
80+
CollectionName: s.collectionName,
81+
Key: s.testDocId(),
82+
})
83+
requireRpcSuccess(s.T(), resp, err)
84+
assertValidCas(s.T(), resp.Cas)
85+
assert.Equal(s.T(), TEST_CONTENT, resp.GetContentUncompressed())
86+
assert.Nil(s.T(), resp.GetContentCompressed())
87+
assert.Equal(s.T(), TEST_CONTENT_FLAGS, resp.ContentFlags)
88+
assert.Nil(s.T(), resp.Expiry)
89+
})
90+
91+
s.Run("NoWritePermission", func() {
92+
docId := s.randomDocId()
93+
_, err := kvClient.Upsert(context.Background(), &kv_v1.UpsertRequest{
94+
BucketName: s.bucketName,
95+
ScopeName: s.scopeName,
96+
CollectionName: s.collectionName,
97+
Key: docId,
98+
Content: &kv_v1.UpsertRequest_ContentUncompressed{
99+
ContentUncompressed: TEST_CONTENT,
100+
},
101+
ContentFlags: TEST_CONTENT_FLAGS,
102+
})
103+
assertRpcStatus(s.T(), err, codes.PermissionDenied)
104+
assert.Contains(s.T(), err.Error(), "No permissions to write documents")
105+
})
106+
107+
// Add write permissions to the user
108+
dino.AddUser(username, false, true)
109+
time.Sleep(time.Second * 5)
110+
111+
s.Run("WriteSuccess", func() {
112+
docId := s.randomDocId()
113+
resp, err := kvClient.Upsert(context.Background(), &kv_v1.UpsertRequest{
114+
BucketName: s.bucketName,
115+
ScopeName: s.scopeName,
116+
CollectionName: s.collectionName,
117+
Key: docId,
118+
Content: &kv_v1.UpsertRequest_ContentUncompressed{
119+
ContentUncompressed: TEST_CONTENT,
120+
},
121+
ContentFlags: TEST_CONTENT_FLAGS,
122+
})
123+
requireRpcSuccess(s.T(), resp, err)
124+
assertValidCas(s.T(), resp.Cas)
125+
assertValidMutationToken(s.T(), resp.MutationToken, s.bucketName)
126+
})
127+
}

gateway/test/suite_test.go

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ package test
22

33
import (
44
"context"
5+
"crypto/ecdsa"
56
"crypto/tls"
7+
"crypto/x509"
68
"encoding/base64"
79
"encoding/json"
810
"fmt"
@@ -23,6 +25,7 @@ import (
2325
"github.com/couchbase/stellar-gateway/contrib/grpcheaderauth"
2426
"github.com/couchbase/stellar-gateway/gateway"
2527
"github.com/couchbase/stellar-gateway/testutils"
28+
"github.com/couchbase/stellar-gateway/utils/certificates"
2629
"github.com/couchbase/stellar-gateway/utils/selfsignedcert"
2730
"github.com/google/uuid"
2831
"github.com/stretchr/testify/assert"
@@ -40,6 +43,7 @@ type GatewayOpsTestSuite struct {
4043
testClusterInfo *testutils.CanonicalTestCluster
4144
gatewayCloseFunc func()
4245
gatewayConn *grpc.ClientConn
46+
gwConnAddr string
4347
gatewayClosedCh chan struct{}
4448
dapiCli *http.Client
4549
dapiAddr string
@@ -55,6 +59,10 @@ type GatewayOpsTestSuite struct {
5559
basicRestCreds string
5660
readRestCreds string
5761

62+
caCert *x509.Certificate
63+
caKey *ecdsa.PrivateKey
64+
clientCaCertPool *x509.CertPool
65+
5866
clusterVersion *NodeVersion
5967
features []TestFeature
6068

@@ -249,11 +257,23 @@ func (s *GatewayOpsTestSuite) SetupSuite() {
249257
s.T().Fatalf("failed to initialize test logging: %s", err)
250258
}
251259

252-
gwCert, err := selfsignedcert.GenerateCertificate()
260+
caCert, caKey, err := selfsignedcert.GenerateCertificate()
253261
if err != nil {
254262
s.T().Fatalf("failed to create testing certificate: %s", err)
255263
}
256264

265+
gwCert, err := certificates.GenerateSignedServerCert(caCert, caKey)
266+
if err != nil {
267+
s.T().Fatalf("failed to create signed gateway certificate: %s", err)
268+
}
269+
270+
clientCaCert := x509.NewCertPool()
271+
clientCaCert.AddCert(caCert)
272+
273+
s.caCert = caCert
274+
s.caKey = caKey
275+
s.clientCaCertPool = clientCaCert
276+
257277
gwStartInfoCh := make(chan *gateway.StartupInfo, 1)
258278
gwCtx, gwCtxCancel := context.WithCancel(context.Background())
259279
gw, err := gateway.NewGateway(&gateway.Config{
@@ -265,6 +285,7 @@ func (s *GatewayOpsTestSuite) SetupSuite() {
265285
BindDapiPort: 0,
266286
GrpcCertificate: *gwCert,
267287
DapiCertificate: *gwCert,
288+
ClientCaCert: clientCaCert,
268289
AlphaEndpoints: true,
269290
NumInstances: 1,
270291
ProxyServices: []string{"query", "analytics", "mgmt", "search"},
@@ -308,6 +329,7 @@ func (s *GatewayOpsTestSuite) SetupSuite() {
308329
}
309330

310331
s.gatewayConn = conn
332+
s.gwConnAddr = connAddr
311333
s.gatewayCloseFunc = gwCtxCancel
312334
s.gatewayClosedCh = gwClosedCh
313335
s.dapiCli = dapiCli
@@ -330,6 +352,7 @@ func (s *GatewayOpsTestSuite) SetupSuite() {
330352
}
331353

332354
s.gatewayConn = conn
355+
s.gwConnAddr = connAddr
333356
s.dapiCli = dapiCli
334357
s.dapiAddr = dapiAddr
335358
}

testutils/dinocluster.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package testutils
33
import (
44
"bufio"
55
"context"
6+
"fmt"
67
"io"
78
"log"
89
"net/http"
@@ -93,6 +94,22 @@ func runDinoRemoveNode(node string) error {
9394
return runNoResDinoCmd([]string{"nodes", "rm", globalTestConfig.DinoId, node})
9495
}
9596

97+
func runDinoAddUser(username string, canRead, canWrite bool) error {
98+
return runNoResDinoCmd([]string{
99+
"users",
100+
"add",
101+
globalTestConfig.DinoId,
102+
username,
103+
"--password=password",
104+
fmt.Sprintf("--can-read=%v", canRead),
105+
fmt.Sprintf("--can-write=%v", canWrite),
106+
})
107+
}
108+
109+
func runDinoRemoveUser(username string) error {
110+
return runNoResDinoCmd([]string{"users", "remove", globalTestConfig.DinoId, username})
111+
}
112+
96113
type DinoController struct {
97114
t *testing.T
98115
oldFoSettings *cbmgmtx.GetAutoFailoverSettingsResponse
@@ -182,3 +199,13 @@ func (c *DinoController) RemoveNode(node string) {
182199
err := runDinoRemoveNode(node)
183200
require.NoError(c.t, err)
184201
}
202+
203+
func (c *DinoController) AddUser(username string, canRead, canWrite bool) {
204+
err := runDinoAddUser(username, canRead, canWrite)
205+
require.NoError(c.t, err)
206+
}
207+
208+
func (c *DinoController) RemoveUser(username string) {
209+
err := runDinoRemoveUser(username)
210+
require.NoError(c.t, err)
211+
}

0 commit comments

Comments
 (0)