Skip to content

Commit 6e7a223

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

File tree

9 files changed

+366
-19
lines changed

9 files changed

+366
-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: 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/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+
username := "kvUser"
26+
conn := s.newClientCertConn(username)
27+
kvClient := kv_v1.NewKvServiceClient(conn)
28+
getFn := func() (*kv_v1.GetResponse, error) {
29+
return kvClient.Get(context.Background(), &kv_v1.GetRequest{
30+
BucketName: s.bucketName,
31+
ScopeName: s.scopeName,
32+
CollectionName: s.collectionName,
33+
Key: s.testDocId(),
34+
})
35+
}
36+
37+
s.Run("UserMissing", func() {
38+
_, err := getFn()
39+
assertRpcStatus(s.T(), err, codes.PermissionDenied)
40+
assert.Contains(s.T(), err.Error(), "Your certificate is invalid")
41+
})
42+
43+
dino.AddUnprivilegedUser(username)
44+
time.Sleep(time.Second * 5)
45+
s.T().Cleanup(func() {
46+
dino.RemoveUser(username)
47+
})
48+
49+
s.Run("NoUserPermissions", func() {
50+
_, err := getFn()
51+
assertRpcStatus(s.T(), err, codes.PermissionDenied)
52+
assert.Contains(s.T(), err.Error(), "No permissions to read documents")
53+
})
54+
55+
dino.AddReadOnlyUser(username)
56+
time.Sleep(time.Second * 5)
57+
58+
s.Run("ReadSuccess", func() {
59+
resp, err := getFn()
60+
requireRpcSuccess(s.T(), resp, err)
61+
})
62+
63+
s.Run("NoWritePermission", func() {
64+
docId := s.randomDocId()
65+
_, err := kvClient.Upsert(context.Background(), &kv_v1.UpsertRequest{
66+
BucketName: s.bucketName,
67+
ScopeName: s.scopeName,
68+
CollectionName: s.collectionName,
69+
Key: docId,
70+
Content: &kv_v1.UpsertRequest_ContentUncompressed{
71+
ContentUncompressed: TEST_CONTENT,
72+
},
73+
ContentFlags: TEST_CONTENT_FLAGS,
74+
})
75+
assertRpcStatus(s.T(), err, codes.PermissionDenied)
76+
assert.Contains(s.T(), err.Error(), "No permissions to write documents")
77+
})
78+
79+
dino.AddWriteUser(username)
80+
time.Sleep(time.Second * 5)
81+
82+
s.Run("WriteSuccess", func() {
83+
docId := s.randomDocId()
84+
resp, err := kvClient.Upsert(context.Background(), &kv_v1.UpsertRequest{
85+
BucketName: s.bucketName,
86+
ScopeName: s.scopeName,
87+
CollectionName: s.collectionName,
88+
Key: docId,
89+
Content: &kv_v1.UpsertRequest_ContentUncompressed{
90+
ContentUncompressed: TEST_CONTENT,
91+
},
92+
ContentFlags: TEST_CONTENT_FLAGS,
93+
})
94+
requireRpcSuccess(s.T(), resp, err)
95+
assertValidCas(s.T(), resp.Cas)
96+
assertValidMutationToken(s.T(), resp.MutationToken, s.bucketName)
97+
})
98+
}
99+
100+
func (s *GatewayOpsTestSuite) newClientCertConn(username string) *grpc.ClientConn {
101+
cert, err := certificates.GenerateSignedClientCert(s.caCert, s.caKey, username)
102+
assert.NoError(s.T(), err)
103+
104+
conn, err := grpc.NewClient(s.gwConnAddr,
105+
grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{
106+
RootCAs: s.clientCaCertPool,
107+
Certificates: []tls.Certificate{*cert},
108+
InsecureSkipVerify: false,
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: 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"
@@ -24,6 +26,7 @@ import (
2426
"github.com/couchbase/stellar-gateway/contrib/grpcheaderauth"
2527
"github.com/couchbase/stellar-gateway/gateway"
2628
"github.com/couchbase/stellar-gateway/testutils"
29+
"github.com/couchbase/stellar-gateway/utils/certificates"
2730
"github.com/couchbase/stellar-gateway/utils/selfsignedcert"
2831
"github.com/google/uuid"
2932
"github.com/stretchr/testify/assert"
@@ -41,6 +44,7 @@ type GatewayOpsTestSuite struct {
4144
testClusterInfo *testutils.CanonicalTestCluster
4245
gatewayCloseFunc func()
4346
gatewayConn *grpc.ClientConn
47+
gwConnAddr string
4448
gatewayClosedCh chan struct{}
4549
dapiCli *http.Client
4650
dapiAddr string
@@ -56,6 +60,10 @@ type GatewayOpsTestSuite struct {
5660
basicRestCreds string
5761
readRestCreds string
5862

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

@@ -264,11 +272,23 @@ func (s *GatewayOpsTestSuite) SetupSuite() {
264272
s.T().Fatalf("failed to initialize test logging: %s", err)
265273
}
266274

267-
gwCert, err := selfsignedcert.GenerateCertificate()
275+
caCert, caKey, err := selfsignedcert.GenerateCertificate()
268276
if err != nil {
269277
s.T().Fatalf("failed to create testing certificate: %s", err)
270278
}
271279

280+
gwCert, err := certificates.GenerateSignedServerCert(caCert, caKey)
281+
if err != nil {
282+
s.T().Fatalf("failed to create signed gateway certificate: %s", err)
283+
}
284+
285+
clientCaCert := x509.NewCertPool()
286+
clientCaCert.AddCert(caCert)
287+
288+
s.caCert = caCert
289+
s.caKey = caKey
290+
s.clientCaCertPool = clientCaCert
291+
272292
gwStartInfoCh := make(chan *gateway.StartupInfo, 1)
273293
gwCtx, gwCtxCancel := context.WithCancel(context.Background())
274294
gw, err := gateway.NewGateway(&gateway.Config{
@@ -280,6 +300,7 @@ func (s *GatewayOpsTestSuite) SetupSuite() {
280300
BindDapiPort: 0,
281301
GrpcCertificate: *gwCert,
282302
DapiCertificate: *gwCert,
303+
ClientCaCert: clientCaCert,
283304
AlphaEndpoints: true,
284305
NumInstances: 1,
285306
ProxyServices: []string{"query", "analytics", "mgmt", "search"},
@@ -324,6 +345,7 @@ func (s *GatewayOpsTestSuite) SetupSuite() {
324345
}
325346

326347
s.gatewayConn = conn
348+
s.gwConnAddr = connAddr
327349
s.gatewayCloseFunc = gwCtxCancel
328350
s.gatewayClosedCh = gwClosedCh
329351
s.dapiCli = dapiCli
@@ -346,6 +368,7 @@ func (s *GatewayOpsTestSuite) SetupSuite() {
346368
}
347369

348370
s.gatewayConn = conn
371+
s.gwConnAddr = connAddr
349372
s.dapiCli = dapiCli
350373
s.dapiAddr = dapiAddr
351374
}

testutils/dinocluster.go

Lines changed: 42 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,28 @@ 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) AddUnprivilegedUser(username string) {
209+
err := runDinoAddUser(username, false, false)
210+
require.NoError(c.t, err)
211+
}
212+
213+
func (c *DinoController) AddReadOnlyUser(username string) {
214+
err := runDinoAddUser(username, true, false)
215+
require.NoError(c.t, err)
216+
}
217+
218+
func (c *DinoController) AddWriteUser(username string) {
219+
err := runDinoAddUser(username, true, true)
220+
require.NoError(c.t, err)
221+
}
222+
223+
func (c *DinoController) RemoveUser(username string) {
224+
err := runDinoRemoveUser(username)
225+
require.NoError(c.t, err)
226+
}

0 commit comments

Comments
 (0)