Skip to content

Commit 1377d19

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

File tree

8 files changed

+278
-19
lines changed

8 files changed

+278
-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

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: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
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) TestKvServiceMtls() {
18+
// TO DO - is there a way to skip if client cert auth is not enabled
19+
testutils.SkipIfNoDinoCluster(s.T())
20+
21+
dino := testutils.StartDinoTesting(s.T(), false)
22+
23+
// Create a user without any permissions
24+
username := "kvUser"
25+
dino.AddUser(username, false, false)
26+
27+
s.T().Cleanup(func() {
28+
dino.RemoveUser(username)
29+
})
30+
31+
clientCert, err := certificates.GenerateSignedClientCert(s.caCert, s.caKey, username)
32+
assert.NoError(s.T(), err)
33+
34+
conn, err := grpc.NewClient(s.gwConnAddr,
35+
grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{
36+
RootCAs: s.clientCaCertPool,
37+
Certificates: []tls.Certificate{*clientCert},
38+
InsecureSkipVerify: false,
39+
})))
40+
if err != nil {
41+
s.T().Fatalf("failed to connect to test gateway: %s", err)
42+
}
43+
44+
kvClient := kv_v1.NewKvServiceClient(conn)
45+
46+
s.Run("ReadFailure", func() {
47+
_, err := kvClient.Get(context.Background(), &kv_v1.GetRequest{
48+
BucketName: s.bucketName,
49+
ScopeName: s.scopeName,
50+
CollectionName: s.collectionName,
51+
Key: s.testDocId(),
52+
})
53+
assertRpcStatus(s.T(), err, codes.PermissionDenied)
54+
assert.Contains(s.T(), err.Error(), "No permissions to read documents")
55+
})
56+
57+
// Add read permissions to the user and wait for this to take effect
58+
dino.AddUser(username, true, false)
59+
time.Sleep(time.Second * 5)
60+
61+
s.Run("ReadSuccess", func() {
62+
resp, 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+
requireRpcSuccess(s.T(), resp, err)
69+
assertValidCas(s.T(), resp.Cas)
70+
assert.Equal(s.T(), TEST_CONTENT, resp.GetContentUncompressed())
71+
assert.Nil(s.T(), resp.GetContentCompressed())
72+
assert.Equal(s.T(), TEST_CONTENT_FLAGS, resp.ContentFlags)
73+
assert.Nil(s.T(), resp.Expiry)
74+
})
75+
76+
s.Run("WriteFailure", func() {
77+
docId := s.randomDocId()
78+
_, err := kvClient.Upsert(context.Background(), &kv_v1.UpsertRequest{
79+
BucketName: s.bucketName,
80+
ScopeName: s.scopeName,
81+
CollectionName: s.collectionName,
82+
Key: docId,
83+
Content: &kv_v1.UpsertRequest_ContentUncompressed{
84+
ContentUncompressed: TEST_CONTENT,
85+
},
86+
ContentFlags: TEST_CONTENT_FLAGS,
87+
})
88+
assertRpcStatus(s.T(), err, codes.PermissionDenied)
89+
assert.Contains(s.T(), err.Error(), "No permissions to write documents")
90+
})
91+
92+
// Add write permissions to the user
93+
dino.AddUser(username, false, true)
94+
time.Sleep(time.Second * 5)
95+
96+
s.Run("WriteSuccess", func() {
97+
docId := s.randomDocId()
98+
resp, err := kvClient.Upsert(context.Background(), &kv_v1.UpsertRequest{
99+
BucketName: s.bucketName,
100+
ScopeName: s.scopeName,
101+
CollectionName: s.collectionName,
102+
Key: docId,
103+
Content: &kv_v1.UpsertRequest_ContentUncompressed{
104+
ContentUncompressed: TEST_CONTENT,
105+
},
106+
ContentFlags: TEST_CONTENT_FLAGS,
107+
})
108+
requireRpcSuccess(s.T(), resp, err)
109+
assertValidCas(s.T(), resp.Cas)
110+
assertValidMutationToken(s.T(), resp.MutationToken, s.bucketName)
111+
})
112+
}

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+
}

utils/certificates/certificates.go

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
package certificates
2+
3+
import (
4+
"crypto/ecdsa"
5+
"crypto/elliptic"
6+
"crypto/rand"
7+
"crypto/tls"
8+
"crypto/x509"
9+
"fmt"
10+
"math/big"
11+
"net"
12+
"time"
13+
14+
"github.com/couchbase/stellar-gateway/utils/selfsignedcert"
15+
"github.com/pkg/errors"
16+
)
17+
18+
func GenerateSignedServerCert(caCert *x509.Certificate, caKey *ecdsa.PrivateKey) (*tls.Certificate, error) {
19+
priv, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
20+
if err != nil {
21+
panic(err)
22+
}
23+
24+
template := x509.Certificate{
25+
SerialNumber: big.NewInt(1),
26+
NotBefore: time.Now(),
27+
NotAfter: time.Now().Add(7 * 24 * time.Hour),
28+
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
29+
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
30+
BasicConstraintsValid: true,
31+
IPAddresses: []net.IP{net.ParseIP("127.0.0.1")},
32+
}
33+
34+
derBytes, err := x509.CreateCertificate(rand.Reader, &template, caCert, &priv.PublicKey, caKey)
35+
if err != nil {
36+
return nil, errors.Wrap(err, "failed to create signed server certificate")
37+
}
38+
39+
cert, err := x509.ParseCertificate(derBytes)
40+
if err != nil {
41+
return nil, errors.Wrap(err, "failed to parse cert from bytes")
42+
}
43+
44+
return selfsignedcert.ConstructTlsCert(cert, priv)
45+
}
46+
47+
func GenerateSignedClientCert(caCert *x509.Certificate, caKey *ecdsa.PrivateKey, username string) (*tls.Certificate, error) {
48+
priv, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
49+
if err != nil {
50+
panic(err)
51+
}
52+
53+
template := x509.Certificate{
54+
NotBefore: time.Now(),
55+
NotAfter: time.Now().Add(7 * 24 * time.Hour),
56+
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
57+
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
58+
BasicConstraintsValid: true,
59+
EmailAddresses: []string{fmt.Sprintf("%[email protected]", username)},
60+
}
61+
62+
derBytes, err := x509.CreateCertificate(rand.Reader, &template, caCert, &priv.PublicKey, caKey)
63+
if err != nil {
64+
return nil, errors.Wrap(err, "failed to create singed server certificate")
65+
}
66+
67+
cert, err := x509.ParseCertificate(derBytes)
68+
if err != nil {
69+
return nil, errors.Wrap(err, "failed to parse cert from bytes")
70+
}
71+
72+
return selfsignedcert.ConstructTlsCert(cert, priv)
73+
}

0 commit comments

Comments
 (0)