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:
50 files changed
+2625
-370
No files matched your search
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
Reference in new issue
Block a user