Skip to content

Commit 0ab78fe

Browse files
committed
ING-1329: Add support for testing client cert auth
1 parent fa18d4c commit 0ab78fe

6 files changed

Lines changed: 289 additions & 1 deletion

File tree

.github/actions/install-cbdinocluster/action.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ runs:
1111
shell: bash
1212
run: |
1313
mkdir -p "$HOME/bin"
14-
wget -nv -O $HOME/bin/cbdinocluster https://github.com/couchbaselabs/cbdinocluster/releases/download/v0.0.86/cbdinocluster-linux-amd64
14+
wget -nv -O $HOME/bin/cbdinocluster https://github.com/couchbaselabs/cbdinocluster/releases/download/v0.0.95/cbdinocluster-linux-amd64
1515
chmod +x $HOME/bin/cbdinocluster
1616
echo "$HOME/bin" >> $GITHUB_PATH
1717
- name: Initialize cbdinocluster

.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: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
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.1.0-1203
20+
- 8.0.0
21+
- 7.6.8
22+
- 7.2.8
23+
24+
runs-on: ubuntu-latest
25+
steps:
26+
- uses: actions/checkout@v4
27+
with:
28+
submodules: recursive
29+
- name: Install cbdinocluster
30+
uses: ./.github/actions/install-cbdinocluster
31+
with:
32+
github-token: ${{ secrets.GITHUB_TOKEN }}
33+
- name: Start couchbase cluster
34+
id: start-cluster
35+
uses: ./.github/actions/start-couchbase-cluster
36+
- uses: actions/setup-go@v5
37+
with:
38+
go-version: 1.24
39+
- uses: arduino/setup-protoc@v3
40+
with:
41+
version: 31.1
42+
repo-token: ${{ secrets.GITHUB_TOKEN }}
43+
- name: Install Tools
44+
run: |
45+
go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.36
46+
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.5
47+
go install github.com/matryer/moq@v0.5
48+
- name: Install Dependencies
49+
run: go get ./...
50+
- name: Generate Files
51+
run: |
52+
go generate ./...
53+
- name: Run Test
54+
env:
55+
SGTEST_CBCONNSTR: ${{ steps.start-cluster.outputs.node-ip }}
56+
SGTEST_DINOID: ${{ steps.start-cluster.outputs.dino-id }}
57+
run: go test ./gateway/test -run TestGatewayOps -v -testify.m TestClientCertAuth
58+
59+
- name: Collect couchbase logs
60+
timeout-minutes: 10
61+
if: failure()
62+
run: |
63+
mkdir -p ./client-cert-auth-logs
64+
cbdinocluster -v collect-logs ${{ steps.start-cluster.outputs.dino-id }} ./client-cert-auth-logs
65+
- name: Upload couchbase logs
66+
if: failure()
67+
uses: actions/upload-artifact@v4
68+
with:
69+
name: cbcollect-logs-${{ matrix.server }}
70+
path: ./client-cert-auth-logs/*
71+
retention-days: 5

gateway/test/mtls_test.go

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
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/stretchr/testify/assert"
11+
"google.golang.org/grpc"
12+
"google.golang.org/grpc/codes"
13+
"google.golang.org/grpc/credentials"
14+
)
15+
16+
func (s *GatewayOpsTestSuite) TestClientCertAuth() {
17+
testutils.SkipIfNoDinoCluster(s.T())
18+
19+
s.Run("KvService", s.KvService)
20+
}
21+
22+
func (s *GatewayOpsTestSuite) KvService() {
23+
dino := testutils.StartDinoTesting(s.T(), false)
24+
username := "kvUser"
25+
conn := s.newClientCertConn(dino, username)
26+
kvClient := kv_v1.NewKvServiceClient(conn)
27+
getFn := func() (*kv_v1.GetResponse, error) {
28+
return kvClient.Get(context.Background(), &kv_v1.GetRequest{
29+
BucketName: s.bucketName,
30+
ScopeName: s.scopeName,
31+
CollectionName: s.collectionName,
32+
Key: s.testDocId(),
33+
})
34+
}
35+
36+
s.Run("UserMissing", func() {
37+
_, err := getFn()
38+
assertRpcStatus(s.T(), err, codes.PermissionDenied)
39+
assert.Contains(s.T(), err.Error(), "Your certificate is invalid")
40+
})
41+
42+
dino.AddUnprivilegedUser(username)
43+
time.Sleep(time.Second * 5)
44+
s.T().Cleanup(func() {
45+
dino.RemoveUser(username)
46+
})
47+
48+
s.Run("NoUserPermissions", func() {
49+
_, err := getFn()
50+
assertRpcStatus(s.T(), err, codes.PermissionDenied)
51+
assert.Contains(s.T(), err.Error(), "No permissions to read documents")
52+
})
53+
54+
dino.AddReadOnlyUser(username)
55+
time.Sleep(time.Second * 5)
56+
57+
s.Run("ReadSuccess", func() {
58+
resp, err := getFn()
59+
requireRpcSuccess(s.T(), resp, err)
60+
})
61+
62+
s.Run("NoWritePermission", func() {
63+
docId := s.randomDocId()
64+
_, err := kvClient.Upsert(context.Background(), &kv_v1.UpsertRequest{
65+
BucketName: s.bucketName,
66+
ScopeName: s.scopeName,
67+
CollectionName: s.collectionName,
68+
Key: docId,
69+
Content: &kv_v1.UpsertRequest_ContentUncompressed{
70+
ContentUncompressed: TEST_CONTENT,
71+
},
72+
ContentFlags: TEST_CONTENT_FLAGS,
73+
})
74+
assertRpcStatus(s.T(), err, codes.PermissionDenied)
75+
assert.Contains(s.T(), err.Error(), "No permissions to write documents")
76+
})
77+
78+
dino.AddWriteUser(username)
79+
time.Sleep(time.Second * 5)
80+
81+
s.Run("WriteSuccess", func() {
82+
docId := s.randomDocId()
83+
resp, err := kvClient.Upsert(context.Background(), &kv_v1.UpsertRequest{
84+
BucketName: s.bucketName,
85+
ScopeName: s.scopeName,
86+
CollectionName: s.collectionName,
87+
Key: docId,
88+
Content: &kv_v1.UpsertRequest_ContentUncompressed{
89+
ContentUncompressed: TEST_CONTENT,
90+
},
91+
ContentFlags: TEST_CONTENT_FLAGS,
92+
})
93+
requireRpcSuccess(s.T(), resp, err)
94+
assertValidCas(s.T(), resp.Cas)
95+
assertValidMutationToken(s.T(), resp.MutationToken, s.bucketName)
96+
})
97+
}
98+
99+
func (s *GatewayOpsTestSuite) newClientCertConn(dino *testutils.DinoController, username string) *grpc.ClientConn {
100+
res := dino.GetClientCert(username)
101+
102+
cert, err := tls.X509KeyPair([]byte(res), []byte(res))
103+
assert.NoError(s.T(), err)
104+
105+
conn, err := grpc.NewClient(s.gwConnAddr,
106+
grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{
107+
RootCAs: s.clientCaCertPool,
108+
Certificates: []tls.Certificate{cert},
109+
})))
110+
if err != nil {
111+
s.T().Fatalf("failed to connect to test gateway: %s", err)
112+
}
113+
114+
return conn
115+
}

gateway/test/suite_test.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@ package test
33
import (
44
"context"
55
"crypto/tls"
6+
"crypto/x509"
67
"encoding/base64"
78
"encoding/json"
9+
"encoding/pem"
810
"fmt"
911
"math/rand"
1012
"net/http"
@@ -41,6 +43,7 @@ type GatewayOpsTestSuite struct {
4143
testClusterInfo *testutils.CanonicalTestCluster
4244
gatewayCloseFunc func()
4345
gatewayConn *grpc.ClientConn
46+
gwConnAddr string
4447
gatewayClosedCh chan struct{}
4548
dapiCli *http.Client
4649
dapiAddr string
@@ -56,6 +59,8 @@ type GatewayOpsTestSuite struct {
5659
basicRestCreds string
5760
readRestCreds string
5861

62+
clientCaCertPool *x509.CertPool
63+
5964
clusterVersion *NodeVersion
6065
features []TestFeature
6166

@@ -269,6 +274,15 @@ func (s *GatewayOpsTestSuite) SetupSuite() {
269274
s.T().Fatalf("failed to create testing certificate: %s", err)
270275
}
271276

277+
s.clientCaCertPool = x509.NewCertPool()
278+
if testConfig.DinoId != "" {
279+
caCert := s.getDinoCaCert()
280+
281+
s.clientCaCertPool.AddCert(caCert)
282+
283+
gwCert = s.getServerCert()
284+
}
285+
272286
gwStartInfoCh := make(chan *gateway.StartupInfo, 1)
273287
gwCtx, gwCtxCancel := context.WithCancel(context.Background())
274288
gw, err := gateway.NewGateway(&gateway.Config{
@@ -280,6 +294,7 @@ func (s *GatewayOpsTestSuite) SetupSuite() {
280294
BindDapiPort: 0,
281295
GrpcCertificate: *gwCert,
282296
DapiCertificate: *gwCert,
297+
ClientCaCert: s.clientCaCertPool,
283298
AlphaEndpoints: true,
284299
NumInstances: 1,
285300
ProxyServices: []string{"query", "analytics", "mgmt", "search"},
@@ -324,6 +339,7 @@ func (s *GatewayOpsTestSuite) SetupSuite() {
324339
}
325340

326341
s.gatewayConn = conn
342+
s.gwConnAddr = connAddr
327343
s.gatewayCloseFunc = gwCtxCancel
328344
s.gatewayClosedCh = gwClosedCh
329345
s.dapiCli = dapiCli
@@ -346,6 +362,7 @@ func (s *GatewayOpsTestSuite) SetupSuite() {
346362
}
347363

348364
s.gatewayConn = conn
365+
s.gwConnAddr = connAddr
349366
s.dapiCli = dapiCli
350367
s.dapiAddr = dapiAddr
351368
}
@@ -388,6 +405,39 @@ func (s *GatewayOpsTestSuite) ParseSupportedFeatures(featsStr string) {
388405
}
389406
}
390407

408+
func (s *GatewayOpsTestSuite) getDinoCaCert() *x509.Certificate {
409+
res, err := testutils.GetDinoCACert()
410+
if err != nil {
411+
s.T().Fatalf("failed to get dino ca cert: %s", err)
412+
}
413+
414+
block, _ := pem.Decode([]byte(res))
415+
if block == nil {
416+
s.T().Fatalf("failed to decode dino ca cert: %s", err)
417+
}
418+
419+
cert, err := x509.ParseCertificate(block.Bytes)
420+
if err != nil {
421+
s.T().Fatalf("failed to parse dino ca cert: %s", err)
422+
}
423+
424+
return cert
425+
}
426+
427+
func (s *GatewayOpsTestSuite) getServerCert() *tls.Certificate {
428+
res, err := testutils.GetServerCert("127.0.0.1", "")
429+
if err != nil {
430+
s.T().Fatalf("failed to get server cert: %s", err)
431+
}
432+
433+
cert, err := tls.X509KeyPair([]byte(res), []byte(res))
434+
if err != nil {
435+
s.T().Fatalf("failed to parse server cert: %s", err)
436+
}
437+
438+
return &cert
439+
}
440+
391441
var TEST_CONTENT = []byte(`{"foo": "bar","obj":{"num":14,"arr":[2,5,8],"str":"zz"},"num":11,"arr":[3,6,9,12]}`)
392442
var TEST_CONTENT_FLAGS = uint32(0x01000000)
393443

testutils/dinocluster.go

Lines changed: 51 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,37 @@ func (c *DinoController) RemoveNode(node string) {
182199
err := runDinoRemoveNode(node)
183200
require.NoError(c.t, err)
184201
}
202+
203+
func (c *DinoController) AddUnprivilegedUser(username string) {
204+
err := runDinoAddUser(username, false, false)
205+
require.NoError(c.t, err)
206+
}
207+
208+
func (c *DinoController) AddReadOnlyUser(username string) {
209+
err := runDinoAddUser(username, true, false)
210+
require.NoError(c.t, err)
211+
}
212+
213+
func (c *DinoController) AddWriteUser(username string) {
214+
err := runDinoAddUser(username, true, true)
215+
require.NoError(c.t, err)
216+
}
217+
218+
func (c *DinoController) RemoveUser(username string) {
219+
err := runDinoRemoveUser(username)
220+
require.NoError(c.t, err)
221+
}
222+
223+
func (c *DinoController) GetClientCert(username string) string {
224+
res, err := runDinoCmd([]string{"certificates", "get-client-cert", username})
225+
require.NoError(c.t, err)
226+
return res
227+
}
228+
229+
func GetDinoCACert() (string, error) {
230+
return runDinoCmd([]string{"certificates", "get-dino-ca"})
231+
}
232+
233+
func GetServerCert(ip string, dns string) (string, error) {
234+
return runDinoCmd([]string{"certificates", "get-server-cert", "--ip", ip, "--dns", dns})
235+
}

0 commit comments

Comments
 (0)