Skip to content

Commit d387ad2

Browse files
committed
ING-1339: Support client cert auth for non-kv ops
ING-1339: Add support for client cert auth to data api ING-1339: Add mtls support for Data API proxy
1 parent 89a8ca8 commit d387ad2

File tree

17 files changed

+545
-92
lines changed

17 files changed

+545
-92
lines changed

.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.95/cbdinocluster-linux-amd64
14+
wget -nv -O $HOME/bin/cbdinocluster https://github.com/couchbaselabs/cbdinocluster/releases/download/v0.0.96/cbdinocluster-linux-amd64
1515
chmod +x $HOME/bin/cbdinocluster
1616
echo "$HOME/bin" >> $GITHUB_PATH
1717
- name: Initialize cbdinocluster

.github/workflows/client_cert_auth_test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ jobs:
5454
env:
5555
SGTEST_CBCONNSTR: ${{ steps.start-cluster.outputs.node-ip }}
5656
SGTEST_DINOID: ${{ steps.start-cluster.outputs.dino-id }}
57-
run: go test ./gateway/test -run TestGatewayOps -v -testify.m TestClientCertAuth
57+
run: go test ./gateway/test -run TestGatewayOps -v -testify.m ClientCertAuth
5858

5959
- name: Collect couchbase logs
6060
timeout-minutes: 10

dataapiv1/spec.yaml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1222,7 +1222,6 @@ components:
12221222
description: Header for authentication.
12231223
schema:
12241224
type: string
1225-
required: true
12261225
AcceptEncodingHeader:
12271226
in: header
12281227
name: Accept-Encoding

gateway/auth/cbauthauthenticator.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ func (a *CbAuthAuthenticator) ValidateConnStateForObo(ctx context.Context, connS
103103
return "", "", ErrInvalidCertificate
104104
}
105105

106-
return "", "", fmt.Errorf("failed to check certificate with cbauth: %s", err.Error())
106+
return "", "", fmt.Errorf("failed to check certificate with cbauth: %w", err)
107107
}
108108

109109
return info.User, info.Domain, nil

gateway/dapiimpl/dapiimpl.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ type NewOptions struct {
1818
ProxyServices []proxy.ServiceType
1919
ProxyBlockAdmin bool
2020
Debug bool
21+
22+
Username string
23+
Password string
2124
}
2225

