Skip to content

Commit 23614a1

Browse files
committed
ING-1339: Add mtls support for Data API proxy
1 parent d3f68a1 commit 23614a1

File tree

9 files changed

+279
-13
lines changed

9 files changed

+279
-13
lines changed

.github/workflows/client_cert_auth_test.yml

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,10 @@ jobs:
1616
strategy:
1717
matrix:
1818
server:
19-
- 8.0.0-3534
20-
- 7.6.5
21-
- 7.2.2
19+
- 8.1.0-1203
20+
- 8.0.0
21+
- 7.6.8
22+
- 7.2.8
2223

2324
runs-on: ubuntu-latest
2425
steps:
@@ -53,7 +54,7 @@ jobs:
5354
env:
5455
SGTEST_CBCONNSTR: ${{ steps.start-cluster.outputs.node-ip }}
5556
SGTEST_DINOID: ${{ steps.start-cluster.outputs.dino-id }}
56-
run: go test ./gateway/test -run TestGatewayOps -v -testify.m TestClientCertAuth
57+
run: go test ./gateway/test -run TestGatewayOps -v -testify.m ClientCertAuth
5758

5859
- name: Collect couchbase logs
5960
timeout-minutes: 10

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 & 2 deletions
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"
@@ -45,12 +50,16 @@ type DataApiProxy struct {
4550
cbClient *gocbcorex.BucketsTrackingAgentManager
4651
disableAdmin bool
4752
debugMode bool
48-
mux *http.ServeMux
53+
mux http.Handler
4954

5055
numFailures metric.Int64Counter
5156
numRequests metric.Int64Counter
5257
ttfbMillis metric.Int64Histogram
5358
durationMillis metric.Int64Histogram
59+
60+
username string
61+
password 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+
username: username,
107+
password: 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,33 @@ 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 cetificate")
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+
oboHdrStr := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", oboUser, oboDomain)))
295+
proxyReq.Header.Set("cb-on-behalf-of", oboHdrStr)
296+
proxyReq.SetBasicAuth(p.username, p.password)
297+
}
298+
}
299+
255300
tr := otelhttp.NewTransport(
256301
roundTripper,
257302
// By setting the otelhttptrace client in this transport, it can be

gateway/dataimpl/server_v1/authhandler.go

Lines changed: 2 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:
@@ -139,7 +139,7 @@ func (a AuthHandler) GetHttpOboInfoFromContext(ctx context.Context) (*cbhttpx.On
139139

140140
switch {
141141
case !credsFound && !certFound:
142-
return nil, nil
142+
return nil, a.ErrorHandler.NewNoAuthStatus()
143143
case credsFound && certFound:
144144
a.Logger.Debug("username/password taking priority over client cert auth as both were given.")
145145
case credsFound:

gateway/gateway.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,8 @@ func (g *Gateway) Run(ctx context.Context) error {
400400
Authenticator: authenticator,
401401
ProxyServices: proxyServices,
402402
ProxyBlockAdmin: config.ProxyBlockAdmin,
403+
Username: config.Username,
404+
Password: config.Password,
403405
})
404406

405407
config.Logger.Info("initializing protostellar system")

gateway/test/dapi_crud_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ func (s *GatewayOpsTestSuite) RunCommonDapiErrorCases(
9595
})
9696
})
9797

98-
s.Run("Unauthenticated", func() {
98+
s.Run("P%", func() {
9999
resp := fn(&commonDapiTestData{
100100
BucketName: s.bucketName,
101101
ScopeName: s.scopeName,
@@ -106,7 +106,7 @@ func (s *GatewayOpsTestSuite) RunCommonDapiErrorCases(
106106
DocumentKey: s.randomDocId(),
107107
})
108108
require.NotNil(s.T(), resp)
109-
require.Equal(s.T(), http.StatusBadRequest, resp.StatusCode)
109+
require.Equal(s.T(), http.StatusUnauthorized, resp.StatusCode)
110110
// Authorization header missing is considered a missing parameter rather
111111
// than an authentication specific error.
112112
})

0 commit comments

Comments
 (0)