refactor (controller): Use a grpc server for the grpc-gateway (#1576)

* refactor (controller): Use a grpc server for the grpc-gateway

This starts a grpc server for the grpc-gateway.  The new gateway
server uses an in-memory listener which also supports "dialing", so
the http proxy can connect to the in-memory listener.

This change allows us to use all grpc server features and was in part
motivated by a need for an interceptor of inbound requests.  With that
said, having a "proper" grpc server for the gateway will provide a lot
more flexibility and capabilities moving forward.

The request context will still be verified via middleware in the
http proxy (see wrapHandlerWithCommonFuncs).  This verified context
will be serialized into a protobuf and sent along to the grpc-gateway
via metadata where an interceptor will add the verified request
info to the request's context (see requestCtxInterceptor).  The
interceptor will verify the serialized request info via a shared ticket
with it's companion in-memory http proxy.  FYI: the ticket is simply
random base62 string (see db.NewPrivateId).

This change also necessitated a minor change to services.  We need to
return an empty response instead of nil, nil when there are no errors
and the response is empty.

* refactor (handlers): Rename gRPC Gateway filter

Originally, this was called an OutgoingInterceptor, which is an
unfortunate name since it's not an interceptor.  Now that we're
refactoring the gateway stuff and have proper interceptors, renaming
this should remove ambiguity

* refactor: Rework how 204 with empty msg is handled

Recent changes to grpc gateway has necessitated a change to how
204s are handled by the gateway

* tests (bats): Check status codes for delete operations
This commit is contained in:
Jim authored and GitHub committed 2021-10-19 17:21:41 -04:00
1 parent 55ba24499a
commit 769416c2ff
50 files changed
+2625 -370

No files matched your search

+39 -53
View File
@@ -11,7 +11,9 @@ import (
"github.com/hashicorp/boundary/api/recovery"
"github.com/hashicorp/boundary/globals"
"github.com/hashicorp/boundary/internal/errors"
authpb "github.com/hashicorp/boundary/internal/gen/controller/auth"
"github.com/hashicorp/boundary/internal/gen/controller/tokens"
"github.com/hashicorp/boundary/internal/kms"
"github.com/hashicorp/boundary/internal/observability/event"
"github.com/hashicorp/boundary/internal/perms"
@@ -27,7 +29,7 @@ import (
"google.golang.org/protobuf/proto"
)
type TokenFormat int
type TokenFormat uint32
const (
// We weren't given one or couldn't parse it
@@ -51,22 +53,6 @@ type key int
var verifierKey key
// RequestInfo contains request parameters necessary for checking authn/authz
type RequestInfo struct {
Path string
Method string
PublicId string
EncryptedToken string
Token string
TokenFormat TokenFormat
// The following are useful for tests
scopeIdOverride string
userIdOverride string
DisableAuthzFailures bool
DisableAuthEntirely bool
}
type VerifyResults struct {
UserId string
AuthTokenId string
@@ -108,7 +94,7 @@ type verifier struct {
authTokenRepoFn common.AuthTokenRepoFactory
serversRepoFn common.ServersRepoFactory
kms *kms.Kms
requestInfo RequestInfo
requestInfo *authpb.RequestInfo
res *perms.Resource
act action.Type
ctx context.Context
@@ -124,7 +110,7 @@ func NewVerifierContext(ctx context.Context,
authTokenRepoFn common.AuthTokenRepoFactory,
serversRepoFn common.ServersRepoFactory,
kms *kms.Kms,
requestInfo RequestInfo) context.Context {
requestInfo *authpb.RequestInfo) context.Context {
return context.WithValue(ctx, verifierKey, &verifier{
iamRepoFn: iamRepoFn,
authTokenRepoFn: authTokenRepoFn,
@@ -174,7 +160,7 @@ func Verify(ctx context.Context, opt ...Option) (ret VerifyResults) {
// without fuss
if v.requestInfo.DisableAuthEntirely {
const op = "auth.(disabled).lookupScope"
ret.Scope.Id = v.requestInfo.scopeIdOverride
ret.Scope.Id = v.requestInfo.ScopeIdOverride
if ret.Scope.Id == "" {
ret.Scope.Id = opts.withScopeId
}
@@ -214,7 +200,7 @@ func Verify(ctx context.Context, opt ...Option) (ret VerifyResults) {
ParentScopeId: scp.GetParentId(),
}
}
ret.UserId = v.requestInfo.userIdOverride
ret.UserId = v.requestInfo.UserIdOverride
if reqInfo != nil {
reqInfo.UserId = ret.UserId
}
@@ -291,40 +277,40 @@ func Verify(ctx context.Context, opt ...Option) (ret VerifyResults) {
func (v *verifier) decryptToken(ctx context.Context) {
const op = "auth.(verifier).decryptToken"
switch v.requestInfo.TokenFormat {
case AuthTokenTypeUnknown:
case uint32(AuthTokenTypeUnknown):
// Nothing to decrypt
return
case AuthTokenTypeBearer, AuthTokenTypeSplitCookie:
case uint32(AuthTokenTypeBearer), uint32(AuthTokenTypeSplitCookie):
if v.kms == nil {
event.WriteError(ctx, op, stderrors.New("no KMS object available to authz system"))
v.requestInfo.TokenFormat = AuthTokenTypeUnknown
v.requestInfo.TokenFormat = uint32(AuthTokenTypeUnknown)
return
}
tokenRepo, err := v.authTokenRepoFn()
if err != nil {
event.WriteError(ctx, op, err, event.WithInfoMsg("failed to get authtoken repo"))
v.requestInfo.TokenFormat = AuthTokenTypeUnknown
v.requestInfo.TokenFormat = uint32(AuthTokenTypeUnknown)
return
}
at, err := tokenRepo.LookupAuthToken(v.ctx, v.requestInfo.PublicId)
if err != nil {
event.WriteError(ctx, op, err, event.WithInfoMsg("failed to look up auth token by public ID"))
v.requestInfo.TokenFormat = AuthTokenTypeUnknown
v.requestInfo.TokenFormat = uint32(AuthTokenTypeUnknown)
return
}
if at == nil {
event.WriteError(ctx, op, stderrors.New("nil result from looking up auth token by public ID"))
v.requestInfo.TokenFormat = AuthTokenTypeUnknown
v.requestInfo.TokenFormat = uint32(AuthTokenTypeUnknown)
return
}
tokenWrapper, err := v.kms.GetWrapper(v.ctx, at.GetScopeId(), kms.KeyPurposeTokens)
if err != nil {
event.WriteError(ctx, op, err, event.WithInfoMsg("unable to get wrapper for tokens; continuing as anonymous user"))
v.requestInfo.TokenFormat = AuthTokenTypeUnknown
v.requestInfo.TokenFormat = uint32(AuthTokenTypeUnknown)
return
}
@@ -333,62 +319,62 @@ func (v *verifier) decryptToken(ctx context.Context) {
case globals.ServiceTokenV1:
default:
event.WriteError(ctx, op, stderrors.New("unknown token encryption version; continuing as anonymous user"), event.WithInfo("version", version))
v.requestInfo.TokenFormat = AuthTokenTypeUnknown
v.requestInfo.TokenFormat = uint32(AuthTokenTypeUnknown)
return
}
marshaledToken, err := base58.FastBase58Decoding(v.requestInfo.EncryptedToken[len(globals.ServiceTokenV1):])
if err != nil {
event.WriteError(ctx, op, err, event.WithInfoMsg("error unmarshaling base58 token; continuing as anonymous user"))
v.requestInfo.TokenFormat = AuthTokenTypeUnknown
v.requestInfo.TokenFormat = uint32(AuthTokenTypeUnknown)
return
}
blobInfo := new(wrapping.EncryptedBlobInfo)
if err := proto.Unmarshal(marshaledToken, blobInfo); err != nil {
event.WriteError(ctx, op, err, event.WithInfoMsg("error decoding encrypted token; continuing as anonymous user"))
v.requestInfo.TokenFormat = AuthTokenTypeUnknown
v.requestInfo.TokenFormat = uint32(AuthTokenTypeUnknown)
return
}
s1Bytes, err := tokenWrapper.Decrypt(v.ctx, blobInfo, []byte(v.requestInfo.PublicId))
if err != nil {
event.WriteError(ctx, op, err, event.WithInfoMsg("error decrypting encrypted token; continuing as anonymous user"))
v.requestInfo.TokenFormat = AuthTokenTypeUnknown
v.requestInfo.TokenFormat = uint32(AuthTokenTypeUnknown)
return
}
var s1Info tokens.S1TokenInfo
if err := proto.Unmarshal(s1Bytes, &s1Info); err != nil {
event.WriteError(ctx, op, err, event.WithInfoMsg("error unmarshaling token info; continuing as anonymous user"))
v.requestInfo.TokenFormat = AuthTokenTypeUnknown
v.requestInfo.TokenFormat = uint32(AuthTokenTypeUnknown)
return
}
if v.requestInfo.TokenFormat == AuthTokenTypeUnknown || s1Info.Token == "" || v.requestInfo.PublicId == "" {
if v.requestInfo.TokenFormat == uint32(AuthTokenTypeUnknown) || s1Info.Token == "" || v.requestInfo.PublicId == "" {
event.WriteError(ctx, op, stderrors.New("after parsing, could not find valid token; continuing as anonymous user"))
v.requestInfo.TokenFormat = AuthTokenTypeUnknown
v.requestInfo.TokenFormat = uint32(AuthTokenTypeUnknown)
return
}
v.requestInfo.Token = s1Info.Token
return
case AuthTokenTypeRecoveryKms:
case uint32(AuthTokenTypeRecoveryKms):
if v.kms == nil {
event.WriteError(ctx, op, stderrors.New("no KMS object available to authz system"))
v.requestInfo.TokenFormat = AuthTokenTypeUnknown
v.requestInfo.TokenFormat = uint32(AuthTokenTypeUnknown)
return
}
wrapper := v.kms.GetExternalWrappers().Recovery()
if wrapper == nil {
event.WriteError(ctx, op, stderrors.New("no recovery KMS is available"))
v.requestInfo.TokenFormat = AuthTokenTypeUnknown
v.requestInfo.TokenFormat = uint32(AuthTokenTypeUnknown)
return
}
info, err := recovery.ParseRecoveryToken(v.ctx, wrapper, v.requestInfo.EncryptedToken)
if err != nil {
event.WriteError(ctx, op, err, event.WithInfoMsg("decrypt recovery token: error parsing and validating recovery token"))
v.requestInfo.TokenFormat = AuthTokenTypeUnknown
v.requestInfo.TokenFormat = uint32(AuthTokenTypeUnknown)
return
}
// If we add the validity period to the creation time (which we've
@@ -396,18 +382,18 @@ func (v *verifier) decryptToken(ctx context.Context) {
// it's before now, it's expired and might be a replay.
if info.CreationTime.Add(globals.RecoveryTokenValidityPeriod).Before(time.Now()) {
event.WriteError(ctx, op, stderrors.New("recovery token has expired (possible replay attack)"))
v.requestInfo.TokenFormat = AuthTokenTypeUnknown
v.requestInfo.TokenFormat = uint32(AuthTokenTypeUnknown)
return
}
repo, err := v.serversRepoFn()
if err != nil {
event.WriteError(ctx, op, err, event.WithInfoMsg("decrypt recovery token: error fetching servers repo"))
v.requestInfo.TokenFormat = AuthTokenTypeUnknown
v.requestInfo.TokenFormat = uint32(AuthTokenTypeUnknown)
return
}
if err := repo.AddRecoveryNonce(v.ctx, info.Nonce); err != nil {
event.WriteError(ctx, op, err, event.WithInfoMsg("decrypt recovery token: error adding nonce to database (possible replay attack)"))
v.requestInfo.TokenFormat = AuthTokenTypeUnknown
v.requestInfo.TokenFormat = uint32(AuthTokenTypeUnknown)
return
}
event.WriteError(ctx, op, stderrors.New("recovery KMS was used to authorize a call"), event.WithInfo("url", v.requestInfo.Path, "method", v.requestInfo.Method))
@@ -426,15 +412,15 @@ func (v verifier) performAuthCheck(ctx context.Context) (aclResults perms.ACLRes
// Validate the token and fetch the corresponding user ID
switch v.requestInfo.TokenFormat {
case AuthTokenTypeUnknown:
case uint32(AuthTokenTypeUnknown):
// Nothing; remain as the anonymous user
case AuthTokenTypeRecoveryKms:
case uint32(AuthTokenTypeRecoveryKms):
// We validated the encrypted token in decryptToken and handled the
// nonces there, so just set the user
userId = "u_recovery"
case AuthTokenTypeBearer, AuthTokenTypeSplitCookie:
case uint32(AuthTokenTypeBearer), uint32(AuthTokenTypeSplitCookie):
if v.requestInfo.Token == "" {
// This will end up staying as the anonymous user
break
@@ -500,7 +486,7 @@ func (v verifier) performAuthCheck(ctx context.Context) (aclResults perms.ACLRes
}
// At this point we don't need to look up grants since it's automatically allowed
if v.requestInfo.TokenFormat == AuthTokenTypeRecoveryKms {
if v.requestInfo.TokenFormat == uint32(AuthTokenTypeRecoveryKms) {
aclResults.AuthenticationFinished = true
aclResults.Authorized = true
retErr = nil
@@ -561,7 +547,7 @@ func (r *VerifyResults) FetchActionSetForType(ctx context.Context, typ resource.
func (r *VerifyResults) fetchActions(id string, typ resource.Type, availableActions action.ActionSet, opt ...Option) action.ActionSet {
switch {
case r.v.requestInfo.DisableAuthEntirely,
r.v.requestInfo.TokenFormat == AuthTokenTypeRecoveryKms:
r.v.requestInfo.TokenFormat == uint32(AuthTokenTypeRecoveryKms):
// TODO: See if we can be better about what we return with the anonymous
// user and recovery KMS, perhaps given exclusionary options for each
return availableActions
@@ -598,7 +584,7 @@ func (r *VerifyResults) fetchActions(id string, typ resource.Type, availableActi
func (r *VerifyResults) FetchOutputFields(res perms.Resource, act action.Type) perms.OutputFieldsMap {
switch {
case r.v.requestInfo.TokenFormat == AuthTokenTypeRecoveryKms:
case r.v.requestInfo.TokenFormat == uint32(AuthTokenTypeRecoveryKms):
return perms.OutputFieldsMap{"*": true}
case r.v.requestInfo.DisableAuthEntirely:
return nil
@@ -611,7 +597,7 @@ func (r *VerifyResults) FetchOutputFields(res perms.Resource, act action.Type) p
// split cookies and parses it. If it cannot be parsed successfully, the issue
// is logged and we return blank, so logic will continue as the anonymous user.
// The public ID and _encrypted_ token are returned along with the token format.
func GetTokenFromRequest(ctx context.Context, kmsCache *kms.Kms, req *http.Request) (string, string, TokenFormat) {
func GetTokenFromRequest(ctx context.Context, kmsCache *kms.Kms, req *http.Request) (string, string, uint32) {
const op = "auth.GetTokenFromRequest"
// First, get the token, either from the authorization header or from split
// cookies
@@ -643,21 +629,21 @@ func GetTokenFromRequest(ctx context.Context, kmsCache *kms.Kms, req *http.Reque
// We didn't find auth info or a client screwed up and put in a blank
// header instead of nothing at all, so return blank which will indicate
// the anonymouse user
return "", "", AuthTokenTypeUnknown
return "", "", uint32(AuthTokenTypeUnknown)
}
if strings.HasPrefix(fullToken, "r_") {
return "", fullToken, AuthTokenTypeRecoveryKms
return "", fullToken, uint32(AuthTokenTypeRecoveryKms)
}
splitFullToken := strings.Split(fullToken, "_")
if len(splitFullToken) != 3 {
event.WriteError(ctx, op, stderrors.New("unexpected number of segments in token; continuing as anonymous user"), event.WithInfo("expected", 3, "found", len(splitFullToken)))
return "", "", AuthTokenTypeUnknown
return "", "", uint32(AuthTokenTypeUnknown)
}
publicId := strings.Join(splitFullToken[0:2], "_")
encryptedToken := splitFullToken[2]
return publicId, encryptedToken, receivedTokenType
return publicId, encryptedToken, uint32(receivedTokenType)
}
@@ -9,6 +9,7 @@ import (
"github.com/hashicorp/boundary/internal/authtoken"
"github.com/hashicorp/boundary/internal/db"
authpb "github.com/hashicorp/boundary/internal/gen/controller/auth"
"github.com/hashicorp/boundary/internal/iam"
"github.com/hashicorp/boundary/internal/kms"
"github.com/hashicorp/boundary/internal/servers"
@@ -107,17 +108,17 @@ func TestAuthTokenAuthenticator(t *testing.T) {
}
// Add values for authn/authz checking
requestInfo := RequestInfo{
requestInfo := authpb.RequestInfo{
Path: req.URL.Path,
Method: req.Method,
}
requestInfo.PublicId, requestInfo.EncryptedToken, requestInfo.TokenFormat = GetTokenFromRequest(context.TODO(), kms, req)
assert.Equal(t, tc.tokenFormat, requestInfo.TokenFormat)
assert.Equal(t, uint32(tc.tokenFormat), requestInfo.TokenFormat)
if tc.userId == "" {
return
}
ctx := NewVerifierContext(context.Background(), iamRepoFn, tokenRepoFn, serversRepoFn, kms, requestInfo)
ctx := NewVerifierContext(context.Background(), iamRepoFn, tokenRepoFn, serversRepoFn, kms, &requestInfo)
v, ok := ctx.Value(verifierKey).(*verifier)
require.True(t, ok)
+9 -8
View File
@@ -3,6 +3,7 @@ package auth
import (
"context"
authpb "github.com/hashicorp/boundary/internal/gen/controller/auth"
"github.com/hashicorp/boundary/internal/requests"
"github.com/hashicorp/boundary/internal/servers/controller/common"
)
@@ -11,16 +12,16 @@ import (
// auth checking entirely disabled. Supported options: WithScopeId an WithUserId
// are used directly; WithKms is passed through into the verifier context.
func DisabledAuthTestContext(iamRepoFn common.IamRepoFactory, scopeId string, opt ...Option) context.Context {
reqInfo := RequestInfo{DisableAuthEntirely: true}
reqInfo := authpb.RequestInfo{DisableAuthEntirely: true}
opts := getOpts(opt...)
reqInfo.scopeIdOverride = opts.withScopeId
if reqInfo.scopeIdOverride == "" {
reqInfo.scopeIdOverride = scopeId
reqInfo.ScopeIdOverride = opts.withScopeId
if reqInfo.ScopeIdOverride == "" {
reqInfo.ScopeIdOverride = scopeId
}
reqInfo.userIdOverride = opts.withUserId
if reqInfo.userIdOverride == "" {
reqInfo.userIdOverride = "u_auth"
reqInfo.UserIdOverride = opts.withUserId
if reqInfo.UserIdOverride == "" {
reqInfo.UserIdOverride = "u_auth"
}
requestContext := context.WithValue(context.Background(), requests.ContextRequestInformationKey, &requests.RequestContext{})
return NewVerifierContext(requestContext, iamRepoFn, nil, nil, opts.withKms, reqInfo)
return NewVerifierContext(requestContext, iamRepoFn, nil, nil, opts.withKms, &reqInfo)
}