2326
type Servers struct {
@@ -44,7 +47,11 @@ func New(opts *NewOptions) *Servers {
4447
opts.CbClient,
4548
opts.ProxyServices,
4649
opts.ProxyBlockAdmin,
47-
opts.Debug),
50+
opts.Debug,
51+
v1AuthHandler,
52+
opts.Username,
53+
opts.Password,
54+
),
4855
DataApiV1Server: server_v1.NewDataApiServer(
4956
opts.Logger.Named("dapi-serverv1"),
5057
v1ErrHandler,

gateway/dapiimpl/proxy/proxy.go

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package proxy
22

33
import (
44
"context"
5+
"encoding/base64"
6+
"errors"
57
"fmt"
68
"io"
79
"net/http"
@@ -16,7 +18,10 @@ import (
1618
"go.opentelemetry.io/otel/propagation"
1719

1820
"github.com/couchbase/gocbcorex"
21+
"github.com/couchbase/gocbcorex/cbauthx"
1922
"github.com/couchbase/gocbcorex/contrib/buildversion"
23+
"github.com/couchbase/stellar-gateway/gateway/auth"
24+
"github.com/couchbase/stellar-gateway/gateway/dapiimpl/server_v1"
2025
"go.opentelemetry.io/otel"
2126
"go.opentelemetry.io/otel/attribute"
2227
"go.opentelemetry.io/otel/metric"
@@ -51,6 +56,10 @@ type DataApiProxy struct {
5156
numRequests metric.Int64Counter
5257
ttfbMillis metric.Int64Histogram
5358
durationMillis metric.Int64Histogram
59+
60+
adminUsername string
61+
adminPassword string
62+
authHander *server_v1.AuthHandler
5463
}
5564

5665
func NewDataApiProxy(
@@ -59,6 +68,8 @@ func NewDataApiProxy(
5968
services []ServiceType,
6069
disableAdmin bool,
6170
debugMode bool,
71+
authHandler *server_v1.AuthHandler,
72+
username, password string,
6273
) *DataApiProxy {
6374
mux := http.NewServeMux()
6475

@@ -92,6 +103,9 @@ func NewDataApiProxy(
92103
numRequests: numRequests,
93104
ttfbMillis: ttfbMillis,
94105
durationMillis: durationMillis,
106+
adminUsername: username,
107+
adminPassword: password,
108+
authHander: authHandler,
95109
}
96110

97111
for _, serviceName := range services {
@@ -127,9 +141,13 @@ func (p *DataApiProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
127141
}
128142

129143
func (p *DataApiProxy) writeError(w http.ResponseWriter, err error, msg string) {
144+
p.writeErrorWithStatus(w, err, msg, 502)
145+
}
146+
147+
func (p *DataApiProxy) writeErrorWithStatus(w http.ResponseWriter, err error, msg string, status int) {
130148
p.logger.Debug(msg, zap.Error(err))
131149

132-
w.WriteHeader(502)
150+
w.WriteHeader(status)
133151

134152
if !p.debugMode {
135153
_, _ = fmt.Fprintf(w, "%s", msg)
@@ -252,6 +270,34 @@ func (p *DataApiProxy) proxyService(
252270
// copy some other details
253271
proxyReq.Header = r.Header
254272

273+
// If no auth header has been given, check for a client cert
274+
authHdr := proxyReq.Header.Get("Authorization")
275+
if authHdr == "" {
276+
oboUser, oboDomain, err := p.authHander.Authenticator.ValidateConnStateForObo(ctx, r.TLS)
277+
if err != nil {
278+
if errors.Is(err, auth.ErrInvalidCertificate) {
279+
p.writeError(w, err, "failed to validate certificate")
280+
return
281+
} else if errors.Is(err, cbauthx.ErrNoCert) {
282+
p.writeErrorWithStatus(w, err, "authorization header or client cert are required", 401)
283+
return
284+
}
285+
286+
p.writeErrorWithStatus(w, err, "received an unexpected cert authentication error", 401)
287+
return
288+
}
289+
290+
// We only set the on behalf of and auth headers if there is a user, if
291+
// we set the onbehalf of header to an empty string and use the admin
292+
// creds then the server seems to ignore the obo header.
293+
if oboUser != "" {
294+
// ING-1371 (support CNG -> cluster mtls)
295+
oboHdrStr := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", oboUser, oboDomain)))
296+
proxyReq.Header.Set("cb-on-behalf-of", oboHdrStr)
297+
proxyReq.SetBasicAuth(p.adminUsername, p.adminPassword)
298+
}
299+
}
300+
255301
tr := otelhttp.NewTransport(
256302
roundTripper,
257303
// By setting the otelhttptrace client in this transport, it can be

gateway/dapiimpl/server_v1/authhandler.go

Lines changed: 48 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package server_v1
22

33
import (
44
"context"
5+
"crypto/tls"
56
"errors"
67
"net/http"
78

@@ -19,6 +20,8 @@ type AuthHandler struct {
1920
CbClient *gocbcorex.BucketsTrackingAgentManager
2021
}
2122

23+
type CtxKeyTlsConnState struct{}
24+
2225
func (a AuthHandler) getUserPassFromRequest(authHdr string) (string, string, error) {
2326
// we reuse the Basic auth parsing built into the go net library
2427
r := http.Request{
@@ -35,23 +38,60 @@ func (a AuthHandler) getUserPassFromRequest(authHdr string) (string, string, err
3538
return username, password, nil
3639
}
3740

38-
func (a AuthHandler) MaybeGetUserPassFromRequest(authHdr string) (string, string, *Status) {
39-
username, password, err := a.getUserPassFromRequest(authHdr)
41+
func (a AuthHandler) MaybeGetUserPassFromRequest(authHdr *string) (string, string, *Status) {
42+
if authHdr == nil {
43+
return "", "", nil
44+
}
45+
46+
username, password, err := a.getUserPassFromRequest(*authHdr)
4047
if err != nil {
4148
return "", "", a.ErrorHandler.NewInvalidAuthHeaderStatus(err)
4249
}
4350

4451
return username, password, nil
4552
}
4653

47-
func (a AuthHandler) MaybeGetOboUserFromContext(ctx context.Context, authHdr string) (string, string, *Status) {
54+
func (a AuthHandler) MaybeGetConnStateFromContext(ctx context.Context) (*tls.ConnectionState, *Status) {
55+
connState, ok := ctx.Value(CtxKeyTlsConnState{}).(*tls.ConnectionState)
56+
if connState == nil || !ok {
57+
return nil, nil
58+
}
59+
60+
return connState, nil
61+
}
62+
63+
func (a AuthHandler) MaybeGetOboUserFromContext(ctx context.Context, authHdr *string) (string, string, *Status) {
4864
username, password, errHe := a.MaybeGetUserPassFromRequest(authHdr)
4965
if errHe != nil {
5066
return "", "", errHe
5167
}
5268

53-
if username == "" && password == "" {
54-
return "", "", nil
69+
connState, errSt := a.MaybeGetConnStateFromContext(ctx)
70+
if errSt != nil {
71+
return "", "", errSt
72+
}
73+
74+
credsFound := username != "" && password != ""
75+
certFound := connState != nil && len(connState.PeerCertificates) != 0
76+
77+
switch {
78+
case !credsFound && !certFound:
79+
return "", "", a.ErrorHandler.NewNoAuthStatus()
80+
case credsFound && certFound:
81+
a.Logger.Debug("username/password taking priority over client cert auth as both were given.")
82+
case credsFound:
83+
case certFound:
84+
oboUser, oboDomain, err := a.Authenticator.ValidateConnStateForObo(ctx, connState)
85+
if err != nil {
86+
if errors.Is(err, auth.ErrInvalidCertificate) {
87+
return "", "", a.ErrorHandler.NewInvalidCertificateStatus()
88+
}
89+
90+
a.Logger.Error("received an unexpected cert authentication error", zap.Error(err))
91+
return "", "", a.ErrorHandler.NewInternalStatus()
92+
}
93+
94+
return oboUser, oboDomain, nil
5595
}
5696

5797
oboUser, oboDomain, err := a.Authenticator.ValidateUserForObo(ctx, username, password)
@@ -67,7 +107,7 @@ func (a AuthHandler) MaybeGetOboUserFromContext(ctx context.Context, authHdr str
67107
return oboUser, oboDomain, nil
68108
}
69109

70-
func (a AuthHandler) GetOboUserFromRequest(ctx context.Context, authHdr string) (string, string, *Status) {
110+
func (a AuthHandler) GetOboUserFromRequest(ctx context.Context, authHdr *string) (string, string, *Status) {
71111
user, domain, st := a.MaybeGetOboUserFromContext(ctx, authHdr)
72112
if st != nil {
73113
return "", "", st
@@ -81,7 +121,7 @@ func (a AuthHandler) GetOboUserFromRequest(ctx context.Context, authHdr string)
81121
}
82122

83123
func (a AuthHandler) GetHttpOboInfoFromContext(ctx context.Context, authHdr string) (*cbhttpx.OnBehalfOfInfo, *Status) {
84-
username, password, errHe := a.MaybeGetUserPassFromRequest(authHdr)
124+
username, password, errHe := a.MaybeGetUserPassFromRequest(&authHdr)
85125
if errHe != nil {
86126
return nil, errHe
87127
}
@@ -119,7 +159,7 @@ func (a AuthHandler) getBucketAgent(ctx context.Context, bucketName string) (*go
119159
}
120160

121161
func (a AuthHandler) GetMemdOboAgent(
122-
ctx context.Context, authHdr string, bucketName string,
162+
ctx context.Context, authHdr *string, bucketName string,
123163
) (*gocbcorex.Agent, string, *Status) {
124164
oboUser, _, errHe := a.GetOboUserFromRequest(ctx, authHdr)
125165
if errHe != nil {

gateway/dapiimpl/server_v1/errorhandler.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,24 @@ func (e ErrorHandler) NewInvalidCredentialsStatus() *Status {
121121
return st
122122
}
123123

124+
func (e ErrorHandler) NewUnexpectedAuthTypeStatus() *Status {
125+
st := &Status{
126+
StatusCode: http.StatusBadRequest,
127+
Code: dataapiv1.ErrorCodeInvalidArgument,
128+
Message: "Unexpected auth type.",
129+
}
130+
return st
131+
}
132+
133+
func (e ErrorHandler) NewInvalidCertificateStatus() *Status {
134+
st := &Status{
135+
StatusCode: http.StatusForbidden,
136+
Code: dataapiv1.ErrorCodeInvalidAuth,
137+
Message: "Your certificate is invalid.",
138+
}
139+
return st
140+
}
141+
124142
func (e ErrorHandler) NewInternalStatus() *Status {
125143
st := &Status{
126144
StatusCode: http.StatusInternalServerError,

gateway/dapiimpl/tlsconnstate.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package dapiimpl
2+
3+
import (
4+
"context"
5+
"net/http"
6+
7+
"github.com/couchbase/stellar-gateway/gateway/dapiimpl/server_v1"
8+
"github.com/oapi-codegen/runtime/strictmiddleware/nethttp"
9+
)
10+
11+
func NewTlsConnStateHandler() func(f nethttp.StrictHTTPHandlerFunc, operationID string) nethttp.StrictHTTPHandlerFunc {
12+
return func(f nethttp.StrictHTTPHandlerFunc, operationID string) nethttp.StrictHTTPHandlerFunc {
13+
return func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (response interface{}, err error) {
14+
ctx = context.WithValue(ctx, server_v1.CtxKeyTlsConnState{}, r.TLS)
15+
return f(ctx, w, r, request)
16+
}
17+
}
18+
}

gateway/dataimpl/server_v1/authhandler.go

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ func (a AuthHandler) MaybeGetOboUserFromContext(ctx context.Context) (string, st
7979

8080
switch {
8181
case !credsFound && !certFound:
82-
return "", "", nil
82+
return "", "", a.ErrorHandler.NewNoAuthStatus()
8383
case credsFound && certFound:
8484
a.Logger.Debug("username/password taking priority over client cert auth as both were given.")
8585
case credsFound:
@@ -129,8 +129,35 @@ func (a AuthHandler) GetHttpOboInfoFromContext(ctx context.Context) (*cbhttpx.On
129129
return nil, errSt
130130
}
131131

132-
if username == "" {
132+
connState, errHe := a.MaybeGetConnStateFromContext(ctx)
133+
if errHe != nil {
134+
return nil, errHe
135+
}
136+
137+
credsFound := username != "" && password != ""
138+
certFound := connState != nil && len(connState.PeerCertificates) != 0
139+
140+
switch {
141+
case !credsFound && !certFound:
133142
return nil, a.ErrorHandler.NewNoAuthStatus()
143+
case credsFound && certFound:
144+
a.Logger.Debug("username/password taking priority over client cert auth as both were given.")
145+
case credsFound:
146+
case certFound:
147+
oboUser, oboDomain, err := a.Authenticator.ValidateConnStateForObo(ctx, connState)
148+
if err != nil {
149+
if errors.Is(err, auth.ErrInvalidCertificate) {
150+
return nil, a.ErrorHandler.NewInvalidCertificateStatus()
151+
}
152+
153+
a.Logger.Error("received an unexpected cert authentication error", zap.Error(err))
154+
return nil, a.ErrorHandler.NewInternalStatus()
155+
}
156+
157+
return &cbhttpx.OnBehalfOfInfo{
158+
Username: oboUser,
159+
Domain: oboDomain,
160+
}, nil
134161
}
135162

136163
return &cbhttpx.OnBehalfOfInfo{

0 commit comments

Comments
 (0)