2021-10-19 17:21:41 -04:00
// Copyright (c) HashiCorp, Inc.
2023-02-02 17:15:17 -08:00
// SPDX-License-Identifier: BUSL-1.1
2021-10-19 17:21:41 -04:00
package controller
import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"strconv"
2021-10-23 21:23:49 -04:00
"sync"
2021-10-19 17:21:41 -04:00
"testing"
2024-02-13 09:17:41 -08:00
"github.com/google/go-cmp/cmp"
2024-04-06 13:48:47 -07:00
"github.com/hashicorp/boundary/globals"
2024-02-13 09:17:41 -08:00
"github.com/hashicorp/boundary/internal/alias"
talias "github.com/hashicorp/boundary/internal/alias/target"
2021-10-19 17:21:41 -04:00
"github.com/hashicorp/boundary/internal/authtoken"
2022-05-09 12:45:10 -07:00
"github.com/hashicorp/boundary/internal/daemon/controller/auth"
"github.com/hashicorp/boundary/internal/daemon/controller/common"
"github.com/hashicorp/boundary/internal/daemon/controller/handlers"
2021-10-19 17:21:41 -04:00
"github.com/hashicorp/boundary/internal/db"
"github.com/hashicorp/boundary/internal/errors"
2023-09-12 08:15:34 -04:00
"github.com/hashicorp/boundary/internal/event"
2021-10-19 17:21:41 -04:00
pb_api "github.com/hashicorp/boundary/internal/gen/controller/api"
2024-02-13 09:17:41 -08:00
pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services"
2021-10-19 17:21:41 -04:00
authpb "github.com/hashicorp/boundary/internal/gen/controller/auth"
pberrors "github.com/hashicorp/boundary/internal/gen/errors"
"github.com/hashicorp/boundary/internal/gen/testing/interceptor"
"github.com/hashicorp/boundary/internal/iam"
"github.com/hashicorp/boundary/internal/kms"
2022-06-20 18:20:17 -07:00
"github.com/hashicorp/boundary/internal/server"
2024-02-13 09:17:41 -08:00
"github.com/hashicorp/boundary/internal/target/tcp"
2025-06-20 15:31:57 -04:00
"github.com/hashicorp/boundary/internal/types/resource"
2021-10-23 21:23:49 -04:00
"github.com/hashicorp/go-hclog"
2024-04-06 13:48:47 -07:00
"github.com/hashicorp/go-uuid"
2021-10-19 17:21:41 -04:00
"github.com/mr-tron/base58"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
2023-05-20 07:23:10 -04:00
"google.golang.org/genproto/googleapis/api/httpbody"
2021-10-19 17:21:41 -04:00
"google.golang.org/grpc"
"google.golang.org/grpc/metadata"
"google.golang.org/protobuf/proto"
2024-02-13 09:17:41 -08:00
"google.golang.org/protobuf/testing/protocmp"
2021-10-19 17:21:41 -04:00
)
2023-05-20 05:45:39 -04:00
func Test_unaryCtxInterceptor ( t * testing . T ) {
2023-06-06 17:09:08 -04:00
ctx := context . Background ()
2021-10-19 17:21:41 -04:00
conn , _ := db . TestSetup ( t , "postgres" )
rw := db . New ( conn )
wrapper := db . TestWrapper ( t )
kmsCache := kms . TestKms ( t , conn , wrapper )
iamRepo := iam . TestRepo ( t , conn , wrapper )
iamRepoFn := func () ( * iam . Repository , error ) {
return iamRepo , nil
}
atRepoFn := func () ( * authtoken . Repository , error ) {
2023-06-06 17:09:08 -04:00
return authtoken . NewRepository ( ctx , rw , rw , kmsCache )
2021-10-19 17:21:41 -04:00
}
2022-06-20 18:20:17 -07:00
serversRepoFn := func () ( * server . Repository , error ) {
2023-06-06 17:09:08 -04:00
return server . NewRepository ( ctx , rw , rw , kmsCache )
2021-10-19 17:21:41 -04:00
}
validGatewayTicket := "valid-ticket"
o , _ := iam . TestScopes ( t , iamRepo )
at := authtoken . TestAuthToken ( t , conn , kmsCache , o . GetPublicId ())
encToken , err := authtoken . EncryptToken ( context . Background (), kmsCache , o . GetPublicId (), at . GetPublicId (), at . GetToken ())
require . NoError ( t , err )
tokValue := at . GetPublicId () + "_" + encToken
newReqCtx := func ( gwTicket string ) context . Context {
req := httptest . NewRequest ( "GET" , "http://127.0.0.1/v1/scopes/o_1" , nil )
req . Header . Set ( "Authorization" , fmt . Sprintf ( "Bearer %s" , tokValue ))
// Add values for authn/authz checking
requestInfo := authpb . RequestInfo {
2021-10-23 21:23:49 -04:00
Path : req . URL . Path ,
Method : req . Method ,
EventId : "test-event-id" ,
TraceId : "test-trace-id" ,
2021-10-19 17:21:41 -04:00
}
requestInfo . PublicId , requestInfo . EncryptedToken , requestInfo . TokenFormat = auth . GetTokenFromRequest ( context . TODO (), kmsCache , req )
requestInfo . Ticket = gwTicket // allows the grpc-gateway to verify the request info came from it's in-memory companion http proxy
marshalledRequestInfo , err := proto . Marshal ( & requestInfo )
require . NoError ( t , err )
md := metadata . Pairs ( requestInfoMdKey , base58 . FastBase58Encoding ( marshalledRequestInfo ))
mdCtx := metadata . NewIncomingContext ( context . Background (), md )
md , ok := metadata . FromIncomingContext ( mdCtx )
require . True ( t , ok )
require . NotNil ( t , md )
return mdCtx
}
factoryCtx := context . Background ()
2022-10-25 18:29:38 -05:00
returnCtxHandler := func ( ctx context . Context , req any ) ( any , error ) {
2021-10-19 17:21:41 -04:00
return ctx , nil
}
2023-05-20 05:45:39 -04:00
c := event . TestEventerConfig ( t , "Test_unaryCtxInterceptor" , event . TestWithAuditSink ( t ), event . TestWithObservationSink ( t ))
2021-10-23 21:23:49 -04:00
testLock := & sync . Mutex {}
testLogger := hclog . New ( & hclog . LoggerOptions {
Mutex : testLock ,
Name : "test" ,
})
2023-05-20 05:45:39 -04:00
testEventer , err := event . NewEventer ( testLogger , testLock , "Test_unaryCtxInterceptor" , c . EventerConfig )
2021-10-23 21:23:49 -04:00
require . NoError ( t , err )
2021-10-19 17:21:41 -04:00
tests := [] struct {
name string
requestCtx context . Context
iamRepoFn common . IamRepoFactory
authTokenRepoFn common . AuthTokenRepoFactory
serversRepoFn common . ServersRepoFactory
kms * kms . Kms
2021-10-23 21:23:49 -04:00
eventer * event . Eventer
2021-10-19 17:21:41 -04:00
ticket string
wantFactoryErr bool
wantFactoryErrMatch * errors . Template
wantFactoryErrContains string
wantRequestErr bool
wantRequestErrMatch * errors . Template
wantRequestErrContains string
}{
{
name : "missing-iam-repo" ,
requestCtx : newReqCtx ( validGatewayTicket ),
authTokenRepoFn : atRepoFn ,
serversRepoFn : serversRepoFn ,
kms : kmsCache ,
2021-10-23 21:23:49 -04:00
eventer : testEventer ,
2021-10-19 17:21:41 -04:00
wantFactoryErr : true ,
wantFactoryErrMatch : errors . T ( errors . InvalidParameter ),
wantFactoryErrContains : "missing iam repo" ,
},
{
name : "missing-at-repo" ,
requestCtx : newReqCtx ( validGatewayTicket ),
iamRepoFn : iamRepoFn ,
serversRepoFn : serversRepoFn ,
kms : kmsCache ,
2021-10-23 21:23:49 -04:00
eventer : testEventer ,
2021-10-19 17:21:41 -04:00
wantFactoryErr : true ,
wantFactoryErrMatch : errors . T ( errors . InvalidParameter ),
wantFactoryErrContains : "missing auth token repo" ,
},
{
name : "missing-servers-repo" ,
requestCtx : newReqCtx ( validGatewayTicket ),
iamRepoFn : iamRepoFn ,
authTokenRepoFn : atRepoFn ,
kms : kmsCache ,
2021-10-23 21:23:49 -04:00
eventer : testEventer ,
2021-10-19 17:21:41 -04:00
wantFactoryErr : true ,
wantFactoryErrMatch : errors . T ( errors . InvalidParameter ),
2022-06-20 18:20:17 -07:00
wantFactoryErrContains : "missing server repo function" ,
2021-10-19 17:21:41 -04:00
},
{
name : "missing-kms" ,
requestCtx : newReqCtx ( validGatewayTicket ),
iamRepoFn : iamRepoFn ,
authTokenRepoFn : atRepoFn ,
serversRepoFn : serversRepoFn ,
2021-10-23 21:23:49 -04:00
eventer : testEventer ,
wantFactoryErr : true ,
wantFactoryErrMatch : errors . T ( errors . InvalidParameter ),
wantFactoryErrContains : "missing kms" ,
},
{
name : "missing-eventer" ,
requestCtx : newReqCtx ( validGatewayTicket ),
iamRepoFn : iamRepoFn ,
authTokenRepoFn : atRepoFn ,
serversRepoFn : serversRepoFn ,
2021-10-19 17:21:41 -04:00
wantFactoryErr : true ,
wantFactoryErrMatch : errors . T ( errors . InvalidParameter ),
wantFactoryErrContains : "missing kms" ,
},
{
name : "missing-factory-ticket" ,
requestCtx : newReqCtx ( validGatewayTicket ),
iamRepoFn : iamRepoFn ,
authTokenRepoFn : atRepoFn ,
serversRepoFn : serversRepoFn ,
kms : kmsCache ,
2021-10-23 21:23:49 -04:00
eventer : testEventer ,
2021-10-19 17:21:41 -04:00
wantFactoryErr : true ,
wantFactoryErrMatch : errors . T ( errors . InvalidParameter ),
wantFactoryErrContains : "missing ticket" ,
},
{
name : "missing-metadata" ,
requestCtx : context . Background (),
iamRepoFn : iamRepoFn ,
authTokenRepoFn : atRepoFn ,
serversRepoFn : serversRepoFn ,
kms : kmsCache ,
2021-10-23 21:23:49 -04:00
eventer : testEventer ,
2021-10-19 17:21:41 -04:00
ticket : validGatewayTicket ,
wantRequestErr : true ,
wantRequestErrMatch : errors . T ( errors . Internal ),
wantRequestErrContains : "No metadata" ,
},
{
name : "missing-request-info-metadata" ,
requestCtx : func () context . Context {
md := metadata . Pairs ( "greeter-md" , "hello" )
return metadata . NewIncomingContext ( context . Background (), md )
}(),
iamRepoFn : iamRepoFn ,
authTokenRepoFn : atRepoFn ,
serversRepoFn : serversRepoFn ,
kms : kmsCache ,
2021-10-23 21:23:49 -04:00
eventer : testEventer ,
2021-10-19 17:21:41 -04:00
ticket : validGatewayTicket ,
wantRequestErr : true ,
wantRequestErrMatch : errors . T ( errors . Internal ),
wantRequestErrContains : "Missing request metadata" ,
},
{
name : "too-many-request-info-metadata" ,
requestCtx : func () context . Context {
md := metadata . Pairs ( requestInfoMdKey , "first" , requestInfoMdKey , "second" )
return metadata . NewIncomingContext ( context . Background (), md )
}(),
iamRepoFn : iamRepoFn ,
authTokenRepoFn : atRepoFn ,
serversRepoFn : serversRepoFn ,
kms : kmsCache ,
2021-10-23 21:23:49 -04:00
eventer : testEventer ,
2021-10-19 17:21:41 -04:00
ticket : validGatewayTicket ,
wantRequestErr : true ,
wantRequestErrMatch : errors . T ( errors . Internal ),
wantRequestErrContains : "expected 1 value" ,
},
{
name : "request-info-metadata-not-encoded" ,
requestCtx : func () context . Context {
md := metadata . Pairs ( requestInfoMdKey , "hello" )
return metadata . NewIncomingContext ( context . Background (), md )
}(),
iamRepoFn : iamRepoFn ,
authTokenRepoFn : atRepoFn ,
serversRepoFn : serversRepoFn ,
kms : kmsCache ,
2021-10-23 21:23:49 -04:00
eventer : testEventer ,
2021-10-19 17:21:41 -04:00
ticket : validGatewayTicket ,
wantRequestErr : true ,
wantRequestErrMatch : errors . T ( errors . Internal ),
wantRequestErrContains : "unable to decode request info" ,
},
{
name : "request-info-metadata-not-proto" ,
requestCtx : func () context . Context {
md := metadata . Pairs ( requestInfoMdKey , base58 . FastBase58Encoding ([] byte ( "hello" )))
return metadata . NewIncomingContext ( context . Background (), md )
}(),
iamRepoFn : iamRepoFn ,
authTokenRepoFn : atRepoFn ,
serversRepoFn : serversRepoFn ,
kms : kmsCache ,
2021-10-23 21:23:49 -04:00
eventer : testEventer ,
2021-10-19 17:21:41 -04:00
ticket : validGatewayTicket ,
wantRequestErr : true ,
wantRequestErrMatch : errors . T ( errors . Internal ),
wantRequestErrContains : "unable to unmarshal request info" ,
},
{
name : "request-info-metadata-not-proto" ,
requestCtx : func () context . Context {
md := metadata . Pairs ( requestInfoMdKey , base58 . FastBase58Encoding ([] byte ( "hello" )))
return metadata . NewIncomingContext ( context . Background (), md )
}(),
iamRepoFn : iamRepoFn ,
authTokenRepoFn : atRepoFn ,
serversRepoFn : serversRepoFn ,
kms : kmsCache ,
2021-10-23 21:23:49 -04:00
eventer : testEventer ,
2021-10-19 17:21:41 -04:00
ticket : validGatewayTicket ,
wantRequestErr : true ,
wantRequestErrMatch : errors . T ( errors . Internal ),
wantRequestErrContains : "unable to unmarshal request info" ,
},
{
name : "missing-request-ticket" ,
requestCtx : newReqCtx ( "" ),
iamRepoFn : iamRepoFn ,
authTokenRepoFn : atRepoFn ,
serversRepoFn : serversRepoFn ,
kms : kmsCache ,
2021-10-23 21:23:49 -04:00
eventer : testEventer ,
2021-10-19 17:21:41 -04:00
ticket : "validGatewayTicket" ,
wantRequestErr : true ,
wantRequestErrMatch : errors . T ( errors . Internal ),
wantRequestErrContains : "Invalid context (missing ticket)" ,
},
{
name : "bad-ticket" ,
requestCtx : newReqCtx ( "bad-ticket" ),
iamRepoFn : iamRepoFn ,
authTokenRepoFn : atRepoFn ,
serversRepoFn : serversRepoFn ,
kms : kmsCache ,
2021-10-23 21:23:49 -04:00
eventer : testEventer ,
2021-10-19 17:21:41 -04:00
ticket : "validGatewayTicket" ,
wantRequestErr : true ,
wantRequestErrMatch : errors . T ( errors . Internal ),
wantRequestErrContains : "Invalid context (bad ticket)" ,
},
{
name : "valid" ,
requestCtx : newReqCtx ( validGatewayTicket ),
iamRepoFn : iamRepoFn ,
authTokenRepoFn : atRepoFn ,
serversRepoFn : serversRepoFn ,
kms : kmsCache ,
2021-10-23 21:23:49 -04:00
eventer : testEventer ,
2021-10-19 17:21:41 -04:00
ticket : validGatewayTicket ,
},
}
for _ , tt := range tests {
t . Run ( tt . name , func ( t * testing . T ) {
assert , require := assert . New ( t ), require . New ( t )
2023-05-20 07:23:10 -04:00
interceptor , err := requestCtxUnaryInterceptor ( factoryCtx , tt . iamRepoFn , tt . authTokenRepoFn , tt . serversRepoFn , nil , nil , nil , tt . kms , tt . ticket , tt . eventer )
2021-10-19 17:21:41 -04:00
if tt . wantFactoryErr {
require . Error ( err )
assert . Nil ( interceptor )
if tt . wantFactoryErrMatch != nil {
assert . Truef ( errors . Match ( tt . wantFactoryErrMatch , err ), "want err code: %q got: %q" , tt . wantFactoryErrMatch . Code , err )
}
if tt . wantFactoryErrContains != "" {
assert . Contains ( err . Error (), tt . wantFactoryErrContains )
}
return
}
require . NoError ( err )
assert . NotNil ( interceptor )
info := & grpc . UnaryServerInfo {
FullMethod : "FakeMethod" ,
}
retCtx , err := interceptor ( tt . requestCtx , nil , info , returnCtxHandler )
if tt . wantRequestErr {
require . Error ( err )
assert . Nil ( retCtx )
if tt . wantRequestErrMatch != nil {
assert . Truef ( errors . Match ( tt . wantRequestErrMatch , err ), "want err code: %q got: %q" , tt . wantRequestErrMatch . Code , err )
}
if tt . wantRequestErrContains != "" {
assert . Contains ( err . Error (), tt . wantRequestErrContains )
}
return
}
require . NoError ( err )
2025-06-20 15:31:57 -04:00
// Use resource.Scope here but resource type shouldn't matter since we're only validating that auth.Verify
// can use the 'context' with additional ctx.Value from the interceptor
verifyResults := auth . Verify ( retCtx .( context . Context ), resource . Scope )
2021-10-19 17:21:41 -04:00
assert . NotEmpty ( verifyResults )
})
}
}
2023-05-20 07:23:10 -04:00
func Test_streamCtxInterceptor ( t * testing . T ) {
t . Parallel ()
2023-06-06 17:09:08 -04:00
factoryCtx := context . Background ()
2023-05-20 07:23:10 -04:00
conn , _ := db . TestSetup ( t , "postgres" )
rw := db . New ( conn )
wrapper := db . TestWrapper ( t )
kmsCache := kms . TestKms ( t , conn , wrapper )
iamRepo := iam . TestRepo ( t , conn , wrapper )
iamRepoFn := func () ( * iam . Repository , error ) {
return iamRepo , nil
}
atRepoFn := func () ( * authtoken . Repository , error ) {
2023-06-06 17:09:08 -04:00
return authtoken . NewRepository ( context . Background (), rw , rw , kmsCache )
2023-05-20 07:23:10 -04:00
}
serversRepoFn := func () ( * server . Repository , error ) {
2023-06-06 17:09:08 -04:00
return server . NewRepository ( factoryCtx , rw , rw , kmsCache )
2023-05-20 07:23:10 -04:00
}
validGatewayTicket := "valid-ticket"
o , _ := iam . TestScopes ( t , iamRepo )
at := authtoken . TestAuthToken ( t , conn , kmsCache , o . GetPublicId ())
2023-06-06 17:09:08 -04:00
encToken , err := authtoken . EncryptToken ( factoryCtx , kmsCache , o . GetPublicId (), at . GetPublicId (), at . GetToken ())
2023-05-20 07:23:10 -04:00
require . NoError ( t , err )
tokValue := at . GetPublicId () + "_" + encToken
newReqCtx := func ( gwTicket string ) context . Context {
req := httptest . NewRequest ( "GET" , "http://127.0.0.1/v1/scopes/o_1" , nil )
req . Header . Set ( "Authorization" , fmt . Sprintf ( "Bearer %s" , tokValue ))
// Add values for authn/authz checking
requestInfo := authpb . RequestInfo {
Path : req . URL . Path ,
Method : req . Method ,
EventId : "test-event-id" ,
TraceId : "test-trace-id" ,
}
requestInfo . PublicId , requestInfo . EncryptedToken , requestInfo . TokenFormat = auth . GetTokenFromRequest ( context . TODO (), kmsCache , req )
requestInfo . Ticket = gwTicket // allows the grpc-gateway to verify the request info came from it's in-memory companion http proxy
marshalledRequestInfo , err := proto . Marshal ( & requestInfo )
require . NoError ( t , err )
md := metadata . Pairs ( requestInfoMdKey , base58 . FastBase58Encoding ( marshalledRequestInfo ))
2023-06-06 17:09:08 -04:00
mdCtx := metadata . NewIncomingContext ( factoryCtx , md )
2023-05-20 07:23:10 -04:00
md , ok := metadata . FromIncomingContext ( mdCtx )
require . True ( t , ok )
require . NotNil ( t , md )
return mdCtx
}
2023-05-20 05:45:39 -04:00
c := event . TestEventerConfig ( t , "Test_streamCtxInterceptor" , event . TestWithAuditSink ( t ), event . TestWithObservationSink ( t ))
2023-05-20 07:23:10 -04:00
testLock := & sync . Mutex {}
testLogger := hclog . New ( & hclog . LoggerOptions {
Mutex : testLock ,
Name : "test" ,
})
2023-05-20 05:45:39 -04:00
testEventer , err := event . NewEventer ( testLogger , testLock , "Test_streamCtxInterceptor" , c . EventerConfig )
2023-05-20 07:23:10 -04:00
require . NoError ( t , err )
tests := [] struct {
name string
requestCtx context . Context
iamRepoFn common . IamRepoFactory
authTokenRepoFn common . AuthTokenRepoFactory
serversRepoFn common . ServersRepoFactory
kms * kms . Kms
eventer * event . Eventer
ticket string
wantFactoryErr bool
wantFactoryErrMatch * errors . Template
wantFactoryErrContains string
wantRequestErr bool
wantRequestErrMatch * errors . Template
wantRequestErrContains string
}{
{
name : "missing-iam-repo" ,
requestCtx : newReqCtx ( validGatewayTicket ),
authTokenRepoFn : atRepoFn ,
serversRepoFn : serversRepoFn ,
kms : kmsCache ,
eventer : testEventer ,
wantFactoryErr : true ,
wantFactoryErrMatch : errors . T ( errors . InvalidParameter ),
wantFactoryErrContains : "missing iam repo" ,
},
{
name : "missing-at-repo" ,
requestCtx : newReqCtx ( validGatewayTicket ),
iamRepoFn : iamRepoFn ,
serversRepoFn : serversRepoFn ,
kms : kmsCache ,
eventer : testEventer ,
wantFactoryErr : true ,
wantFactoryErrMatch : errors . T ( errors . InvalidParameter ),
wantFactoryErrContains : "missing auth token repo" ,
},
{
name : "missing-servers-repo" ,
requestCtx : newReqCtx ( validGatewayTicket ),
iamRepoFn : iamRepoFn ,
authTokenRepoFn : atRepoFn ,
kms : kmsCache ,
eventer : testEventer ,
wantFactoryErr : true ,
wantFactoryErrMatch : errors . T ( errors . InvalidParameter ),
wantFactoryErrContains : "missing server repo function" ,
},
{
name : "missing-kms" ,
requestCtx : newReqCtx ( validGatewayTicket ),
iamRepoFn : iamRepoFn ,
authTokenRepoFn : atRepoFn ,
serversRepoFn : serversRepoFn ,
eventer : testEventer ,
wantFactoryErr : true ,
wantFactoryErrMatch : errors . T ( errors . InvalidParameter ),
wantFactoryErrContains : "missing kms" ,
},
{
name : "missing-eventer" ,
requestCtx : newReqCtx ( validGatewayTicket ),
iamRepoFn : iamRepoFn ,
authTokenRepoFn : atRepoFn ,
serversRepoFn : serversRepoFn ,
wantFactoryErr : true ,
wantFactoryErrMatch : errors . T ( errors . InvalidParameter ),
wantFactoryErrContains : "missing kms" ,
},
{
name : "missing-factory-ticket" ,
requestCtx : newReqCtx ( validGatewayTicket ),
iamRepoFn : iamRepoFn ,
authTokenRepoFn : atRepoFn ,
serversRepoFn : serversRepoFn ,
kms : kmsCache ,
eventer : testEventer ,
wantFactoryErr : true ,
wantFactoryErrMatch : errors . T ( errors . InvalidParameter ),
wantFactoryErrContains : "missing ticket" ,
},
{
name : "missing-metadata" ,
requestCtx : context . Background (),
iamRepoFn : iamRepoFn ,
authTokenRepoFn : atRepoFn ,
serversRepoFn : serversRepoFn ,
kms : kmsCache ,
eventer : testEventer ,
ticket : validGatewayTicket ,
wantRequestErr : true ,
wantRequestErrMatch : errors . T ( errors . Internal ),
wantRequestErrContains : "No metadata" ,
},
{
name : "too-many-request-info-metadata" ,
requestCtx : func () context . Context {
md := metadata . Pairs ( requestInfoMdKey , "first" , requestInfoMdKey , "second" )
return metadata . NewIncomingContext ( context . Background (), md )
}(),
iamRepoFn : iamRepoFn ,
authTokenRepoFn : atRepoFn ,
serversRepoFn : serversRepoFn ,
kms : kmsCache ,
eventer : testEventer ,
ticket : validGatewayTicket ,
wantRequestErr : true ,
wantRequestErrMatch : errors . T ( errors . Internal ),
wantRequestErrContains : "expected 1 value" ,
},
{
name : "request-info-metadata-not-encoded" ,
requestCtx : func () context . Context {
md := metadata . Pairs ( requestInfoMdKey , "hello" )
return metadata . NewIncomingContext ( context . Background (), md )
}(),
iamRepoFn : iamRepoFn ,
authTokenRepoFn : atRepoFn ,
serversRepoFn : serversRepoFn ,
kms : kmsCache ,
eventer : testEventer ,
ticket : validGatewayTicket ,
wantRequestErr : true ,
wantRequestErrMatch : errors . T ( errors . Internal ),
wantRequestErrContains : "unable to decode request info" ,
},
{
name : "request-info-metadata-not-proto" ,
requestCtx : func () context . Context {
md := metadata . Pairs ( requestInfoMdKey , base58 . FastBase58Encoding ([] byte ( "hello" )))
return metadata . NewIncomingContext ( context . Background (), md )
}(),
iamRepoFn : iamRepoFn ,
authTokenRepoFn : atRepoFn ,
serversRepoFn : serversRepoFn ,
kms : kmsCache ,
eventer : testEventer ,
ticket : validGatewayTicket ,
wantRequestErr : true ,
wantRequestErrMatch : errors . T ( errors . Internal ),
wantRequestErrContains : "unable to unmarshal request info" ,
},
{
name : "missing-request-ticket" ,
requestCtx : newReqCtx ( "" ),
iamRepoFn : iamRepoFn ,
authTokenRepoFn : atRepoFn ,
serversRepoFn : serversRepoFn ,
kms : kmsCache ,
eventer : testEventer ,
ticket : "validGatewayTicket" ,
wantRequestErr : true ,
wantRequestErrMatch : errors . T ( errors . Internal ),
wantRequestErrContains : "Invalid context (missing ticket)" ,
},
{
name : "bad-ticket" ,
requestCtx : newReqCtx ( "bad-ticket" ),
iamRepoFn : iamRepoFn ,
authTokenRepoFn : atRepoFn ,
serversRepoFn : serversRepoFn ,
kms : kmsCache ,
eventer : testEventer ,
ticket : "validGatewayTicket" ,
wantRequestErr : true ,
wantRequestErrMatch : errors . T ( errors . Internal ),
wantRequestErrContains : "Invalid context (bad ticket)" ,
},
{
name : "valid" ,
requestCtx : newReqCtx ( validGatewayTicket ),
iamRepoFn : iamRepoFn ,
authTokenRepoFn : atRepoFn ,
serversRepoFn : serversRepoFn ,
kms : kmsCache ,
eventer : testEventer ,
ticket : validGatewayTicket ,
},
}
for _ , tt := range tests {
t . Run ( tt . name , func ( t * testing . T ) {
assert , require := assert . New ( t ), require . New ( t )
interceptor , err := requestCtxStreamInterceptor ( factoryCtx , tt . iamRepoFn , tt . authTokenRepoFn , tt . serversRepoFn , nil , nil , nil , tt . kms , tt . ticket , tt . eventer )
if tt . wantFactoryErr {
require . Error ( err )
assert . Nil ( interceptor )
if tt . wantFactoryErrMatch != nil {
assert . Truef ( errors . Match ( tt . wantFactoryErrMatch , err ), "want err code: %q got: %q" , tt . wantFactoryErrMatch . Code , err )
}
if tt . wantFactoryErrContains != "" {
assert . Contains ( err . Error (), tt . wantFactoryErrContains )
}
return
}
require . NoError ( err )
assert . NotNil ( interceptor )
info := & grpc . StreamServerInfo {
FullMethod : "FakeMethod" ,
IsClientStream : true ,
}
var hdCtx context . Context
hd := func ( srv interface {}, stream grpc . ServerStream ) error {
hdCtx = stream . Context ()
return nil
}
m := & streamMock { ctx : tt . requestCtx }
err = interceptor ( nil , m , info , hd )
if tt . wantRequestErr {
require . Error ( err )
if tt . wantRequestErrMatch != nil {
assert . Truef ( errors . Match ( tt . wantRequestErrMatch , err ), "want err code: %q got: %q" , tt . wantRequestErrMatch . Code , err )
}
if tt . wantRequestErrContains != "" {
assert . Contains ( err . Error (), tt . wantRequestErrContains )
}
return
}
require . NoError ( err )
2025-06-20 15:31:57 -04:00
// Use resource.Scope here but resource type shouldn't matter since we're only validating that auth.Verify
// can use the 'context' with additional ctx.Value from the interceptor
verifyResults := auth . Verify ( hdCtx .( context . Context ), resource . Scope )
2023-05-20 07:23:10 -04:00
assert . NotEmpty ( verifyResults )
})
}
}
type streamMock struct {
grpc . ServerStream
ctx context . Context
}
func ( m * streamMock ) Context () context . Context {
return m . ctx
}
func ( m * streamMock ) Send ( req * httpbody . HttpBody ) error {
panic ( "send not implemented" )
}
func ( m * streamMock ) RecvToClient () ( * httpbody . HttpBody , error ) {
panic ( "recv not implemented" )
}
2024-02-13 09:17:41 -08:00
func Test_aliasResolutionInterceptor ( t * testing . T ) {
ctx := context . Background ()
conn , _ := db . TestSetup ( t , "postgres" )
rw := db . New ( conn )
wrapper := db . TestWrapper ( t )
kmsCache := kms . TestKms ( t , conn , wrapper )
aliasRepoFn := func () ( * alias . Repository , error ) {
return alias . NewRepository ( context . Background (), rw , rw , kmsCache )
}
_ , proj := iam . TestScopes ( t , iam . TestRepo ( t , conn , wrapper ))
tar := tcp . TestTarget ( ctx , t , conn , proj . GetPublicId (), "test-target" )
al := talias . TestAlias ( t , rw , "test-alias.example" , talias . WithDestinationId ( tar . GetPublicId ()))
alWithoutDest := talias . TestAlias ( t , rw , "no-destination.alias" )
interceptor := aliasResolutionInterceptor ( ctx , aliasRepoFn )
require . NotNil ( t , interceptor )
returnCtxHandler := func ( ctx context . Context , req any ) ( any , error ) {
return ctx , nil
}
info := & grpc . UnaryServerInfo {
FullMethod : "FakeMethod" ,
}
cases := [] struct {
name string
req proto . Message
wantModifiedReq proto . Message
wantAlias * alias . Alias
2024-03-27 16:14:49 -07:00
errorIs error
2024-02-13 09:17:41 -08:00
}{
{
name : "non aliasable request" ,
req : & pbs . GetAccountRequest { Id : "test" },
wantModifiedReq : & pbs . GetAccountRequest { Id : "test" },
},
{
name : "non aliasable request with valid alias" ,
req : & pbs . GetAccountRequest { Id : al . GetValue ()},
wantModifiedReq : & pbs . GetAccountRequest { Id : al . GetValue ()},
},
{
name : "aliasable request with id" ,
req : & pbs . GetTargetRequest { Id : tar . GetPublicId ()},
wantModifiedReq : & pbs . GetTargetRequest { Id : tar . GetPublicId ()},
},
{
name : "aliasable request with alias" ,
req : & pbs . GetTargetRequest { Id : al . GetValue ()},
wantModifiedReq : & pbs . GetTargetRequest { Id : tar . GetPublicId ()},
wantAlias : & alias . Alias {
PublicId : al . GetPublicId (),
Value : al . GetValue (),
DestinationId : tar . GetPublicId (),
},
},
{
name : "aliasable request with unknown alias" ,
req : & pbs . GetTargetRequest { Id : "not.a.registered.alias" },
wantModifiedReq : & pbs . GetTargetRequest { Id : "not.a.registered.alias" },
2024-08-13 14:24:01 +01:00
errorIs : handlers . NotFoundError (),
2024-02-13 09:17:41 -08:00
},
{
name : "aliasable request with destinationless alias" ,
req : & pbs . GetTargetRequest { Id : alWithoutDest . GetValue ()},
wantModifiedReq : & pbs . GetTargetRequest { Id : alWithoutDest . GetValue ()},
2024-08-13 14:24:01 +01:00
errorIs : handlers . NotFoundError (),
2024-02-13 09:17:41 -08:00
},
}
for _ , tc := range cases {
t . Run ( tc . name , func ( t * testing . T ) {
ctx := context . Background ()
req := proto . Clone ( tc . req )
retCtx , err := interceptor ( ctx , req , info , returnCtxHandler )
assert . Empty ( t , cmp . Diff ( tc . wantModifiedReq , req , protocmp . Transform ()))
2024-03-27 16:14:49 -07:00
if tc . errorIs != nil {
2024-02-13 09:17:41 -08:00
require . Error ( t , err )
2024-03-27 16:14:49 -07:00
require . ErrorIs ( t , err , tc . errorIs )
2024-02-13 09:17:41 -08:00
return
}
ctxAlias := alias . FromContext ( retCtx .( context . Context ))
if tc . wantAlias == nil {
require . Nil ( t , ctxAlias )
return
}
require . NotNil ( t , ctxAlias )
assert . EqualValues ( t , tc . wantAlias , ctxAlias )
})
}
}
2021-10-19 17:21:41 -04:00
func Test_errorInterceptor ( t * testing . T ) {
ctx := context . Background ()
tests := [] struct {
name string
factoryCtx context . Context
wantRequestErr bool
wantDomainErr bool
wantApiErr bool
}{
{
name : "domain-error" ,
factoryCtx : ctx ,
wantRequestErr : true ,
wantDomainErr : true ,
},
{
name : "api-error" ,
factoryCtx : ctx ,
wantRequestErr : true ,
wantApiErr : true ,
},
{
name : "success" ,
factoryCtx : ctx ,
},
}
for _ , tt := range tests {
t . Run ( tt . name , func ( t * testing . T ) {
assert , require := assert . New ( t ), require . New ( t )
require . Falsef ( tt . wantApiErr && tt . wantDomainErr , "tests can't require both wantApiErr and wantDomainErr" )
errInterceptor := errorInterceptor ( tt . factoryCtx )
client := startTestGreeterService ( t , & testGreeter {}, errInterceptor )
var header metadata . MD
resp , err := client . SayHello (
context . Background (),
& interceptor . SayHelloRequest { Name : tt . name },
grpc . Header ( & header ),
)
if tt . wantRequestErr {
require . Error ( err )
domainHdr := header . Get ( domainErrHeader )
apiErrHdr := header . Get ( apiErrHeader )
require . Error ( err )
assert . Nil ( resp )
if tt . wantDomainErr {
require . Len ( domainHdr , 1 )
assert . Len ( apiErrHdr , 0 )
decoded , err := base58 . FastBase58Decoding ( domainHdr [ 0 ])
require . NoError ( err )
var pbErr pberrors . Err
err = proto . Unmarshal ( decoded , & pbErr )
require . NoError ( err )
}
if tt . wantApiErr {
require . Len ( apiErrHdr , 1 )
assert . Len ( domainHdr , 0 )
decoded , err := base58 . FastBase58Decoding ( apiErrHdr [ 0 ])
require . NoError ( err )
var pbErr pberrors . ApiError
err = proto . Unmarshal ( decoded , & pbErr )
require . NoError ( err )
}
return
}
require . NoError ( err )
})
}
}
func Test_statusCodeInterceptor ( t * testing . T ) {
ctx := context . Background ()
tests := [] struct {
name string
wantStatusCode int
wantErr bool
}{
{
name : "nil-nil" ,
wantStatusCode : http . StatusNoContent ,
},
{
name : "nil-err" ,
wantErr : true ,
},
{
name : "hello" ,
},
}
for _ , tt := range tests {
t . Run ( tt . name , func ( t * testing . T ) {
assert , require := assert . New ( t ), require . New ( t )
statusInterceptor := statusCodeInterceptor ( ctx )
client := startTestGreeterService ( t , & testGreeter {}, statusInterceptor )
var header metadata . MD
_ , err := client . SayHello (
context . Background (),
& interceptor . SayHelloRequest { Name : tt . name },
grpc . Header ( & header ),
)
if tt . wantErr {
assert . Error ( err )
}
statusHdr := header . Get ( handlers . StatusCodeHeader )
if tt . wantStatusCode > 0 {
require . Len ( statusHdr , 1 )
code , err := strconv . Atoi ( statusHdr [ 0 ])
require . NoError ( err )
assert . Equal ( tt . wantStatusCode , code )
} else {
require . Len ( statusHdr , 0 )
}
})
}
}
2025-04-17 18:16:25 +00:00
func Test_parseUserAgents ( t * testing . T ) {
t . Parallel ()
tests := [] struct {
name string
rawUserAgent string
expected [] * event . UserAgent
}{
{
name : "valid single user-agent" ,
rawUserAgent : "Boundary-client-agent/0.1.4" ,
expected : [] * event . UserAgent {
{
Product : "Boundary-client-agent" ,
ProductVersion : "0.1.4" ,
},
},
},
{
name : "multiple valid agents with comments" ,
rawUserAgent : "Boundary-client-agent/0.1.4 (foo; bar); AnotherApp/2.0.0 (baz )" ,
expected : [] * event . UserAgent {
{
Product : "Boundary-client-agent" ,
ProductVersion : "0.1.4" ,
Comments : [] string { "foo" , "bar" },
},
{
Product : "AnotherApp" ,
ProductVersion : "2.0.0" ,
Comments : [] string { "baz" },
},
},
},
{
name : "complex but valid user agents" ,
rawUserAgent : "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36 surveyon/2.9.5 (iPhone; CPU iPhone OS 12_5_7 like Mac OS X)" ,
expected : [] * event . UserAgent {
{
Product : "Mozilla" ,
ProductVersion : "5.0" ,
Comments : [] string { "Macintosh" , "Intel Mac OS X 10_15_7" },
},
{
Product : "AppleWebKit" ,
ProductVersion : "537.36" ,
Comments : [] string { "KHTML, like Gecko" },
},
{
Product : "Chrome" ,
ProductVersion : "87.0.4280.88" ,
},
{
Product : "Safari" ,
ProductVersion : "537.36" ,
},
{
Product : "surveyon" ,
ProductVersion : "2.9.5" ,
Comments : [] string { "iPhone" , "CPU iPhone OS 12_5_7 like Mac OS X" },
},
},
},
{
name : "invalid client-agent version format (starts with 'v')" ,
rawUserAgent : "Boundary-client-agent/v0.1.4" ,
expected : nil ,
},
{
name : "invalid client-agent version format (non-semver)" ,
rawUserAgent : "Boundary-client-agent/0.1.x" ,
expected : nil ,
},
{
name : "empty user-agent" ,
rawUserAgent : "" ,
expected : nil ,
},
{
name : "valid non client-agent user-agent" ,
rawUserAgent : "SomeOtherApp/v1.2.3" ,
expected : [] * event . UserAgent {
{
Product : "SomeOtherApp" ,
ProductVersion : "v1.2.3" ,
},
},
},
{
name : "mixed valid and invalid agents" ,
rawUserAgent : "Boundary-client-agent/0.1.4 NoVersionApp SomeOtherApp/" ,
expected : [] * event . UserAgent {
{
Product : "Boundary-client-agent" ,
ProductVersion : "0.1.4" ,
},
},
},
}
for _ , tt := range tests {
t . Run ( tt . name , func ( t * testing . T ) {
t . Parallel ()
result := parseUserAgents ( tt . rawUserAgent )
assert . ElementsMatch ( t , tt . expected , result )
})
}
}
2021-11-09 11:43:41 -05:00
func Test_workerRequestInfoInterceptor ( t * testing . T ) {
factoryCtx := context . Background ()
requestCtx := context . Background ()
2022-10-25 18:29:38 -05:00
returnCtxHandler := func ( ctx context . Context , req any ) ( any , error ) {
2021-11-09 11:43:41 -05:00
return ctx , nil
}
2023-05-20 05:45:39 -04:00
c := event . TestEventerConfig ( t , "Test_unaryCtxInterceptor" , event . TestWithAuditSink ( t ), event . TestWithObservationSink ( t ))
2021-11-09 11:43:41 -05:00
testLock := & sync . Mutex {}
testLogger := hclog . New ( & hclog . LoggerOptions {
Mutex : testLock ,
Name : "test" ,
})
2023-05-20 05:45:39 -04:00
testEventer , err := event . NewEventer ( testLogger , testLock , "Test_unaryCtxInterceptor" , c . EventerConfig )
2021-11-09 11:43:41 -05:00
require . NoError ( t , err )
tests := [] struct {
name string
requestCtx context . Context
eventer * event . Eventer
wantFactoryErr bool
wantFactoryErrMatch * errors . Template
wantFactoryErrContains string
wantRequestErr bool
wantRequestErrMatch * errors . Template
wantRequestErrContains string
}{
{
name : "missing-eventer" ,
requestCtx : requestCtx ,
wantFactoryErr : true ,
wantFactoryErrMatch : errors . T ( errors . InvalidParameter ),
wantFactoryErrContains : "missing eventer" ,
},
{
name : "valid" ,
requestCtx : requestCtx ,
eventer : testEventer ,
},
}
for _ , tt := range tests {
t . Run ( tt . name , func ( t * testing . T ) {
assert , require := assert . New ( t ), require . New ( t )
interceptor , err := workerRequestInfoInterceptor ( factoryCtx , tt . eventer )
if tt . wantFactoryErr {
require . Error ( err )
assert . Nil ( interceptor )
if tt . wantFactoryErrMatch != nil {
assert . Truef ( errors . Match ( tt . wantFactoryErrMatch , err ), "want err code: %q got: %q" , tt . wantFactoryErrMatch . Code , err )
}
if tt . wantFactoryErrContains != "" {
assert . Contains ( err . Error (), tt . wantFactoryErrContains )
}
return
}
require . NoError ( err )
assert . NotNil ( interceptor )
info := & grpc . UnaryServerInfo {
FullMethod : "FakeMethod" ,
}
retCtx , err := interceptor ( tt . requestCtx , nil , info , returnCtxHandler )
if tt . wantRequestErr {
require . Error ( err )
assert . Nil ( retCtx )
if tt . wantRequestErrMatch != nil {
assert . Truef ( errors . Match ( tt . wantRequestErrMatch , err ), "want err code: %q got: %q" , tt . wantRequestErrMatch . Code , err )
}
if tt . wantRequestErrContains != "" {
assert . Contains ( err . Error (), tt . wantRequestErrContains )
}
return
}
require . NoError ( err )
requestInfo , found := event . RequestInfoFromContext ( retCtx .( context . Context ))
require . True ( found )
assert . NotNil ( requestInfo )
assert . NotEmpty ( requestInfo . Id )
assert . NotEmpty ( requestInfo . EventId )
assert . Equal ( "FakeMethod" , requestInfo . Method )
eventer , found := event . EventerFromContext ( retCtx .( context . Context ))
require . True ( found )
assert . NotNil ( eventer )
})
}
}
2021-10-19 17:21:41 -04:00
type testGreeter struct {
interceptor . UnimplementedGreeterServiceServer
}
func ( g * testGreeter ) SayHello ( ctx context . Context , req * interceptor . SayHelloRequest ) ( * interceptor . SayHelloResponse , error ) {
const op = "SayHello"
switch req . GetName () {
case "api-error" :
return & interceptor . SayHelloResponse {}, & handlers . ApiError {
Status : http . StatusInternalServerError ,
Inner : & pb_api . Error {
Kind : "internal" ,
Op : op ,
Message : "api error msg" ,
},
}
case "domain-error" :
return & interceptor . SayHelloResponse { Message : "hello" }, errors . New ( ctx , errors . Internal , op , "domain error msg" )
case "nil-nil" :
return nil , nil
case "nil-err" :
return nil , errors . New ( ctx , errors . Internal , op , "nil response error msg" )
default :
return & interceptor . SayHelloResponse { Message : "hello" }, nil
}
}
2024-04-06 13:48:47 -07:00
func Test_correlationIdInterceptor ( t * testing . T ) {
interceptor := correlationIdInterceptor ( context . Background ())
require . NotNil ( t , interceptor )
corId , err := uuid . GenerateUUID ()
require . NoError ( t , err )
returnCtxHandler := func ( ctx context . Context , req any ) ( any , error ) {
return ctx , nil
}
cases := [] struct {
name string
ctx context . Context
wantCorId string
wantErr bool
wantErrStr string
}{
{
name : "no metadata" ,
ctx : context . Background (),
wantErr : true ,
wantErrStr : "controller.correlationIdInterceptor: no metadata" ,
},
{
name : "no correlation id" ,
ctx : metadata . NewIncomingContext ( context . Background (), metadata . New ( map [ string ] string {
"not-correlation-id" : "this is not a correlation id" ,
})),
wantErr : true ,
wantErrStr : "controller.correlationIdInterceptor: missing correlation id metadata" ,
},
{
name : "too many correlation ids" ,
ctx : metadata . NewIncomingContext ( context . Background (), metadata . New ( map [ string ] string {
globals . CorrelationIdKey : corId ,
"x-Correlation-id" : corId , // metadata.New does a toLower so this is an easy way to add multiple of same key
})),
wantErr : true ,
wantErrStr : "controller.correlationIdInterceptor: expected 1 value for x-correlation-id metadata and got 2" ,
},
{
name : "invalid correlation id" ,
ctx : metadata . NewIncomingContext ( context . Background (), metadata . New ( map [ string ] string {
globals . CorrelationIdKey : "this is wrong" ,
})),
wantErr : true ,
wantErrStr : "controller.correlationIdInterceptor: failed to validated correlation id" ,
},
{
name : "valid correlation id" ,
ctx : metadata . NewIncomingContext ( context . Background (), metadata . New ( map [ string ] string {
globals . CorrelationIdKey : corId ,
})),
wantCorId : corId ,
},
}
for _ , tc := range cases {
t . Run ( tc . name , func ( t * testing . T ) {
req := & pbs . GetAccountRequest { Id : "test" }
retCtx , err := interceptor ( tc . ctx , req , nil , returnCtxHandler )
if tc . wantErr {
require . Error ( t , err )
require . Contains ( t , err . Error (), tc . wantErrStr )
return
}
assert . NoError ( t , err )
corId , ok := event . CorrelationIdFromContext ( retCtx .( context . Context ))
require . True ( t , ok )
assert . Equal ( t , tc . wantCorId , corId )
})
}
}