diff --git a/go.mod b/go.mod index 72fe9c2ed..29869f0d4 100644 --- a/go.mod +++ b/go.mod @@ -13,10 +13,12 @@ require ( github.com/fatih/color v1.12.0 github.com/fatih/structs v1.1.0 github.com/favadi/protoc-go-inject-tag v1.3.0 + github.com/godbus/dbus/v5 v5.0.4 // indirect github.com/golang-migrate/migrate/v4 v4.14.1 github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe github.com/golang/protobuf v1.5.2 github.com/google/go-cmp v0.5.6 + github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.6.0 github.com/hashicorp/boundary/api v0.0.19 github.com/hashicorp/boundary/sdk v0.0.11 diff --git a/go.sum b/go.sum index 632cd926c..c36a7696e 100644 --- a/go.sum +++ b/go.sum @@ -322,8 +322,9 @@ github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/E github.com/gocql/gocql v0.0.0-20190301043612-f6df8288f9b4/go.mod h1:4Fw1eo5iaEhDUs8XyuhSVCVy52Jq3L+/3GJgYkwc+/0= github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e h1:BWhy2j3IXJhjCbC68FptL43tDKIq8FladmaTs3Xs7Z8= github.com/godbus/dbus v0.0.0-20190422162347-ade71ed3457e/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= -github.com/godbus/dbus/v5 v5.0.3 h1:ZqHaoEF7TBzh4jzPmqVhE/5A1z9of6orkAe5uHoAeME= github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/godbus/dbus/v5 v5.0.4 h1:9349emZab16e7zQvpmsbtjc18ykshndd8y2PG3sgJbA= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gofrs/flock v0.8.0 h1:MSdYClljsF3PbENUUEx85nkWfJSGfzYI9yEBZOJz6CY= github.com/gofrs/flock v0.8.0/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= @@ -432,6 +433,8 @@ github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0U github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 h1:+9834+KizmvFV7pXQGSXQTsaWhq2GjuNUt0aUU0YBYw= +github.com/grpc-ecosystem/go-grpc-middleware v1.3.0/go.mod h1:z0ButlSOZa5vEBq9m2m2hlwIgKw+rp3sdCBRoJY+30Y= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= @@ -1407,6 +1410,7 @@ google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfG google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200323114720-3f67cca34472/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= diff --git a/internal/auth/additional_verification_test.go b/internal/auth/additional_verification_test.go index a73c21f4b..2f0b6e633 100644 --- a/internal/auth/additional_verification_test.go +++ b/internal/auth/additional_verification_test.go @@ -11,6 +11,7 @@ import ( "github.com/hashicorp/boundary/api/authtokens" "github.com/hashicorp/boundary/internal/auth/password" "github.com/hashicorp/boundary/internal/authtoken" + authpb "github.com/hashicorp/boundary/internal/gen/controller/auth" "github.com/hashicorp/boundary/internal/iam" "github.com/hashicorp/boundary/internal/servers" "github.com/hashicorp/boundary/internal/servers/controller" @@ -92,10 +93,10 @@ func TestFetchActionSetForId(t *testing.T) { authTokenRepoFn, serversRepoFn, tc.Kms(), - auth.RequestInfo{ + &authpb.RequestInfo{ PublicId: token.Id, EncryptedToken: strings.Split(token.Token, "_")[2], - TokenFormat: auth.AuthTokenTypeBearer, + TokenFormat: uint32(auth.AuthTokenTypeBearer), }) typ := resource.Target if tt.typeOverride != resource.Unknown { diff --git a/internal/errors/code.go b/internal/errors/code.go index bc4c254cf..02ea814ec 100644 --- a/internal/errors/code.go +++ b/internal/errors/code.go @@ -66,9 +66,11 @@ const ( GenKey Code = 304 // GenKey represents an error occurred during the underlying key generation process GenCert Code = 305 // GenCert represents an error occurred during the underlying certificate generation process - // General system errors are reserved Codes 400-599 and align with http client and server error codes - Forbidden Code = 403 // Forbidden represents the operation is forbidden - Internal Code = 500 // InternalError represents the system encountered an unexpected condition. + // General system errors are reserved Codes 400-599 and align with http + // client and server error codes + Unauthorized Code = 401 // Unauthorized represents the operation is unauthorized + Forbidden Code = 403 // Forbidden represents the operation is forbidden + Internal Code = 500 // InternalError represents the system encountered an unexpected condition. // DB errors are reserved Codes from 1000-1999 CheckConstraint Code = 1000 // CheckConstraint represents a check constraint error diff --git a/internal/errors/error.go b/internal/errors/error.go index c729f9e80..a4fdf3f99 100644 --- a/internal/errors/error.go +++ b/internal/errors/error.go @@ -7,6 +7,7 @@ import ( "runtime" "strings" + pberrors "github.com/hashicorp/boundary/internal/gen/errors" "github.com/hashicorp/boundary/internal/observability/event" "github.com/jackc/pgconn" ) @@ -266,6 +267,49 @@ func (e *Err) Error() string { return s.String() } +// ToPbErrors will convert to an Err protobuf +func ToPbErrors(err *Err) *pberrors.Err { + pbErr := &pberrors.Err{ + Code: uint32(err.Code), + Msg: err.Msg, + Op: string(err.Op), + } + + var wrappedErr *Err + isWrappedErr := As(err.Wrapped, &wrappedErr) + switch { + case err.Wrapped == nil: + pbErr.Wrapped = &pberrors.Err_None{ + None: false, + } + case isWrappedErr: + pbErr.Wrapped = &pberrors.Err_Err{ + Err: ToPbErrors(wrappedErr), + } + default: + pbErr.Wrapped = &pberrors.Err_StdError{ + StdError: err.Wrapped.Error(), + } + } + return pbErr +} + +// FromPbErrors will convert from Err protobuf +func FromPbErrors(pbErr *pberrors.Err) *Err { + err := &Err{ + Code: Code(pbErr.Code), + Msg: pbErr.Msg, + Op: Op(pbErr.Op), + } + switch w := pbErr.Wrapped.(type) { + case *pberrors.Err_Err: + err.Wrapped = FromPbErrors(w.Err) + case *pberrors.Err_StdError: + err.Wrapped = errors.New(w.StdError) + } + return err +} + func join(str *strings.Builder, delim string, s string) { if str.Len() == 0 { _, _ = str.WriteString(s) diff --git a/internal/errors/info.go b/internal/errors/info.go index 3d832623a..c9ec7a77c 100644 --- a/internal/errors/info.go +++ b/internal/errors/info.go @@ -148,6 +148,10 @@ var errorCodeInfo = map[Code]Info{ Message: "forbidden", Kind: Other, }, + Unauthorized: { + Message: "unauthorized", + Kind: Other, + }, CheckConstraint: { Message: "constraint check failed", Kind: Integrity, diff --git a/internal/gen/controller/api/empty_msg.pb.go b/internal/gen/controller/api/empty_msg.pb.go new file mode 100644 index 000000000..340e4d468 --- /dev/null +++ b/internal/gen/controller/api/empty_msg.pb.go @@ -0,0 +1,137 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.27.1 +// protoc v3.17.3 +// source: controller/api/v1/empty_msg.proto + +package api + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// EmptyResponse represents an empty response +type EmptyResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields +} + +func (x *EmptyResponse) Reset() { + *x = EmptyResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_api_v1_empty_msg_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *EmptyResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EmptyResponse) ProtoMessage() {} + +func (x *EmptyResponse) ProtoReflect() protoreflect.Message { + mi := &file_controller_api_v1_empty_msg_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use EmptyResponse.ProtoReflect.Descriptor instead. +func (*EmptyResponse) Descriptor() ([]byte, []int) { + return file_controller_api_v1_empty_msg_proto_rawDescGZIP(), []int{0} +} + +var File_controller_api_v1_empty_msg_proto protoreflect.FileDescriptor + +var file_controller_api_v1_empty_msg_proto_rawDesc = []byte{ + 0x0a, 0x21, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x61, 0x70, 0x69, + 0x2f, 0x76, 0x31, 0x2f, 0x65, 0x6d, 0x70, 0x74, 0x79, 0x5f, 0x6d, 0x73, 0x67, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x12, 0x11, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, + 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x22, 0x0f, 0x0a, 0x0d, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x3f, 0x5a, 0x3d, 0x67, 0x69, 0x74, 0x68, 0x75, + 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, + 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, + 0x6c, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, + 0x2f, 0x61, 0x70, 0x69, 0x3b, 0x61, 0x70, 0x69, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_controller_api_v1_empty_msg_proto_rawDescOnce sync.Once + file_controller_api_v1_empty_msg_proto_rawDescData = file_controller_api_v1_empty_msg_proto_rawDesc +) + +func file_controller_api_v1_empty_msg_proto_rawDescGZIP() []byte { + file_controller_api_v1_empty_msg_proto_rawDescOnce.Do(func() { + file_controller_api_v1_empty_msg_proto_rawDescData = protoimpl.X.CompressGZIP(file_controller_api_v1_empty_msg_proto_rawDescData) + }) + return file_controller_api_v1_empty_msg_proto_rawDescData +} + +var file_controller_api_v1_empty_msg_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_controller_api_v1_empty_msg_proto_goTypes = []interface{}{ + (*EmptyResponse)(nil), // 0: controller.api.v1.EmptyResponse +} +var file_controller_api_v1_empty_msg_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_controller_api_v1_empty_msg_proto_init() } +func file_controller_api_v1_empty_msg_proto_init() { + if File_controller_api_v1_empty_msg_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_controller_api_v1_empty_msg_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*EmptyResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_controller_api_v1_empty_msg_proto_rawDesc, + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_controller_api_v1_empty_msg_proto_goTypes, + DependencyIndexes: file_controller_api_v1_empty_msg_proto_depIdxs, + MessageInfos: file_controller_api_v1_empty_msg_proto_msgTypes, + }.Build() + File_controller_api_v1_empty_msg_proto = out.File + file_controller_api_v1_empty_msg_proto_rawDesc = nil + file_controller_api_v1_empty_msg_proto_goTypes = nil + file_controller_api_v1_empty_msg_proto_depIdxs = nil +} diff --git a/internal/gen/controller/auth/auth.pb.go b/internal/gen/controller/auth/auth.pb.go new file mode 100644 index 000000000..103a2360a --- /dev/null +++ b/internal/gen/controller/auth/auth.pb.go @@ -0,0 +1,263 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.27.1 +// protoc v3.17.3 +// source: controller/auth/v1/auth.proto + +package auth + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// RequestInfo contains request parameters necessary for checking authn/authz +type RequestInfo struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // path of the request + Path string `protobuf:"bytes,10,opt,name=path,proto3" json:"path,omitempty"` + // method (http verb) of the request + Method string `protobuf:"bytes,20,opt,name=method,proto3" json:"method,omitempty"` + // public_id for the request + PublicId string `protobuf:"bytes,30,opt,name=public_id,json=publicId,proto3" json:"public_id,omitempty"` + // encrypted_token from the request + EncryptedToken string `protobuf:"bytes,40,opt,name=encrypted_token,json=encryptedToken,proto3" json:"encrypted_token,omitempty"` + // token from the request + Token string `protobuf:"bytes,50,opt,name=token,proto3" json:"token,omitempty"` + // token_format of the request's token + TokenFormat uint32 `protobuf:"varint,60,opt,name=token_format,json=tokenFormat,proto3" json:"token_format,omitempty"` + // scope_id_override for the request (helpful for tests) + ScopeIdOverride string `protobuf:"bytes,70,opt,name=scope_id_override,json=scopeIdOverride,proto3" json:"scope_id_override,omitempty"` + // user_id_override for the request (helpful for tests) + UserIdOverride string `protobuf:"bytes,80,opt,name=user_id_override,json=userIdOverride,proto3" json:"user_id_override,omitempty"` + // disable_authz_failures for the request (helpful for tests) + DisableAuthzFailures bool `protobuf:"varint,90,opt,name=disable_authz_failures,json=disableAuthzFailures,proto3" json:"disable_authz_failures,omitempty"` + // disable_auth_entirely for the request (helpful for tests) + DisableAuthEntirely bool `protobuf:"varint,100,opt,name=disable_auth_entirely,json=disableAuthEntirely,proto3" json:"disable_auth_entirely,omitempty"` + // ticket is a unique id that allows the grpc-gateway to verify that the info + // came from its companion http proxy + Ticket string `protobuf:"bytes,110,opt,name=ticket,proto3" json:"ticket,omitempty"` +} + +func (x *RequestInfo) Reset() { + *x = RequestInfo{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_auth_v1_auth_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *RequestInfo) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RequestInfo) ProtoMessage() {} + +func (x *RequestInfo) ProtoReflect() protoreflect.Message { + mi := &file_controller_auth_v1_auth_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RequestInfo.ProtoReflect.Descriptor instead. +func (*RequestInfo) Descriptor() ([]byte, []int) { + return file_controller_auth_v1_auth_proto_rawDescGZIP(), []int{0} +} + +func (x *RequestInfo) GetPath() string { + if x != nil { + return x.Path + } + return "" +} + +func (x *RequestInfo) GetMethod() string { + if x != nil { + return x.Method + } + return "" +} + +func (x *RequestInfo) GetPublicId() string { + if x != nil { + return x.PublicId + } + return "" +} + +func (x *RequestInfo) GetEncryptedToken() string { + if x != nil { + return x.EncryptedToken + } + return "" +} + +func (x *RequestInfo) GetToken() string { + if x != nil { + return x.Token + } + return "" +} + +func (x *RequestInfo) GetTokenFormat() uint32 { + if x != nil { + return x.TokenFormat + } + return 0 +} + +func (x *RequestInfo) GetScopeIdOverride() string { + if x != nil { + return x.ScopeIdOverride + } + return "" +} + +func (x *RequestInfo) GetUserIdOverride() string { + if x != nil { + return x.UserIdOverride + } + return "" +} + +func (x *RequestInfo) GetDisableAuthzFailures() bool { + if x != nil { + return x.DisableAuthzFailures + } + return false +} + +func (x *RequestInfo) GetDisableAuthEntirely() bool { + if x != nil { + return x.DisableAuthEntirely + } + return false +} + +func (x *RequestInfo) GetTicket() string { + if x != nil { + return x.Ticket + } + return "" +} + +var File_controller_auth_v1_auth_proto protoreflect.FileDescriptor + +var file_controller_auth_v1_auth_proto_rawDesc = []byte{ + 0x0a, 0x1d, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x61, 0x75, 0x74, + 0x68, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, + 0x12, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x75, 0x74, 0x68, + 0x2e, 0x76, 0x31, 0x22, 0x90, 0x03, 0x0a, 0x0b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x49, + 0x6e, 0x66, 0x6f, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x74, 0x68, 0x18, 0x0a, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x04, 0x70, 0x61, 0x74, 0x68, 0x12, 0x16, 0x0a, 0x06, 0x6d, 0x65, 0x74, 0x68, 0x6f, + 0x64, 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, + 0x1b, 0x0a, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x64, 0x18, 0x1e, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x08, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x49, 0x64, 0x12, 0x27, 0x0a, 0x0f, + 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, + 0x28, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, + 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x32, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x21, 0x0a, 0x0c, 0x74, + 0x6f, 0x6b, 0x65, 0x6e, 0x5f, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x18, 0x3c, 0x20, 0x01, 0x28, + 0x0d, 0x52, 0x0b, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x12, 0x2a, + 0x0a, 0x11, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x5f, 0x69, 0x64, 0x5f, 0x6f, 0x76, 0x65, 0x72, 0x72, + 0x69, 0x64, 0x65, 0x18, 0x46, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x73, 0x63, 0x6f, 0x70, 0x65, + 0x49, 0x64, 0x4f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, 0x12, 0x28, 0x0a, 0x10, 0x75, 0x73, + 0x65, 0x72, 0x5f, 0x69, 0x64, 0x5f, 0x6f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, 0x18, 0x50, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x4f, 0x76, 0x65, 0x72, + 0x72, 0x69, 0x64, 0x65, 0x12, 0x34, 0x0a, 0x16, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, + 0x61, 0x75, 0x74, 0x68, 0x7a, 0x5f, 0x66, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x73, 0x18, 0x5a, + 0x20, 0x01, 0x28, 0x08, 0x52, 0x14, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x41, 0x75, 0x74, + 0x68, 0x7a, 0x46, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x73, 0x12, 0x32, 0x0a, 0x15, 0x64, 0x69, + 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x65, 0x6e, 0x74, 0x69, 0x72, + 0x65, 0x6c, 0x79, 0x18, 0x64, 0x20, 0x01, 0x28, 0x08, 0x52, 0x13, 0x64, 0x69, 0x73, 0x61, 0x62, + 0x6c, 0x65, 0x41, 0x75, 0x74, 0x68, 0x45, 0x6e, 0x74, 0x69, 0x72, 0x65, 0x6c, 0x79, 0x12, 0x16, + 0x0a, 0x06, 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x18, 0x6e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, + 0x74, 0x69, 0x63, 0x6b, 0x65, 0x74, 0x42, 0x41, 0x5a, 0x3f, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, + 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x62, + 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, + 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, + 0x61, 0x75, 0x74, 0x68, 0x3b, 0x61, 0x75, 0x74, 0x68, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x33, +} + +var ( + file_controller_auth_v1_auth_proto_rawDescOnce sync.Once + file_controller_auth_v1_auth_proto_rawDescData = file_controller_auth_v1_auth_proto_rawDesc +) + +func file_controller_auth_v1_auth_proto_rawDescGZIP() []byte { + file_controller_auth_v1_auth_proto_rawDescOnce.Do(func() { + file_controller_auth_v1_auth_proto_rawDescData = protoimpl.X.CompressGZIP(file_controller_auth_v1_auth_proto_rawDescData) + }) + return file_controller_auth_v1_auth_proto_rawDescData +} + +var file_controller_auth_v1_auth_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_controller_auth_v1_auth_proto_goTypes = []interface{}{ + (*RequestInfo)(nil), // 0: controller.auth.v1.RequestInfo +} +var file_controller_auth_v1_auth_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_controller_auth_v1_auth_proto_init() } +func file_controller_auth_v1_auth_proto_init() { + if File_controller_auth_v1_auth_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_controller_auth_v1_auth_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*RequestInfo); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_controller_auth_v1_auth_proto_rawDesc, + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_controller_auth_v1_auth_proto_goTypes, + DependencyIndexes: file_controller_auth_v1_auth_proto_depIdxs, + MessageInfos: file_controller_auth_v1_auth_proto_msgTypes, + }.Build() + File_controller_auth_v1_auth_proto = out.File + file_controller_auth_v1_auth_proto_rawDesc = nil + file_controller_auth_v1_auth_proto_goTypes = nil + file_controller_auth_v1_auth_proto_depIdxs = nil +} diff --git a/internal/gen/errors/errors.pb.go b/internal/gen/errors/errors.pb.go new file mode 100644 index 000000000..80d525d88 --- /dev/null +++ b/internal/gen/errors/errors.pb.go @@ -0,0 +1,322 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.27.1 +// protoc v3.17.3 +// source: errors/v1/errors.proto + +package errors + +import ( + api "github.com/hashicorp/boundary/internal/gen/controller/api" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// Err is a protobuf representation of a domain error +type Err struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // code is the error's code, which can be used to get the error's + // errorCodeInfo, which contains the error's Kind and Message + Code uint32 `protobuf:"varint,10,opt,name=code,proto3" json:"code,omitempty"` + // msg for the error + Msg string `protobuf:"bytes,20,opt,name=msg,proto3" json:"msg,omitempty"` + // op represents the operation raising/propagating an error and is optional. + Op string `protobuf:"bytes,30,opt,name=op,proto3" json:"op,omitempty"` + // wrapped represents an optionally wrapped error + // + // Types that are assignable to Wrapped: + // *Err_Err + // *Err_StdError + // *Err_None + Wrapped isErr_Wrapped `protobuf_oneof:"wrapped"` +} + +func (x *Err) Reset() { + *x = Err{} + if protoimpl.UnsafeEnabled { + mi := &file_errors_v1_errors_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Err) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Err) ProtoMessage() {} + +func (x *Err) ProtoReflect() protoreflect.Message { + mi := &file_errors_v1_errors_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Err.ProtoReflect.Descriptor instead. +func (*Err) Descriptor() ([]byte, []int) { + return file_errors_v1_errors_proto_rawDescGZIP(), []int{0} +} + +func (x *Err) GetCode() uint32 { + if x != nil { + return x.Code + } + return 0 +} + +func (x *Err) GetMsg() string { + if x != nil { + return x.Msg + } + return "" +} + +func (x *Err) GetOp() string { + if x != nil { + return x.Op + } + return "" +} + +func (m *Err) GetWrapped() isErr_Wrapped { + if m != nil { + return m.Wrapped + } + return nil +} + +func (x *Err) GetErr() *Err { + if x, ok := x.GetWrapped().(*Err_Err); ok { + return x.Err + } + return nil +} + +func (x *Err) GetStdError() string { + if x, ok := x.GetWrapped().(*Err_StdError); ok { + return x.StdError + } + return "" +} + +func (x *Err) GetNone() bool { + if x, ok := x.GetWrapped().(*Err_None); ok { + return x.None + } + return false +} + +type isErr_Wrapped interface { + isErr_Wrapped() +} + +type Err_Err struct { + // err is a wrapped Err (aka a domain error) + Err *Err `protobuf:"bytes,40,opt,name=err,proto3,oneof"` +} + +type Err_StdError struct { + // std_error is a wrapped std error + StdError string `protobuf:"bytes,50,opt,name=std_error,json=stdError,proto3,oneof"` +} + +type Err_None struct { + // no error or err is wrapped + None bool `protobuf:"varint,60,opt,name=none,proto3,oneof"` +} + +func (*Err_Err) isErr_Wrapped() {} + +func (*Err_StdError) isErr_Wrapped() {} + +func (*Err_None) isErr_Wrapped() {} + +// ApiError is a protobuf representation of a controller ApiError +type ApiError struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // api_error is the controller ApiError + ApiError *api.Error `protobuf:"bytes,10,opt,name=api_error,json=apiError,proto3" json:"api_error,omitempty"` + // status is the status code assocatiated with the api error + Status int32 `protobuf:"varint,20,opt,name=status,proto3" json:"status,omitempty"` +} + +func (x *ApiError) Reset() { + *x = ApiError{} + if protoimpl.UnsafeEnabled { + mi := &file_errors_v1_errors_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ApiError) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ApiError) ProtoMessage() {} + +func (x *ApiError) ProtoReflect() protoreflect.Message { + mi := &file_errors_v1_errors_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ApiError.ProtoReflect.Descriptor instead. +func (*ApiError) Descriptor() ([]byte, []int) { + return file_errors_v1_errors_proto_rawDescGZIP(), []int{1} +} + +func (x *ApiError) GetApiError() *api.Error { + if x != nil { + return x.ApiError + } + return nil +} + +func (x *ApiError) GetStatus() int32 { + if x != nil { + return x.Status + } + return 0 +} + +var File_errors_v1_errors_proto protoreflect.FileDescriptor + +var file_errors_v1_errors_proto_rawDesc = []byte{ + 0x0a, 0x16, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x73, 0x2f, 0x76, 0x31, 0x2f, 0x65, 0x72, 0x72, 0x6f, + 0x72, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x09, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x73, + 0x2e, 0x76, 0x31, 0x1a, 0x1d, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, + 0x61, 0x70, 0x69, 0x2f, 0x76, 0x31, 0x2f, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x22, 0x9f, 0x01, 0x0a, 0x03, 0x45, 0x72, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f, + 0x64, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x12, 0x10, + 0x0a, 0x03, 0x6d, 0x73, 0x67, 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6d, 0x73, 0x67, + 0x12, 0x0e, 0x0a, 0x02, 0x6f, 0x70, 0x18, 0x1e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x6f, 0x70, + 0x12, 0x22, 0x0a, 0x03, 0x65, 0x72, 0x72, 0x18, 0x28, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0e, 0x2e, + 0x65, 0x72, 0x72, 0x6f, 0x72, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x72, 0x72, 0x48, 0x00, 0x52, + 0x03, 0x65, 0x72, 0x72, 0x12, 0x1d, 0x0a, 0x09, 0x73, 0x74, 0x64, 0x5f, 0x65, 0x72, 0x72, 0x6f, + 0x72, 0x18, 0x32, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x08, 0x73, 0x74, 0x64, 0x45, 0x72, + 0x72, 0x6f, 0x72, 0x12, 0x14, 0x0a, 0x04, 0x6e, 0x6f, 0x6e, 0x65, 0x18, 0x3c, 0x20, 0x01, 0x28, + 0x08, 0x48, 0x00, 0x52, 0x04, 0x6e, 0x6f, 0x6e, 0x65, 0x42, 0x09, 0x0a, 0x07, 0x77, 0x72, 0x61, + 0x70, 0x70, 0x65, 0x64, 0x22, 0x59, 0x0a, 0x08, 0x41, 0x70, 0x69, 0x45, 0x72, 0x72, 0x6f, 0x72, + 0x12, 0x35, 0x0a, 0x09, 0x61, 0x70, 0x69, 0x5f, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x0a, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, + 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x52, 0x08, 0x61, + 0x70, 0x69, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, + 0x73, 0x18, 0x14, 0x20, 0x01, 0x28, 0x05, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x42, + 0x3a, 0x5a, 0x38, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, + 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, + 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x65, 0x72, + 0x72, 0x6f, 0x72, 0x73, 0x3b, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x33, +} + +var ( + file_errors_v1_errors_proto_rawDescOnce sync.Once + file_errors_v1_errors_proto_rawDescData = file_errors_v1_errors_proto_rawDesc +) + +func file_errors_v1_errors_proto_rawDescGZIP() []byte { + file_errors_v1_errors_proto_rawDescOnce.Do(func() { + file_errors_v1_errors_proto_rawDescData = protoimpl.X.CompressGZIP(file_errors_v1_errors_proto_rawDescData) + }) + return file_errors_v1_errors_proto_rawDescData +} + +var file_errors_v1_errors_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_errors_v1_errors_proto_goTypes = []interface{}{ + (*Err)(nil), // 0: errors.v1.Err + (*ApiError)(nil), // 1: errors.v1.ApiError + (*api.Error)(nil), // 2: controller.api.v1.Error +} +var file_errors_v1_errors_proto_depIdxs = []int32{ + 0, // 0: errors.v1.Err.err:type_name -> errors.v1.Err + 2, // 1: errors.v1.ApiError.api_error:type_name -> controller.api.v1.Error + 2, // [2:2] is the sub-list for method output_type + 2, // [2:2] is the sub-list for method input_type + 2, // [2:2] is the sub-list for extension type_name + 2, // [2:2] is the sub-list for extension extendee + 0, // [0:2] is the sub-list for field type_name +} + +func init() { file_errors_v1_errors_proto_init() } +func file_errors_v1_errors_proto_init() { + if File_errors_v1_errors_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_errors_v1_errors_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Err); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_errors_v1_errors_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ApiError); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + file_errors_v1_errors_proto_msgTypes[0].OneofWrappers = []interface{}{ + (*Err_Err)(nil), + (*Err_StdError)(nil), + (*Err_None)(nil), + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_errors_v1_errors_proto_rawDesc, + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_errors_v1_errors_proto_goTypes, + DependencyIndexes: file_errors_v1_errors_proto_depIdxs, + MessageInfos: file_errors_v1_errors_proto_msgTypes, + }.Build() + File_errors_v1_errors_proto = out.File + file_errors_v1_errors_proto_rawDesc = nil + file_errors_v1_errors_proto_goTypes = nil + file_errors_v1_errors_proto_depIdxs = nil +} diff --git a/internal/gen/testing/interceptor/greeter.pb.go b/internal/gen/testing/interceptor/greeter.pb.go new file mode 100644 index 000000000..a4b9df67f --- /dev/null +++ b/internal/gen/testing/interceptor/greeter.pb.go @@ -0,0 +1,222 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.27.1 +// protoc v3.17.3 +// source: testing/interceptor/v1/greeter.proto + +package interceptor + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// The request message containing the user's name. +type SayHelloRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` +} + +func (x *SayHelloRequest) Reset() { + *x = SayHelloRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_testing_interceptor_v1_greeter_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SayHelloRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SayHelloRequest) ProtoMessage() {} + +func (x *SayHelloRequest) ProtoReflect() protoreflect.Message { + mi := &file_testing_interceptor_v1_greeter_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SayHelloRequest.ProtoReflect.Descriptor instead. +func (*SayHelloRequest) Descriptor() ([]byte, []int) { + return file_testing_interceptor_v1_greeter_proto_rawDescGZIP(), []int{0} +} + +func (x *SayHelloRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +// The response message containing the greetings +type SayHelloResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"` +} + +func (x *SayHelloResponse) Reset() { + *x = SayHelloResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_testing_interceptor_v1_greeter_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SayHelloResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SayHelloResponse) ProtoMessage() {} + +func (x *SayHelloResponse) ProtoReflect() protoreflect.Message { + mi := &file_testing_interceptor_v1_greeter_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SayHelloResponse.ProtoReflect.Descriptor instead. +func (*SayHelloResponse) Descriptor() ([]byte, []int) { + return file_testing_interceptor_v1_greeter_proto_rawDescGZIP(), []int{1} +} + +func (x *SayHelloResponse) GetMessage() string { + if x != nil { + return x.Message + } + return "" +} + +var File_testing_interceptor_v1_greeter_proto protoreflect.FileDescriptor + +var file_testing_interceptor_v1_greeter_proto_rawDesc = []byte{ + 0x0a, 0x24, 0x74, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x63, + 0x65, 0x70, 0x74, 0x6f, 0x72, 0x2f, 0x76, 0x31, 0x2f, 0x67, 0x72, 0x65, 0x65, 0x74, 0x65, 0x72, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x16, 0x74, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x2e, + 0x69, 0x6e, 0x74, 0x65, 0x72, 0x63, 0x65, 0x70, 0x74, 0x6f, 0x72, 0x2e, 0x76, 0x31, 0x22, 0x25, + 0x0a, 0x0f, 0x53, 0x61, 0x79, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x2c, 0x0a, 0x10, 0x53, 0x61, 0x79, 0x48, 0x65, 0x6c, 0x6c, + 0x6f, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, + 0x73, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, + 0x61, 0x67, 0x65, 0x32, 0x71, 0x0a, 0x0e, 0x47, 0x72, 0x65, 0x65, 0x74, 0x65, 0x72, 0x53, 0x65, + 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x5f, 0x0a, 0x08, 0x53, 0x61, 0x79, 0x48, 0x65, 0x6c, 0x6c, + 0x6f, 0x12, 0x27, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x2e, 0x69, 0x6e, 0x74, 0x65, + 0x72, 0x63, 0x65, 0x70, 0x74, 0x6f, 0x72, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x61, 0x79, 0x48, 0x65, + 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x28, 0x2e, 0x74, 0x65, 0x73, + 0x74, 0x69, 0x6e, 0x67, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x63, 0x65, 0x70, 0x74, 0x6f, 0x72, + 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x61, 0x79, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x4c, 0x5a, 0x4a, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, + 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x62, + 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, + 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x74, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x67, 0x2f, 0x69, 0x6e, 0x74, + 0x65, 0x72, 0x63, 0x65, 0x70, 0x74, 0x6f, 0x72, 0x3b, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x63, 0x65, + 0x70, 0x74, 0x6f, 0x72, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_testing_interceptor_v1_greeter_proto_rawDescOnce sync.Once + file_testing_interceptor_v1_greeter_proto_rawDescData = file_testing_interceptor_v1_greeter_proto_rawDesc +) + +func file_testing_interceptor_v1_greeter_proto_rawDescGZIP() []byte { + file_testing_interceptor_v1_greeter_proto_rawDescOnce.Do(func() { + file_testing_interceptor_v1_greeter_proto_rawDescData = protoimpl.X.CompressGZIP(file_testing_interceptor_v1_greeter_proto_rawDescData) + }) + return file_testing_interceptor_v1_greeter_proto_rawDescData +} + +var file_testing_interceptor_v1_greeter_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_testing_interceptor_v1_greeter_proto_goTypes = []interface{}{ + (*SayHelloRequest)(nil), // 0: testing.interceptor.v1.SayHelloRequest + (*SayHelloResponse)(nil), // 1: testing.interceptor.v1.SayHelloResponse +} +var file_testing_interceptor_v1_greeter_proto_depIdxs = []int32{ + 0, // 0: testing.interceptor.v1.GreeterService.SayHello:input_type -> testing.interceptor.v1.SayHelloRequest + 1, // 1: testing.interceptor.v1.GreeterService.SayHello:output_type -> testing.interceptor.v1.SayHelloResponse + 1, // [1:2] is the sub-list for method output_type + 0, // [0:1] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_testing_interceptor_v1_greeter_proto_init() } +func file_testing_interceptor_v1_greeter_proto_init() { + if File_testing_interceptor_v1_greeter_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_testing_interceptor_v1_greeter_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SayHelloRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_testing_interceptor_v1_greeter_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SayHelloResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_testing_interceptor_v1_greeter_proto_rawDesc, + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_testing_interceptor_v1_greeter_proto_goTypes, + DependencyIndexes: file_testing_interceptor_v1_greeter_proto_depIdxs, + MessageInfos: file_testing_interceptor_v1_greeter_proto_msgTypes, + }.Build() + File_testing_interceptor_v1_greeter_proto = out.File + file_testing_interceptor_v1_greeter_proto_rawDesc = nil + file_testing_interceptor_v1_greeter_proto_goTypes = nil + file_testing_interceptor_v1_greeter_proto_depIdxs = nil +} diff --git a/internal/gen/testing/interceptor/greeter_grpc.pb.go b/internal/gen/testing/interceptor/greeter_grpc.pb.go new file mode 100644 index 000000000..7a4e8ce89 --- /dev/null +++ b/internal/gen/testing/interceptor/greeter_grpc.pb.go @@ -0,0 +1,103 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. + +package interceptor + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.32.0 or later. +const _ = grpc.SupportPackageIsVersion7 + +// GreeterServiceClient is the client API for GreeterService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type GreeterServiceClient interface { + // Sends a greeting + SayHello(ctx context.Context, in *SayHelloRequest, opts ...grpc.CallOption) (*SayHelloResponse, error) +} + +type greeterServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewGreeterServiceClient(cc grpc.ClientConnInterface) GreeterServiceClient { + return &greeterServiceClient{cc} +} + +func (c *greeterServiceClient) SayHello(ctx context.Context, in *SayHelloRequest, opts ...grpc.CallOption) (*SayHelloResponse, error) { + out := new(SayHelloResponse) + err := c.cc.Invoke(ctx, "/testing.interceptor.v1.GreeterService/SayHello", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// GreeterServiceServer is the server API for GreeterService service. +// All implementations must embed UnimplementedGreeterServiceServer +// for forward compatibility +type GreeterServiceServer interface { + // Sends a greeting + SayHello(context.Context, *SayHelloRequest) (*SayHelloResponse, error) + mustEmbedUnimplementedGreeterServiceServer() +} + +// UnimplementedGreeterServiceServer must be embedded to have forward compatible implementations. +type UnimplementedGreeterServiceServer struct { +} + +func (UnimplementedGreeterServiceServer) SayHello(context.Context, *SayHelloRequest) (*SayHelloResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method SayHello not implemented") +} +func (UnimplementedGreeterServiceServer) mustEmbedUnimplementedGreeterServiceServer() {} + +// UnsafeGreeterServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to GreeterServiceServer will +// result in compilation errors. +type UnsafeGreeterServiceServer interface { + mustEmbedUnimplementedGreeterServiceServer() +} + +func RegisterGreeterServiceServer(s grpc.ServiceRegistrar, srv GreeterServiceServer) { + s.RegisterService(&GreeterService_ServiceDesc, srv) +} + +func _GreeterService_SayHello_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SayHelloRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GreeterServiceServer).SayHello(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/testing.interceptor.v1.GreeterService/SayHello", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GreeterServiceServer).SayHello(ctx, req.(*SayHelloRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// GreeterService_ServiceDesc is the grpc.ServiceDesc for GreeterService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var GreeterService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "testing.interceptor.v1.GreeterService", + HandlerType: (*GreeterServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "SayHello", + Handler: _GreeterService_SayHello_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "testing/interceptor/v1/greeter.proto", +} diff --git a/internal/proto/local/controller/api/v1/empty_msg.proto b/internal/proto/local/controller/api/v1/empty_msg.proto new file mode 100644 index 000000000..2be840d52 --- /dev/null +++ b/internal/proto/local/controller/api/v1/empty_msg.proto @@ -0,0 +1,8 @@ +syntax = "proto3"; + +package controller.api.v1; + +option go_package = "github.com/hashicorp/boundary/internal/gen/controller/api;api"; + +// EmptyResponse represents an empty response +message EmptyResponse {} diff --git a/internal/proto/local/controller/auth/v1/auth.proto b/internal/proto/local/controller/auth/v1/auth.proto new file mode 100644 index 000000000..840ffa12b --- /dev/null +++ b/internal/proto/local/controller/auth/v1/auth.proto @@ -0,0 +1,42 @@ +syntax = "proto3"; + +package controller.auth.v1; + +option go_package = "github.com/hashicorp/boundary/internal/gen/controller/auth;auth"; + +// RequestInfo contains request parameters necessary for checking authn/authz +message RequestInfo { + // path of the request + string path = 10; + + // method (http verb) of the request + string method = 20; + + // public_id for the request + string public_id = 30; + + // encrypted_token from the request + string encrypted_token = 40; + + // token from the request + string token = 50; + + // token_format of the request's token + uint32 token_format = 60; + + // scope_id_override for the request (helpful for tests) + string scope_id_override = 70; + + // user_id_override for the request (helpful for tests) + string user_id_override = 80; + + // disable_authz_failures for the request (helpful for tests) + bool disable_authz_failures = 90; + + // disable_auth_entirely for the request (helpful for tests) + bool disable_auth_entirely = 100; + + // ticket is a unique id that allows the grpc-gateway to verify that the info + // came from its companion http proxy + string ticket = 110; +} diff --git a/internal/proto/local/errors/v1/errors.proto b/internal/proto/local/errors/v1/errors.proto new file mode 100644 index 000000000..d8acb33fc --- /dev/null +++ b/internal/proto/local/errors/v1/errors.proto @@ -0,0 +1,41 @@ +syntax = "proto3"; + +package errors.v1; + +option go_package = "github.com/hashicorp/boundary/internal/gen/errors;errors"; + +import "controller/api/v1/error.proto"; + +// Err is a protobuf representation of a domain error +message Err { + // code is the error's code, which can be used to get the error's + // errorCodeInfo, which contains the error's Kind and Message + uint32 code = 10; + + // msg for the error + string msg = 20; + + // op represents the operation raising/propagating an error and is optional. + string op = 30; + + // wrapped represents an optionally wrapped error + oneof wrapped { + // err is a wrapped Err (aka a domain error) + Err err = 40; + + // std_error is a wrapped std error + string std_error = 50; + + // no error or err is wrapped + bool none = 60; + } +} + +// ApiError is a protobuf representation of a controller ApiError +message ApiError { + // api_error is the controller ApiError + controller.api.v1.Error api_error = 10; + + // status is the status code assocatiated with the api error + int32 status = 20; +} \ No newline at end of file diff --git a/internal/proto/local/testing/interceptor/v1/greeter.proto b/internal/proto/local/testing/interceptor/v1/greeter.proto new file mode 100644 index 000000000..f1fc1b4e5 --- /dev/null +++ b/internal/proto/local/testing/interceptor/v1/greeter.proto @@ -0,0 +1,22 @@ +syntax = "proto3"; + +package testing.interceptor.v1; + +option go_package = "github.com/hashicorp/boundary/internal/gen/testing/interceptor;interceptor"; + +// The greeting service definition. This is just a dead simple service that can +// be used to test interceptors. (See controller.startTestGreeterService) +service GreeterService { + // Sends a greeting + rpc SayHello(SayHelloRequest) returns (SayHelloResponse) {} +} + +// The request message containing the user's name. +message SayHelloRequest { + string name = 1; +} + +// The response message containing the greetings +message SayHelloResponse { + string message = 1; +} \ No newline at end of file diff --git a/internal/servers/controller/auth/auth.go b/internal/servers/controller/auth/auth.go index 6fd9f6946..b72d4b7c5 100644 --- a/internal/servers/controller/auth/auth.go +++ b/internal/servers/controller/auth/auth.go @@ -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) } diff --git a/internal/servers/controller/auth/auth_test.go b/internal/servers/controller/auth/auth_test.go index 574ac0cb3..e90b523f2 100644 --- a/internal/servers/controller/auth/auth_test.go +++ b/internal/servers/controller/auth/auth_test.go @@ -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) diff --git a/internal/servers/controller/auth/testing.go b/internal/servers/controller/auth/testing.go index 731da8bd2..6a50b5b49 100644 --- a/internal/servers/controller/auth/testing.go +++ b/internal/servers/controller/auth/testing.go @@ -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) } diff --git a/internal/servers/controller/common/scopeids/scope_ids_test.go b/internal/servers/controller/common/scopeids/scope_ids_test.go index 570098e9d..d0598b975 100644 --- a/internal/servers/controller/common/scopeids/scope_ids_test.go +++ b/internal/servers/controller/common/scopeids/scope_ids_test.go @@ -7,6 +7,7 @@ import ( "github.com/hashicorp/boundary/internal/authtoken" "github.com/hashicorp/boundary/internal/db" pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services" + 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" @@ -229,9 +230,9 @@ func TestListingScopeIds(t *testing.T) { authTokenRepoFn, serversRepoFn, kms, - auth.RequestInfo{ + &authpb.RequestInfo{ Token: at.GetToken(), - TokenFormat: auth.AuthTokenTypeBearer, + TokenFormat: uint32(auth.AuthTokenTypeBearer), PublicId: at.GetPublicId(), }) diff --git a/internal/servers/controller/controller.go b/internal/servers/controller/controller.go index 0e389e3b2..6041e93aa 100644 --- a/internal/servers/controller/controller.go +++ b/internal/servers/controller/controller.go @@ -6,6 +6,8 @@ import ( "fmt" "sync" + "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" + "github.com/hashicorp/boundary/internal/auth/oidc" "github.com/hashicorp/boundary/internal/auth/password" "github.com/hashicorp/boundary/internal/authtoken" @@ -26,6 +28,7 @@ import ( "github.com/hashicorp/go-secure-stdlib/mlock" "github.com/patrickmn/go-cache" ua "go.uber.org/atomic" + "google.golang.org/grpc" ) type Controller struct { @@ -44,6 +47,12 @@ type Controller struct { // Used for testing and tracking worker health workerStatusUpdateTimes *sync.Map + // grpc gateway server + gatewayServer *grpc.Server + gatewayTicket string + gatewayListener gatewayListener + gatewayMux *runtime.ServeMux + // Repo factory methods AuthTokenRepoFn common.AuthTokenRepoFactory VaultCredentialRepoFn common.VaultCredentialRepoFactory @@ -172,7 +181,7 @@ func (c *Controller) Start() error { if err := c.scheduler.Start(c.baseContext, &c.schedulerWg); err != nil { return fmt.Errorf("error starting scheduler: %w", err) } - if err := c.startListeners(); err != nil { + if err := c.startListeners(c.baseContext); err != nil { return fmt.Errorf("error starting controller listeners: %w", err) } diff --git a/internal/servers/controller/gateway.go b/internal/servers/controller/gateway.go new file mode 100644 index 000000000..41f6d76a9 --- /dev/null +++ b/internal/servers/controller/gateway.go @@ -0,0 +1,67 @@ +package controller + +import ( + "context" + "math" + "net" + + grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware" + "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" + "github.com/hashicorp/boundary/globals" + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/servers/controller/common" + "github.com/hashicorp/boundary/internal/servers/controller/handlers" + "google.golang.org/grpc" + "google.golang.org/grpc/test/bufconn" +) + +// newGatewayListener will create an in-memory listener +func newGatewayListener() (gatewayListener, string) { + buffer := globals.DefaultMaxRequestSize // seems like a reasonable size for the ring buffer, but then happily change the size if more info becomes available + return bufconn.Listen(int(buffer)), "" +} + +const gatewayTarget = "" + +type gatewayListener interface { + net.Listener + Dial() (net.Conn, error) +} + +func gatewayDialOptions(lis gatewayListener) []grpc.DialOption { + return []grpc.DialOption{ + grpc.WithInsecure(), + grpc.WithContextDialer(func(context.Context, string) (net.Conn, error) { + return lis.Dial() + }), + } +} + +func newGatewayMux() *runtime.ServeMux { + return runtime.NewServeMux( + runtime.WithMarshalerOption(runtime.MIMEWildcard, &runtime.HTTPBodyMarshaler{ + Marshaler: handlers.JSONMarshaler(), + }), + runtime.WithErrorHandler(handlers.ErrorHandler()), + runtime.WithForwardResponseOption(handlers.OutgoingResponseFilter), + ) +} + +func newGatewayServer(ctx context.Context, iamRepoFn common.IamRepoFactory, authTokenRepoFn common.AuthTokenRepoFactory, serversRepoFn common.ServersRepoFactory, kms *kms.Kms) (*grpc.Server, string, error) { + const op = "controller.newGatewayServer" + ticket, err := db.NewPrivateId("gwticket") + if err != nil { + return nil, "", errors.Wrap(ctx, err, op, errors.WithMsg("unable to generate gateway ticket")) + } + requestCtxInterceptor, err := requestCtxInterceptor(ctx, iamRepoFn, authTokenRepoFn, serversRepoFn, kms, ticket) + if err != nil { + return nil, "", err + } + return grpc.NewServer( + grpc.MaxRecvMsgSize(math.MaxInt32), + grpc.MaxSendMsgSize(math.MaxInt32), + grpc.UnaryInterceptor(grpc_middleware.ChainUnaryServer(requestCtxInterceptor, errorInterceptor(ctx), statusCodeInterceptor(ctx))), + ), ticket, nil +} diff --git a/internal/servers/controller/handler.go b/internal/servers/controller/handler.go index 0bd972914..da5f6d5ec 100644 --- a/internal/servers/controller/handler.go +++ b/internal/servers/controller/handler.go @@ -13,35 +13,36 @@ import ( "strings" "time" - "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" "github.com/hashicorp/boundary/globals" "github.com/hashicorp/boundary/internal/auth/oidc" "github.com/hashicorp/boundary/internal/gen/controller/api/services" + authpb "github.com/hashicorp/boundary/internal/gen/controller/auth" "github.com/hashicorp/boundary/internal/observability/event" - "github.com/hashicorp/boundary/internal/requests" "github.com/hashicorp/boundary/internal/servers/common" "github.com/hashicorp/boundary/internal/servers/controller/auth" "github.com/hashicorp/boundary/internal/servers/controller/handlers/accounts" "github.com/hashicorp/boundary/internal/servers/controller/handlers/authmethods" + "github.com/hashicorp/boundary/internal/servers/controller/handlers/authtokens" "github.com/hashicorp/boundary/internal/servers/controller/handlers/credentiallibraries" "github.com/hashicorp/boundary/internal/servers/controller/handlers/credentialstores" + "github.com/hashicorp/boundary/internal/servers/controller/handlers/groups" + "github.com/hashicorp/boundary/internal/servers/controller/handlers/host_catalogs" "github.com/hashicorp/boundary/internal/servers/controller/handlers/host_sets" + "github.com/hashicorp/boundary/internal/servers/controller/handlers/hosts" "github.com/hashicorp/boundary/internal/servers/controller/handlers/managed_groups" + "github.com/hashicorp/boundary/internal/servers/controller/handlers/roles" + "github.com/hashicorp/boundary/internal/servers/controller/handlers/scopes" "github.com/hashicorp/boundary/internal/servers/controller/handlers/sessions" "github.com/hashicorp/boundary/internal/servers/controller/handlers/targets" + "github.com/hashicorp/boundary/internal/servers/controller/handlers/users" "github.com/hashicorp/go-cleanhttp" "github.com/hashicorp/go-secure-stdlib/listenerutil" "github.com/hashicorp/go-secure-stdlib/strutil" + "github.com/mr-tron/base58" "google.golang.org/grpc/codes" + "google.golang.org/protobuf/proto" "github.com/hashicorp/boundary/internal/servers/controller/handlers" - "github.com/hashicorp/boundary/internal/servers/controller/handlers/authtokens" - "github.com/hashicorp/boundary/internal/servers/controller/handlers/groups" - "github.com/hashicorp/boundary/internal/servers/controller/handlers/host_catalogs" - "github.com/hashicorp/boundary/internal/servers/controller/handlers/hosts" - "github.com/hashicorp/boundary/internal/servers/controller/handlers/roles" - "github.com/hashicorp/boundary/internal/servers/controller/handlers/scopes" - "github.com/hashicorp/boundary/internal/servers/controller/handlers/users" ) type HandlerProperties struct { @@ -78,127 +79,169 @@ func handleGrpcGateway(c *Controller, props HandlerProperties) (http.Handler, er // Register*ServiceHandlerServer methods ignore the passed in ctx. Using it // now however in case this changes in the future. ctx := props.CancelCtx - mux := runtime.NewServeMux( - runtime.WithMarshalerOption(runtime.MIMEWildcard, &runtime.HTTPBodyMarshaler{ - Marshaler: handlers.JSONMarshaler(), - }), - runtime.WithErrorHandler(handlers.ErrorHandler()), - runtime.WithForwardResponseOption(handlers.OutgoingInterceptor), - ) - hcs, err := host_catalogs.NewService(c.StaticHostRepoFn, c.IamRepoFn) - if err != nil { - return nil, fmt.Errorf("failed to create host catalog handler service: %w", err) + + currentServices := c.gatewayServer.GetServiceInfo() + dialOptions := gatewayDialOptions(c.gatewayListener) + + if _, ok := currentServices[services.HostCatalogService_ServiceDesc.ServiceName]; !ok { + hcs, err := host_catalogs.NewService(c.StaticHostRepoFn, c.IamRepoFn) + if err != nil { + return nil, fmt.Errorf("failed to create host catalog handler service: %w", err) + } + services.RegisterHostCatalogServiceServer(c.gatewayServer, hcs) + if err := services.RegisterHostCatalogServiceHandlerFromEndpoint(ctx, c.gatewayMux, gatewayTarget, dialOptions); err != nil { + return nil, fmt.Errorf("failed to register host catalog service handler: %w", err) + } } - if err := services.RegisterHostCatalogServiceHandlerServer(ctx, mux, hcs); err != nil { - return nil, fmt.Errorf("failed to register host catalog service handler: %w", err) + if _, ok := currentServices[services.HostSetService_ServiceDesc.ServiceName]; !ok { + hss, err := host_sets.NewService(c.StaticHostRepoFn) + if err != nil { + return nil, fmt.Errorf("failed to create host set handler service: %w", err) + } + services.RegisterHostSetServiceServer(c.gatewayServer, hss) + if err := services.RegisterHostSetServiceHandlerFromEndpoint(ctx, c.gatewayMux, gatewayTarget, dialOptions); err != nil { + return nil, fmt.Errorf("failed to register host set service handler: %w", err) + } } - hss, err := host_sets.NewService(c.StaticHostRepoFn) - if err != nil { - return nil, fmt.Errorf("failed to create host set handler service: %w", err) + if _, ok := currentServices[services.HostService_ServiceDesc.ServiceName]; !ok { + hs, err := hosts.NewService(c.StaticHostRepoFn) + if err != nil { + return nil, fmt.Errorf("failed to create host handler service: %w", err) + } + services.RegisterHostServiceServer(c.gatewayServer, hs) + if err := services.RegisterHostServiceHandlerFromEndpoint(ctx, c.gatewayMux, gatewayTarget, dialOptions); err != nil { + return nil, fmt.Errorf("failed to register host service handler: %w", err) + } } - if err := services.RegisterHostSetServiceHandlerServer(ctx, mux, hss); err != nil { - return nil, fmt.Errorf("failed to register host set service handler: %w", err) + if _, ok := currentServices[services.AccountService_ServiceDesc.ServiceName]; !ok { + accts, err := accounts.NewService(c.PasswordAuthRepoFn, c.OidcRepoFn) + if err != nil { + return nil, fmt.Errorf("failed to create account handler service: %w", err) + } + services.RegisterAccountServiceServer(c.gatewayServer, accts) + if err := services.RegisterAccountServiceHandlerFromEndpoint(ctx, c.gatewayMux, gatewayTarget, dialOptions); err != nil { + return nil, fmt.Errorf("failed to register account service handler: %w", err) + } } - hs, err := hosts.NewService(c.StaticHostRepoFn) - if err != nil { - return nil, fmt.Errorf("failed to create host handler service: %w", err) + if _, ok := currentServices[services.AuthMethodService_ServiceDesc.ServiceName]; !ok { + authMethods, err := authmethods.NewService(c.kms, c.PasswordAuthRepoFn, c.OidcRepoFn, c.IamRepoFn, c.AuthTokenRepoFn) + if err != nil { + return nil, fmt.Errorf("failed to create auth method handler service: %w", err) + } + services.RegisterAuthMethodServiceServer(c.gatewayServer, authMethods) + if err := services.RegisterAuthMethodServiceHandlerFromEndpoint(ctx, c.gatewayMux, gatewayTarget, dialOptions); err != nil { + return nil, fmt.Errorf("failed to register auth method service handler: %w", err) + } } - if err := services.RegisterHostServiceHandlerServer(ctx, mux, hs); err != nil { - return nil, fmt.Errorf("failed to register host service handler: %w", err) + if _, ok := currentServices[services.AuthTokenService_ServiceDesc.ServiceName]; !ok { + authtoks, err := authtokens.NewService(c.AuthTokenRepoFn, c.IamRepoFn) + if err != nil { + return nil, fmt.Errorf("failed to create auth token handler service: %w", err) + } + services.RegisterAuthTokenServiceServer(c.gatewayServer, authtoks) + if err := services.RegisterAuthTokenServiceHandlerFromEndpoint(ctx, c.gatewayMux, gatewayTarget, dialOptions); err != nil { + return nil, fmt.Errorf("failed to register auth token service handler: %w", err) + } } - accts, err := accounts.NewService(c.PasswordAuthRepoFn, c.OidcRepoFn) - if err != nil { - return nil, fmt.Errorf("failed to create account handler service: %w", err) + if _, ok := currentServices[services.ScopeService_ServiceDesc.ServiceName]; !ok { + os, err := scopes.NewService(c.IamRepoFn) + if err != nil { + return nil, fmt.Errorf("failed to create scope handler service: %w", err) + } + services.RegisterScopeServiceServer(c.gatewayServer, os) + if err := services.RegisterScopeServiceHandlerFromEndpoint(ctx, c.gatewayMux, gatewayTarget, dialOptions); err != nil { + return nil, fmt.Errorf("failed to register scope service handler: %w", err) + } } - if err := services.RegisterAccountServiceHandlerServer(ctx, mux, accts); err != nil { - return nil, fmt.Errorf("failed to register account service handler: %w", err) + if _, ok := currentServices[services.UserService_ServiceDesc.ServiceName]; !ok { + us, err := users.NewService(c.IamRepoFn) + if err != nil { + return nil, fmt.Errorf("failed to create user handler service: %w", err) + } + services.RegisterUserServiceServer(c.gatewayServer, us) + if err := services.RegisterUserServiceHandlerFromEndpoint(ctx, c.gatewayMux, gatewayTarget, dialOptions); err != nil { + return nil, fmt.Errorf("failed to register user service handler: %w", err) + } } - authMethods, err := authmethods.NewService(c.kms, c.PasswordAuthRepoFn, c.OidcRepoFn, c.IamRepoFn, c.AuthTokenRepoFn) - if err != nil { - return nil, fmt.Errorf("failed to create auth method handler service: %w", err) + if _, ok := currentServices[services.TargetService_ServiceDesc.ServiceName]; !ok { + ts, err := targets.NewService( + c.kms, + c.TargetRepoFn, + c.IamRepoFn, + c.ServersRepoFn, + c.SessionRepoFn, + c.StaticHostRepoFn, + c.VaultCredentialRepoFn) + if err != nil { + return nil, fmt.Errorf("failed to create target handler service: %w", err) + } + services.RegisterTargetServiceServer(c.gatewayServer, ts) + if err := services.RegisterTargetServiceHandlerFromEndpoint(ctx, c.gatewayMux, gatewayTarget, dialOptions); err != nil { + return nil, fmt.Errorf("failed to register target service handler: %w", err) + } } - if err := services.RegisterAuthMethodServiceHandlerServer(ctx, mux, authMethods); err != nil { - return nil, fmt.Errorf("failed to register auth method service handler: %w", err) + if _, ok := currentServices[services.GroupService_ServiceDesc.ServiceName]; !ok { + gs, err := groups.NewService(c.IamRepoFn) + if err != nil { + return nil, fmt.Errorf("failed to create group handler service: %w", err) + } + services.RegisterGroupServiceServer(c.gatewayServer, gs) + if err := services.RegisterGroupServiceHandlerFromEndpoint(ctx, c.gatewayMux, gatewayTarget, dialOptions); err != nil { + return nil, fmt.Errorf("failed to register group service handler: %w", err) + } } - authtoks, err := authtokens.NewService(c.AuthTokenRepoFn, c.IamRepoFn) - if err != nil { - return nil, fmt.Errorf("failed to create auth token handler service: %w", err) + if _, ok := currentServices[services.RoleService_ServiceDesc.ServiceName]; !ok { + rs, err := roles.NewService(c.IamRepoFn) + if err != nil { + return nil, fmt.Errorf("failed to create role handler service: %w", err) + } + services.RegisterRoleServiceServer(c.gatewayServer, rs) + if err := services.RegisterRoleServiceHandlerFromEndpoint(ctx, c.gatewayMux, gatewayTarget, dialOptions); err != nil { + return nil, fmt.Errorf("failed to register role service handler: %w", err) + } } - if err := services.RegisterAuthTokenServiceHandlerServer(ctx, mux, authtoks); err != nil { - return nil, fmt.Errorf("failed to register auth token service handler: %w", err) + if _, ok := currentServices[services.SessionService_ServiceDesc.ServiceName]; !ok { + ss, err := sessions.NewService(c.SessionRepoFn, c.IamRepoFn) + if err != nil { + return nil, fmt.Errorf("failed to create session handler service: %w", err) + } + services.RegisterSessionServiceServer(c.gatewayServer, ss) + if err := services.RegisterSessionServiceHandlerFromEndpoint(ctx, c.gatewayMux, gatewayTarget, dialOptions); err != nil { + return nil, fmt.Errorf("failed to register session service handler: %w", err) + } } - os, err := scopes.NewService(c.IamRepoFn) - if err != nil { - return nil, fmt.Errorf("failed to create scope handler service: %w", err) + if _, ok := currentServices[services.ManagedGroupService_ServiceDesc.ServiceName]; !ok { + mgs, err := managed_groups.NewService(c.OidcRepoFn) + if err != nil { + return nil, fmt.Errorf("failed to create managed groups handler service: %w", err) + } + services.RegisterManagedGroupServiceServer(c.gatewayServer, mgs) + if err := services.RegisterManagedGroupServiceHandlerFromEndpoint(ctx, c.gatewayMux, gatewayTarget, dialOptions); err != nil { + return nil, fmt.Errorf("failed to register managed groups service handler: %w", err) + } } - if err := services.RegisterScopeServiceHandlerServer(ctx, mux, os); err != nil { - return nil, fmt.Errorf("failed to register scope service handler: %w", err) + if _, ok := currentServices[services.CredentialStoreService_ServiceDesc.ServiceName]; !ok { + cs, err := credentialstores.NewService(c.VaultCredentialRepoFn, c.IamRepoFn) + if err != nil { + return nil, fmt.Errorf("failed to create credential store handler service: %w", err) + } + services.RegisterCredentialStoreServiceServer(c.gatewayServer, cs) + if err := services.RegisterCredentialStoreServiceHandlerFromEndpoint(ctx, c.gatewayMux, gatewayTarget, dialOptions); err != nil { + return nil, fmt.Errorf("failed to register credential store service handler: %w", err) + } } - us, err := users.NewService(c.IamRepoFn) - if err != nil { - return nil, fmt.Errorf("failed to create user handler service: %w", err) - } - if err := services.RegisterUserServiceHandlerServer(ctx, mux, us); err != nil { - return nil, fmt.Errorf("failed to register user service handler: %w", err) - } - ts, err := targets.NewService( - c.kms, - c.TargetRepoFn, - c.IamRepoFn, - c.ServersRepoFn, - c.SessionRepoFn, - c.StaticHostRepoFn, - c.VaultCredentialRepoFn) - if err != nil { - return nil, fmt.Errorf("failed to create target handler service: %w", err) - } - if err := services.RegisterTargetServiceHandlerServer(ctx, mux, ts); err != nil { - return nil, fmt.Errorf("failed to register target service handler: %w", err) - } - gs, err := groups.NewService(c.IamRepoFn) - if err != nil { - return nil, fmt.Errorf("failed to create group handler service: %w", err) - } - if err := services.RegisterGroupServiceHandlerServer(ctx, mux, gs); err != nil { - return nil, fmt.Errorf("failed to register group service handler: %w", err) - } - rs, err := roles.NewService(c.IamRepoFn) - if err != nil { - return nil, fmt.Errorf("failed to create role handler service: %w", err) - } - if err := services.RegisterRoleServiceHandlerServer(ctx, mux, rs); err != nil { - return nil, fmt.Errorf("failed to register role service handler: %w", err) - } - ss, err := sessions.NewService(c.SessionRepoFn, c.IamRepoFn) - if err != nil { - return nil, fmt.Errorf("failed to create session handler service: %w", err) - } - if err := services.RegisterSessionServiceHandlerServer(ctx, mux, ss); err != nil { - return nil, fmt.Errorf("failed to register session service handler: %w", err) - } - mgs, err := managed_groups.NewService(c.OidcRepoFn) - if err != nil { - return nil, fmt.Errorf("failed to create managed groups handler service: %w", err) - } - if err := services.RegisterManagedGroupServiceHandlerServer(ctx, mux, mgs); err != nil { - return nil, fmt.Errorf("failed to register managed groups service handler: %w", err) - } - cs, err := credentialstores.NewService(c.VaultCredentialRepoFn, c.IamRepoFn) - if err != nil { - return nil, fmt.Errorf("failed to create credential store handler service: %w", err) - } - if err := services.RegisterCredentialStoreServiceHandlerServer(ctx, mux, cs); err != nil { - return nil, fmt.Errorf("failed to register credential store service handler: %w", err) - } - cl, err := credentiallibraries.NewService(c.VaultCredentialRepoFn, c.IamRepoFn) - if err != nil { - return nil, fmt.Errorf("failed to create credential library handler service: %w", err) - } - if err := services.RegisterCredentialLibraryServiceHandlerServer(ctx, mux, cl); err != nil { - return nil, fmt.Errorf("failed to register credential library service handler: %w", err) + if _, ok := currentServices[services.CredentialLibraryService_ServiceDesc.ServiceName]; !ok { + cl, err := credentiallibraries.NewService(c.VaultCredentialRepoFn, c.IamRepoFn) + if err != nil { + return nil, fmt.Errorf("failed to create credential library handler service: %w", err) + } + services.RegisterCredentialLibraryServiceServer(c.gatewayServer, cl) + if err := services.RegisterCredentialLibraryServiceHandlerFromEndpoint(ctx, c.gatewayMux, gatewayTarget, dialOptions); err != nil { + return nil, fmt.Errorf("failed to register credential library service handler: %w", err) + } } - return mux, nil + return c.gatewayMux, nil } func wrapHandlerWithCommonFuncs(h http.Handler, c *Controller, props HandlerProperties) http.Handler { @@ -236,29 +279,27 @@ func wrapHandlerWithCommonFuncs(h http.Handler, c *Controller, props HandlerProp } // Add values for authn/authz checking - requestInfo := auth.RequestInfo{ + requestInfo := authpb.RequestInfo{ Path: r.URL.Path, Method: r.Method, DisableAuthzFailures: disableAuthzFailures, } requestInfo.PublicId, requestInfo.EncryptedToken, requestInfo.TokenFormat = auth.GetTokenFromRequest(ctx, c.kms, r) - ctx = auth.NewVerifierContext(ctx, c.IamRepoFn, c.AuthTokenRepoFn, c.ServersRepoFn, c.kms, requestInfo) - // Add general request information to the context. The information from - // the auth verifier context is pretty specifically curated to - // authentication/authorization verification so this is more - // general-purpose. - // - // We could use requests.NewRequestContext but this saves an immediate - // lookup. - ctx = context.WithValue(ctx, requests.ContextRequestInformationKey, &requests.RequestContext{ - Path: r.URL.Path, - Method: r.Method, - }) - - // Set the context back on the request - r = r.WithContext(ctx) + // Serialize the request info to send it across the wire to the + // grpc-gateway via an http header + requestInfo.Ticket = c.gatewayTicket // allows the grpc-gateway to verify the request info came from it's in-memory companion http proxy + marshalledRequestInfo, err := proto.Marshal(&requestInfo) + if err != nil { + event.WriteError(ctx, op, err, event.WithInfoMsg("error marshaling request info")) + w.WriteHeader(http.StatusInternalServerError) + return + } + // Use the default grpc-gateway mapping rule to pass the request info as + // metadata. + // See: https://pkg.go.dev/github.com/grpc-ecosystem/grpc-gateway/runtime#DefaultHeaderMatcher + r.Header.Set("Grpc-Metadata-"+requestInfoMdKey, base58.FastBase58Encoding(marshalledRequestInfo)) h.ServeHTTP(w, r) }) diff --git a/internal/servers/controller/handlers/authmethods/authmethod_service.go b/internal/servers/controller/handlers/authmethods/authmethod_service.go index 4fb5f0aa2..4bf43902a 100644 --- a/internal/servers/controller/handlers/authmethods/authmethod_service.go +++ b/internal/servers/controller/handlers/authmethods/authmethod_service.go @@ -389,7 +389,7 @@ func (s Service) DeleteAuthMethod(ctx context.Context, req *pbs.DeleteAuthMethod if err != nil { return nil, err } - return nil, nil + return &pbs.DeleteAuthMethodResponse{}, nil } // Authenticate implements the interface pbs.AuthenticationServiceServer. diff --git a/internal/servers/controller/handlers/authmethods/authmethod_service_test.go b/internal/servers/controller/handlers/authmethods/authmethod_service_test.go index f1d472a11..8dbd20d93 100644 --- a/internal/servers/controller/handlers/authmethods/authmethod_service_test.go +++ b/internal/servers/controller/handlers/authmethods/authmethod_service_test.go @@ -428,12 +428,14 @@ func TestDelete(t *testing.T) { req: &pbs.DeleteAuthMethodRequest{ Id: pwam.GetPublicId(), }, + res: &pbs.DeleteAuthMethodResponse{}, }, { name: "Delete an Existing OIDC AuthMethod", req: &pbs.DeleteAuthMethodRequest{ Id: oidcam.GetPublicId(), }, + res: &pbs.DeleteAuthMethodResponse{}, }, { name: "Delete bad auth_method id", diff --git a/internal/servers/controller/handlers/authmethods/oidc_test.go b/internal/servers/controller/handlers/authmethods/oidc_test.go index d843c4603..ddc308af8 100644 --- a/internal/servers/controller/handlers/authmethods/oidc_test.go +++ b/internal/servers/controller/handlers/authmethods/oidc_test.go @@ -13,6 +13,8 @@ import ( "time" "github.com/google/go-cmp/cmp" + authpb "github.com/hashicorp/boundary/internal/gen/controller/auth" + "github.com/hashicorp/boundary/internal/auth/oidc" "github.com/hashicorp/boundary/internal/auth/password" "github.com/hashicorp/boundary/internal/authtoken" @@ -206,9 +208,9 @@ func TestList_FilterNonPublic(t *testing.T) { atRepoFn, serversRepoFn, kmsCache, - auth.RequestInfo{ + &authpb.RequestInfo{ Token: at.GetToken(), - TokenFormat: auth.AuthTokenTypeBearer, + TokenFormat: uint32(auth.AuthTokenTypeBearer), PublicId: at.GetPublicId(), }) }(), diff --git a/internal/servers/controller/handlers/authtokens/authtoken_service_test.go b/internal/servers/controller/handlers/authtokens/authtoken_service_test.go index 784ac8a3e..cff486682 100644 --- a/internal/servers/controller/handlers/authtokens/authtoken_service_test.go +++ b/internal/servers/controller/handlers/authtokens/authtoken_service_test.go @@ -11,6 +11,7 @@ import ( "github.com/hashicorp/boundary/internal/authtoken" "github.com/hashicorp/boundary/internal/db" pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services" + 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/requests" @@ -87,15 +88,15 @@ func TestGetSelf(t *testing.T) { require, assert := require.New(t), assert.New(t) // Setup the auth request information req := httptest.NewRequest("GET", fmt.Sprintf("http://127.0.0.1/v1/auth-tokens/%s", tc.readId), nil) - requestInfo := auth.RequestInfo{ + requestInfo := authpb.RequestInfo{ Path: req.URL.Path, Method: req.Method, - TokenFormat: auth.AuthTokenTypeBearer, + TokenFormat: uint32(auth.AuthTokenTypeBearer), PublicId: tc.token.GetPublicId(), Token: tc.token.GetToken(), } - ctx := auth.NewVerifierContext(context.Background(), iamRepoFn, tokenRepoFn, serversRepoFn, kms, requestInfo) + ctx := auth.NewVerifierContext(context.Background(), iamRepoFn, tokenRepoFn, serversRepoFn, kms, &requestInfo) ctx = context.WithValue(ctx, requests.ContextRequestInformationKey, &requests.RequestContext{}) got, err := a.GetAuthToken(ctx, &pbs.GetAuthTokenRequest{Id: tc.readId}) if tc.err != nil { @@ -236,15 +237,15 @@ func TestList_Self(t *testing.T) { require, assert := require.New(t), assert.New(t) // Setup the auth request information req := httptest.NewRequest("GET", fmt.Sprintf("http://127.0.0.1/v1/auth-tokens?scope_id=%s", o.GetPublicId()), nil) - requestInfo := auth.RequestInfo{ + requestInfo := authpb.RequestInfo{ Path: req.URL.Path, Method: req.Method, - TokenFormat: auth.AuthTokenTypeBearer, + TokenFormat: uint32(auth.AuthTokenTypeBearer), PublicId: tc.requester.GetPublicId(), Token: tc.requester.GetToken(), } - ctx := auth.NewVerifierContext(context.Background(), iamRepoFn, tokenRepoFn, serversRepoFn, kms, requestInfo) + ctx := auth.NewVerifierContext(context.Background(), iamRepoFn, tokenRepoFn, serversRepoFn, kms, &requestInfo) got, err := a.ListAuthTokens(ctx, &pbs.ListAuthTokensRequest{ScopeId: o.GetPublicId()}) require.NoError(err) require.Len(got.Items, 1) @@ -482,15 +483,15 @@ func TestDeleteSelf(t *testing.T) { require := require.New(t) // Setup the auth request information req := httptest.NewRequest("DELETE", fmt.Sprintf("http://127.0.0.1/v1/auth-tokens/%s", tc.deleteId), nil) - requestInfo := auth.RequestInfo{ + requestInfo := authpb.RequestInfo{ Path: req.URL.Path, Method: req.Method, - TokenFormat: auth.AuthTokenTypeBearer, + TokenFormat: uint32(auth.AuthTokenTypeBearer), PublicId: tc.token.GetPublicId(), Token: tc.token.GetToken(), } - ctx := auth.NewVerifierContext(context.Background(), iamRepoFn, tokenRepoFn, serversRepoFn, kms, requestInfo) + ctx := auth.NewVerifierContext(context.Background(), iamRepoFn, tokenRepoFn, serversRepoFn, kms, &requestInfo) got, err := a.DeleteAuthToken(ctx, &pbs.DeleteAuthTokenRequest{Id: tc.deleteId}) if tc.err != nil { require.EqualError(err, tc.err.Error()) diff --git a/internal/servers/controller/handlers/credentiallibraries/credentiallibrary_service_test.go b/internal/servers/controller/handlers/credentiallibraries/credentiallibrary_service_test.go index d9dd0118b..681294a1a 100644 --- a/internal/servers/controller/handlers/credentiallibraries/credentiallibrary_service_test.go +++ b/internal/servers/controller/handlers/credentiallibraries/credentiallibrary_service_test.go @@ -555,6 +555,7 @@ func TestDelete(t *testing.T) { name string id string err error + res *pbs.DeleteCredentialLibraryResponse }{ { name: "success", @@ -574,7 +575,7 @@ func TestDelete(t *testing.T) { for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { got, gErr := s.DeleteCredentialLibrary(auth.DisabledAuthTestContext(iamRepoFn, prj.GetPublicId()), &pbs.DeleteCredentialLibraryRequest{Id: tc.id}) - assert.Nil(t, got) + assert.EqualValuesf(t, tc.res, got, "DeleteCredentialLibrary got response %q, wanted %q", got, tc.res) if tc.err != nil { require.Error(t, gErr) assert.True(t, errors.Is(gErr, tc.err)) diff --git a/internal/servers/controller/handlers/credentialstores/credentialstore_service_test.go b/internal/servers/controller/handlers/credentialstores/credentialstore_service_test.go index 9e087bd46..bfa43c54b 100644 --- a/internal/servers/controller/handlers/credentialstores/credentialstore_service_test.go +++ b/internal/servers/controller/handlers/credentialstores/credentialstore_service_test.go @@ -674,6 +674,7 @@ func TestDelete(t *testing.T) { name string id string err error + res *pbs.DeleteCredentialStoreResponse }{ { name: "success", @@ -693,7 +694,7 @@ func TestDelete(t *testing.T) { for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { got, gErr := s.DeleteCredentialStore(auth.DisabledAuthTestContext(iamRepoFn, prj.GetPublicId()), &pbs.DeleteCredentialStoreRequest{Id: tc.id}) - assert.Nil(t, got) + assert.EqualValuesf(t, tc.res, got, "DeleteCredentialStore got response %q, wanted %q", got, tc.res) if tc.err != nil { require.Error(t, gErr) assert.True(t, errors.Is(gErr, tc.err)) diff --git a/internal/servers/controller/handlers/errors.go b/internal/servers/controller/handlers/errors.go index 1628de364..c23e59164 100644 --- a/internal/servers/controller/handlers/errors.go +++ b/internal/servers/controller/handlers/errors.go @@ -5,31 +5,46 @@ import ( "fmt" "io" "net/http" + "regexp" "sort" + "strconv" "strings" "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" "github.com/hashicorp/boundary/internal/errors" pb "github.com/hashicorp/boundary/internal/gen/controller/api" + pberrors "github.com/hashicorp/boundary/internal/gen/errors" "github.com/hashicorp/boundary/internal/observability/event" + "github.com/mr-tron/base58" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" + "google.golang.org/protobuf/proto" ) const ( genericUniquenessMsg = "Invalid request. Request attempted to make second resource with the same field value that must be unique." genericNotFoundMsg = "Unable to find requested resource." + + // domainErrHeader defines an http header for encoded domain errors from the + // grpc server. + domainErrHeader = "x-domain-err" + // domainErrMetadataHeader defines an http header for encoded domain errors from the + // grpc server via metadata + domainErrMetadataHeader = "Grpc-Metadata-X-Domain-Err" + + apiErrHeader = "x-api-err" + apiErrMetadataHeader = "Grpc-Metadata-X-Api-Err" ) -type apiError struct { - status int32 - inner *pb.Error +type ApiError struct { + Status int32 + Inner *pb.Error } -func (e *apiError) Error() string { - res := fmt.Sprintf("Status: %d, Kind: %q, Error: %q", e.status, e.inner.GetKind(), e.inner.GetMessage()) +func (e *ApiError) Error() string { + res := fmt.Sprintf("Status: %d, Kind: %q, Error: %q", e.Status, e.Inner.GetKind(), e.Inner.GetMessage()) var dets []string - for _, rf := range e.inner.GetDetails().GetRequestFields() { + for _, rf := range e.Inner.GetDetails().GetRequestFields() { dets = append(dets, fmt.Sprintf("{name: %q, desc: %q}", rf.GetName(), rf.GetDescription())) } if len(dets) > 0 { @@ -39,19 +54,19 @@ func (e *apiError) Error() string { return res } -func (e *apiError) Is(target error) bool { - var tApiErr *apiError +func (e *ApiError) Is(target error) bool { + var tApiErr *ApiError if !errors.As(target, &tApiErr) { return false } - return tApiErr.inner.Kind == e.inner.Kind && tApiErr.status == e.status + return tApiErr.Inner.Kind == e.Inner.Kind && tApiErr.Status == e.Status } // ApiErrorWithCode returns an api error with the provided code. func ApiErrorWithCode(c codes.Code) error { - return &apiError{ - status: int32(runtime.HTTPStatusFromCode(c)), - inner: &pb.Error{ + return &ApiError{ + Status: int32(runtime.HTTPStatusFromCode(c)), + Inner: &pb.Error{ Kind: c.String(), }, } @@ -59,9 +74,9 @@ func ApiErrorWithCode(c codes.Code) error { // ApiErrorWithCodeAndMessage returns an api error with the provided code and message. func ApiErrorWithCodeAndMessage(c codes.Code, msg string, args ...interface{}) error { - return &apiError{ - status: int32(runtime.HTTPStatusFromCode(c)), - inner: &pb.Error{ + return &ApiError{ + Status: int32(runtime.HTTPStatusFromCode(c)), + Inner: &pb.Error{ Kind: c.String(), Message: fmt.Sprintf(msg, args...), }, @@ -70,9 +85,9 @@ func ApiErrorWithCodeAndMessage(c codes.Code, msg string, args ...interface{}) e // NotFoundError returns an ApiError indicating a resource couldn't be found. func NotFoundError() error { - return &apiError{ - status: http.StatusNotFound, - inner: &pb.Error{ + return &ApiError{ + Status: http.StatusNotFound, + Inner: &pb.Error{ Kind: codes.NotFound.String(), Message: "Resource not found.", }, @@ -80,19 +95,19 @@ func NotFoundError() error { } // NotFoundErrorf returns an ApiError indicating a resource couldn't be found. -func NotFoundErrorf(msg string, a ...interface{}) *apiError { - return &apiError{ - status: http.StatusNotFound, - inner: &pb.Error{ +func NotFoundErrorf(msg string, a ...interface{}) *ApiError { + return &ApiError{ + Status: http.StatusNotFound, + Inner: &pb.Error{ Kind: codes.NotFound.String(), Message: fmt.Sprintf(msg, a...), }, } } -var unauthorizedError = &apiError{ - status: http.StatusForbidden, - inner: &pb.Error{ +var unauthorizedError = &ApiError{ + Status: http.StatusForbidden, + Inner: &pb.Error{ Kind: codes.PermissionDenied.String(), Message: "Forbidden.", }, @@ -102,9 +117,9 @@ func ForbiddenError() error { return unauthorizedError } -var unauthenticatedError = &apiError{ - status: http.StatusUnauthorized, - inner: &pb.Error{ +var unauthenticatedError = &ApiError{ + Status: http.StatusUnauthorized, + Inner: &pb.Error{ Kind: codes.Unauthenticated.String(), Message: "Unauthenticated, or invalid token.", }, @@ -114,47 +129,49 @@ func UnauthenticatedError() error { return unauthenticatedError } -func InvalidArgumentErrorf(msg string, fields map[string]string) *apiError { +func InvalidArgumentErrorf(msg string, fields map[string]string) *ApiError { const op = "handlers.InvalidArgumentErrorf" ctx := context.TODO() err := ApiErrorWithCodeAndMessage(codes.InvalidArgument, msg) - var apiErr *apiError + var apiErr *ApiError if !errors.As(err, &apiErr) { event.WriteError(ctx, op, err, event.WithInfoMsg("Unable to build invalid argument api error.")) } if len(fields) > 0 { - apiErr.inner.Details = &pb.ErrorDetails{} + apiErr.Inner.Details = &pb.ErrorDetails{} } for k, v := range fields { - apiErr.inner.Details.RequestFields = append(apiErr.inner.Details.RequestFields, &pb.FieldError{Name: k, Description: v}) + apiErr.Inner.Details.RequestFields = append(apiErr.Inner.Details.RequestFields, &pb.FieldError{Name: k, Description: v}) } - sort.Slice(apiErr.inner.GetDetails().GetRequestFields(), func(i, j int) bool { - return apiErr.inner.Details.RequestFields[i].GetName() < apiErr.inner.Details.RequestFields[j].GetName() + sort.Slice(apiErr.Inner.GetDetails().GetRequestFields(), func(i, j int) bool { + return apiErr.Inner.Details.RequestFields[i].GetName() < apiErr.Inner.Details.RequestFields[j].GetName() }) return apiErr } +var statusRegEx = regexp.MustCompile("Status: ([0-9]+), Kind: \"(.*)\", Error: \"(.*)\"") + // Converts a known errors into an error that can presented to an end user over the API. -func backendErrorToApiError(inErr error) *apiError { +func backendErrorToApiError(inErr error) *ApiError { stErr := status.Convert(inErr) switch { case errors.Is(inErr, runtime.ErrNotMatch): // grpc gateway uses this error when the path was not matched, but the error uses codes.Unimplemented which doesn't match the intention. // Overwrite the error to match our expected behavior. - return &apiError{ - status: http.StatusNotFound, - inner: &pb.Error{ + return &ApiError{ + Status: http.StatusNotFound, + Inner: &pb.Error{ Kind: codes.NotFound.String(), Message: http.StatusText(http.StatusNotFound), }, } case status.Code(inErr) == codes.Unimplemented: // Instead of returning a 501 we always want to return a 405 when a method isn't implemented. - return &apiError{ - status: http.StatusMethodNotAllowed, - inner: &pb.Error{ + return &ApiError{ + Status: http.StatusMethodNotAllowed, + Inner: &pb.Error{ Kind: codes.Unimplemented.String(), Message: stErr.Message(), }, @@ -176,11 +193,23 @@ func backendErrorToApiError(inErr error) *apiError { statusCode = int32(domainErr.Code) } + // perhaps the error is from the grpc gateway, so match against the known + // apiError msg format. + if found := statusRegEx.FindStringSubmatch(stErr.Message()); len(found) == 4 { + u32, err := strconv.ParseInt(found[1], 10, 32) + if err == nil { // notice it's testing for NO err + return &ApiError{ + Status: int32(u32), + Inner: &pb.Error{Kind: found[2], Message: found[3]}, + } + } + } + // TODO: Don't return potentially sensitive information (like which user id an account // is already associated with when attempting to re-associate it). - return &apiError{ - status: statusCode, - inner: &pb.Error{Kind: codes.Internal.String(), Message: inErr.Error()}, + return &ApiError{ + Status: statusCode, + Inner: &pb.Error{Kind: codes.Internal.String(), Message: inErr.Error()}, } } @@ -189,7 +218,70 @@ func ErrorHandler() runtime.ErrorHandlerFunc { const errorFallback = `{"error": "failed to marshal error message"}` return func(ctx context.Context, _ *runtime.ServeMux, mar runtime.Marshaler, w http.ResponseWriter, r *http.Request, inErr error) { // API specified error, otherwise we need to translate repo/db errors. - var apiErr *apiError + + // the grpc server will encoded domain errors into the x-domain-err, so + // let's check there first. (see: controller.errorInterceptor) + md, ok := runtime.ServerMetadataFromContext(ctx) + if ok { + defer func() { + // make sure we don't leak the headers that were used as a comm + // channel between the grpc server and the http proxy + delete(md.HeaderMD, domainErrHeader) + delete(w.Header(), domainErrMetadataHeader) + + delete(md.HeaderMD, apiErrHeader) + delete(w.Header(), apiErrMetadataHeader) + }() + domainErrHdrs := md.HeaderMD.Get(domainErrHeader) + apiErrHdrs := md.HeaderMD.Get(apiErrHeader) + + switch { + case len(domainErrHdrs) > 0: + decoded, err := base58.FastBase58Decoding(domainErrHdrs[0]) + if err != nil { + event.WriteError(ctx, op, err, event.WithInfoMsg("failed to decode domain err header")) + w.WriteHeader(http.StatusInternalServerError) + if _, err := io.WriteString(w, errorFallback); err != nil { + event.WriteError(ctx, op, err, event.WithInfoMsg("failed to write response")) + } + return + } + var pbErr pberrors.Err + if err := proto.Unmarshal(decoded, &pbErr); err != nil { + event.WriteError(ctx, op, err, event.WithInfoMsg("failed to marshal domain err header")) + w.WriteHeader(http.StatusInternalServerError) + if _, err := io.WriteString(w, errorFallback); err != nil { + event.WriteError(ctx, op, err, event.WithInfoMsg("failed to write response")) + } + return + } + inErr = errors.FromPbErrors(&pbErr) + case len(apiErrHdrs) > 0: + decoded, err := base58.FastBase58Decoding(apiErrHdrs[0]) + if err != nil { + event.WriteError(ctx, op, err, event.WithInfoMsg("failed to decode api err header")) + w.WriteHeader(http.StatusInternalServerError) + if _, err := io.WriteString(w, errorFallback); err != nil { + event.WriteError(ctx, op, err, event.WithInfoMsg("failed to write response")) + } + return + } + var pbApiErr pberrors.ApiError + if err := proto.Unmarshal(decoded, &pbApiErr); err != nil { + event.WriteError(ctx, op, err, event.WithInfoMsg("failed to marshal api err header")) + w.WriteHeader(http.StatusInternalServerError) + if _, err := io.WriteString(w, errorFallback); err != nil { + event.WriteError(ctx, op, err, event.WithInfoMsg("failed to write response")) + } + return + } + inErr = &ApiError{ + Status: pbApiErr.Status, + Inner: pbApiErr.ApiError, + } + } + } + var apiErr *ApiError isApiErr := errors.As(inErr, &apiErr) if !isApiErr { if err := backendErrorToApiError(inErr); err != nil && !errors.As(err, &apiErr) { @@ -197,13 +289,13 @@ func ErrorHandler() runtime.ErrorHandlerFunc { } } - if apiErr.status == http.StatusInternalServerError { + if apiErr.Status == http.StatusInternalServerError { event.WriteError(ctx, op, inErr, event.WithInfoMsg("internal error returned")) } - buf, merr := mar.Marshal(apiErr.inner) + buf, merr := mar.Marshal(apiErr.Inner) if merr != nil { - event.WriteError(ctx, op, merr, event.WithInfoMsg("failed to marshal error response", "response", fmt.Sprintf("%#v", apiErr.inner))) + event.WriteError(ctx, op, merr, event.WithInfoMsg("failed to marshal error response", "response", fmt.Sprintf("%#v", apiErr.Inner))) w.WriteHeader(http.StatusInternalServerError) if _, err := io.WriteString(w, errorFallback); err != nil { event.WriteError(ctx, op, err, event.WithInfoMsg("failed to write response")) @@ -211,8 +303,8 @@ func ErrorHandler() runtime.ErrorHandlerFunc { return } - w.Header().Set("Content-Type", mar.ContentType(apiErr.inner)) - w.WriteHeader(int(apiErr.status)) + w.Header().Set("Content-Type", mar.ContentType(apiErr.Inner)) + w.WriteHeader(int(apiErr.Status)) if _, err := w.Write(buf); err != nil { event.WriteError(ctx, op, err, event.WithInfoMsg("failed to send response chunk")) return @@ -221,5 +313,5 @@ func ErrorHandler() runtime.ErrorHandlerFunc { } func ToApiError(e error) *pb.Error { - return backendErrorToApiError(e).inner + return backendErrorToApiError(e).Inner } diff --git a/internal/servers/controller/handlers/errors_test.go b/internal/servers/controller/handlers/errors_test.go index f4b2f34fc..f76a1a51b 100644 --- a/internal/servers/controller/handlers/errors_test.go +++ b/internal/servers/controller/handlers/errors_test.go @@ -32,14 +32,14 @@ func TestApiErrorHandler(t *testing.T) { testCases := []struct { name string err error - expected apiError + expected ApiError }{ { name: "Not Found", err: NotFoundErrorf("Test"), - expected: apiError{ - status: http.StatusNotFound, - inner: &pb.Error{ + expected: ApiError{ + Status: http.StatusNotFound, + Inner: &pb.Error{ Kind: "NotFound", Message: "Test", }, @@ -51,9 +51,9 @@ func TestApiErrorHandler(t *testing.T) { "k1": "v1", "k2": "v2", }), - expected: apiError{ - status: http.StatusBadRequest, - inner: &pb.Error{ + expected: ApiError{ + Status: http.StatusBadRequest, + Inner: &pb.Error{ Kind: "InvalidArgument", Message: "Test", Details: &pb.ErrorDetails{ @@ -74,9 +74,9 @@ func TestApiErrorHandler(t *testing.T) { { name: "GrpcGateway Routing Error", err: runtime.ErrNotMatch, - expected: apiError{ - status: http.StatusNotFound, - inner: &pb.Error{ + expected: ApiError{ + Status: http.StatusNotFound, + Inner: &pb.Error{ Kind: "NotFound", Message: http.StatusText(http.StatusNotFound), }, @@ -85,9 +85,9 @@ func TestApiErrorHandler(t *testing.T) { { name: "Unimplemented error", err: status.Error(codes.Unimplemented, "Test"), - expected: apiError{ - status: http.StatusMethodNotAllowed, - inner: &pb.Error{ + expected: ApiError{ + Status: http.StatusMethodNotAllowed, + Inner: &pb.Error{ Kind: "Unimplemented", Message: "Test", }, @@ -96,9 +96,9 @@ func TestApiErrorHandler(t *testing.T) { { name: "Unknown error", err: stderrors.New("Some random error"), - expected: apiError{ - status: http.StatusInternalServerError, - inner: &pb.Error{ + expected: ApiError{ + Status: http.StatusInternalServerError, + Inner: &pb.Error{ Kind: "Internal", Message: "Some random error", }, @@ -107,9 +107,9 @@ func TestApiErrorHandler(t *testing.T) { { name: "Domain error Db invalid public id", err: errors.E(ctx, errors.WithCode(errors.InvalidPublicId)), - expected: apiError{ - status: http.StatusInternalServerError, - inner: &pb.Error{ + expected: ApiError{ + Status: http.StatusInternalServerError, + Inner: &pb.Error{ Kind: "Internal", Message: "invalid public id, parameter violation: error #102", }, @@ -118,9 +118,9 @@ func TestApiErrorHandler(t *testing.T) { { name: "Domain error Db invalid parameter", err: errors.E(ctx, errors.WithCode(errors.InvalidParameter)), - expected: apiError{ - status: http.StatusInternalServerError, - inner: &pb.Error{ + expected: ApiError{ + Status: http.StatusInternalServerError, + Inner: &pb.Error{ Kind: "Internal", Message: "invalid parameter, parameter violation: error #100", }, @@ -129,9 +129,9 @@ func TestApiErrorHandler(t *testing.T) { { name: "Domain error Db invalid field mask", err: errors.E(ctx, errors.WithCode(errors.InvalidFieldMask)), - expected: apiError{ - status: http.StatusBadRequest, - inner: &pb.Error{ + expected: ApiError{ + Status: http.StatusBadRequest, + Inner: &pb.Error{ Kind: "InvalidArgument", Message: "Error in provided request", Details: &pb.ErrorDetails{RequestFields: []*pb.FieldError{{Name: "update_mask", Description: "Invalid update mask provided."}}}, @@ -141,9 +141,9 @@ func TestApiErrorHandler(t *testing.T) { { name: "Domain error Db empty field mask", err: errors.E(ctx, errors.WithCode(errors.EmptyFieldMask)), - expected: apiError{ - status: http.StatusBadRequest, - inner: &pb.Error{ + expected: ApiError{ + Status: http.StatusBadRequest, + Inner: &pb.Error{ Kind: "InvalidArgument", Message: "Error in provided request", Details: &pb.ErrorDetails{RequestFields: []*pb.FieldError{{Name: "update_mask", Description: "Invalid update mask provided."}}}, @@ -153,9 +153,9 @@ func TestApiErrorHandler(t *testing.T) { { name: "Domain error Db not unqiue", err: errors.E(ctx, errors.WithCode(errors.NotUnique)), - expected: apiError{ - status: http.StatusBadRequest, - inner: &pb.Error{ + expected: ApiError{ + Status: http.StatusBadRequest, + Inner: &pb.Error{ Kind: "InvalidArgument", Message: genericUniquenessMsg, }, @@ -164,9 +164,9 @@ func TestApiErrorHandler(t *testing.T) { { name: "Domain error Db record not found", err: errors.E(ctx, errors.WithCode(errors.RecordNotFound)), - expected: apiError{ - status: http.StatusNotFound, - inner: &pb.Error{ + expected: ApiError{ + Status: http.StatusNotFound, + Inner: &pb.Error{ Kind: "NotFound", Message: genericNotFoundMsg, }, @@ -175,9 +175,9 @@ func TestApiErrorHandler(t *testing.T) { { name: "Domain error Db multiple records", err: errors.E(ctx, errors.WithCode(errors.MultipleRecords)), - expected: apiError{ - status: http.StatusInternalServerError, - inner: &pb.Error{ + expected: ApiError{ + Status: http.StatusInternalServerError, + Inner: &pb.Error{ Kind: "Internal", Message: "multiple records, search issue: error #1101", }, @@ -186,9 +186,9 @@ func TestApiErrorHandler(t *testing.T) { { name: "Domain error account already associated", err: errors.E(ctx, errors.WithCode(errors.AccountAlreadyAssociated)), - expected: apiError{ - status: http.StatusBadRequest, - inner: &pb.Error{ + expected: ApiError{ + Status: http.StatusBadRequest, + Inner: &pb.Error{ Kind: "InvalidArgument", Message: "account already associated with another user, parameter violation: error #114", }, @@ -197,9 +197,9 @@ func TestApiErrorHandler(t *testing.T) { { name: "Wrapped domain error", err: errors.E(ctx, errors.WithCode(errors.InvalidAddress), errors.WithMsg("test msg"), errors.WithWrap(errors.E(ctx, errors.WithCode(errors.NotNull), errors.WithMsg("inner msg")))), - expected: apiError{ - status: http.StatusInternalServerError, - inner: &pb.Error{ + expected: ApiError{ + Status: http.StatusInternalServerError, + Inner: &pb.Error{ Kind: "Internal", Message: "test msg: parameter violation: error #101: inner msg: integrity violation: error #1001", }, @@ -208,9 +208,9 @@ func TestApiErrorHandler(t *testing.T) { { name: "Forbidden domain error", err: errors.E(ctx, errors.WithCode(errors.Forbidden), errors.WithMsg("test msg")), - expected: apiError{ - status: http.StatusForbidden, - inner: &pb.Error{ + expected: ApiError{ + Status: http.StatusForbidden, + Inner: &pb.Error{ Kind: "Internal", Message: "test msg: unknown: error #403", }, @@ -219,9 +219,9 @@ func TestApiErrorHandler(t *testing.T) { { name: "Wrapped forbidden domain error", err: fmt.Errorf("got error: %w", errors.E(ctx, errors.WithCode(errors.Forbidden), errors.WithMsg("test msg"))), - expected: apiError{ - status: http.StatusForbidden, - inner: &pb.Error{ + expected: ApiError{ + Status: http.StatusForbidden, + Inner: &pb.Error{ Kind: "Internal", Message: "got error: test msg: unknown: error #403", }, @@ -234,7 +234,7 @@ func TestApiErrorHandler(t *testing.T) { w := httptest.NewRecorder() tested(ctx, mux, outMarsh, w, req, tc.err) resp := w.Result() - assert.EqualValues(tc.expected.status, resp.StatusCode) + assert.EqualValues(tc.expected.Status, resp.StatusCode) got, err := ioutil.ReadAll(resp.Body) require.NoError(err) @@ -243,8 +243,8 @@ func TestApiErrorHandler(t *testing.T) { err = inMarsh.Unmarshal(got, gotErr) require.NoError(err) - assert.Equal(tc.expected.status, int32(resp.StatusCode)) - assert.Empty(cmp.Diff(tc.expected.inner, gotErr, protocmp.Transform())) + assert.Equal(tc.expected.Status, int32(resp.StatusCode)) + assert.Empty(cmp.Diff(tc.expected.Inner, gotErr, protocmp.Transform())) }) } } diff --git a/internal/servers/controller/handlers/managed_groups/managed_group_service_test.go b/internal/servers/controller/handlers/managed_groups/managed_group_service_test.go index 7c468be33..1ca2cda25 100644 --- a/internal/servers/controller/handlers/managed_groups/managed_group_service_test.go +++ b/internal/servers/controller/handlers/managed_groups/managed_group_service_test.go @@ -417,6 +417,7 @@ func TestDelete_twice(t *testing.T) { o, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrap)) databaseWrapper, err := kmsCache.GetWrapper(context.Background(), o.PublicId, kms.KeyPurposeDatabase) + require.NoError(err) oidcAm := oidc.TestAuthMethod( t, conn, databaseWrapper, o.PublicId, oidc.ActivePrivateState, diff --git a/internal/servers/controller/handlers/outgoing_interceptor_test.go b/internal/servers/controller/handlers/outgoing_interceptor_test.go deleted file mode 100644 index 798368596..000000000 --- a/internal/servers/controller/handlers/outgoing_interceptor_test.go +++ /dev/null @@ -1,26 +0,0 @@ -package handlers - -import ( - "context" - "net/http" - "net/http/httptest" - "testing" - - pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services" - pb "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/authtokens" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "google.golang.org/protobuf/types/known/structpb" -) - -func TestOutgoingSplitCookie(t *testing.T) { - rec := httptest.NewRecorder() - attrs, err := ProtoToStruct(&pb.AuthToken{Token: "t_abc_1234567890"}) - require.NoError(t, err) - attrs.GetFields()["token_type"] = structpb.NewStringValue("cookie") - require.NoError(t, OutgoingInterceptor(context.Background(), rec, &pbs.AuthenticateResponse{Attributes: attrs})) - assert.ElementsMatch(t, rec.Result().Cookies(), []*http.Cookie{ - {Name: HttpOnlyCookieName, Value: "34567890", HttpOnly: true, Raw: "wt-http-token-cookie=34567890; HttpOnly"}, - {Name: JsVisibleCookieName, Value: "t_abc_12", Raw: "wt-js-token-cookie=t_abc_12"}, - }) -} diff --git a/internal/servers/controller/handlers/outgoing_interceptor.go b/internal/servers/controller/handlers/outgoing_response_filter.go similarity index 55% rename from internal/servers/controller/handlers/outgoing_interceptor.go rename to internal/servers/controller/handlers/outgoing_response_filter.go index 2814c2b10..cf6242002 100644 --- a/internal/servers/controller/handlers/outgoing_interceptor.go +++ b/internal/servers/controller/handlers/outgoing_response_filter.go @@ -4,30 +4,66 @@ import ( "context" "fmt" "net/http" + "strconv" "strings" + "github.com/hashicorp/boundary/internal/errors" + + "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services" "github.com/hashicorp/boundary/internal/observability/event" authtokenpb "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/authtokens" + "google.golang.org/grpc" + "google.golang.org/grpc/metadata" "google.golang.org/protobuf/proto" ) const ( - HttpOnlyCookieName = "wt-http-token-cookie" - JsVisibleCookieName = "wt-js-token-cookie" - tokenTypeField = "token_type" - finalRedirectUrlField = "final_redirect_url" - statusField = "status" + HttpOnlyCookieName = "wt-http-token-cookie" + JsVisibleCookieName = "wt-js-token-cookie" + tokenTypeField = "token_type" + finalRedirectUrlField = "final_redirect_url" + statusField = "status" + StatusCodeHeader = "x-http-code" + statusCodeMetadataHeader = "Grpc-Metadata-X-Http-Code" ) -func OutgoingInterceptor(ctx context.Context, w http.ResponseWriter, m proto.Message) error { - const op = "handlers.OutgoingInterceptor" - m = m.ProtoReflect().Interface() - if !m.ProtoReflect().IsValid() { - // This would be the case if it's a nil pointer - w.WriteHeader(http.StatusNoContent) - return nil +// SetStatusCode allows a grpc service handler to set the outgoing http status +// code +func SetStatusCode(ctx context.Context, code int) error { + const op = "handlers.SetHttpStatusCode" + if http.StatusText(code) == "" { + return errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("unknown status code: %d", code)) + } + if err := grpc.SetHeader(ctx, metadata.Pairs(StatusCodeHeader, strconv.Itoa(code))); err != nil { + return errors.Wrap(ctx, err, op, errors.WithCode(errors.Internal)) + } + return nil +} + +// OutgoingResponseFilter is a gRPC gateway WithForwardResponseOption. It's +// basically a filter that can manipulate the http response and has acesss to +// the outgoing proto msg +func OutgoingResponseFilter(ctx context.Context, w http.ResponseWriter, m proto.Message) error { + const op = "handlers.OutgoingResponseFilter" + + if md, ok := runtime.ServerMetadataFromContext(ctx); ok { + + // set http status codes based on metadata set by the grpc service + if statusCodes := md.HeaderMD.Get(StatusCodeHeader); len(statusCodes) > 0 { + defer func() { + // delete the headers to not expose any grpc-metadata in http response + delete(md.HeaderMD, StatusCodeHeader) + delete(w.Header(), statusCodeMetadataHeader) + }() + lastStatus, err := strconv.Atoi(statusCodes[len(statusCodes)-1]) + if err != nil { + return fmt.Errorf("%s: unable to convert status code %s: %w", op, statusCodes[len(statusCodes)-1], err) + } + w.WriteHeader(lastStatus) + return nil + } } // add the outgoing resp proto. See: common.flushGatedEvents is where the diff --git a/internal/servers/controller/handlers/outgoing_response_filter_test.go b/internal/servers/controller/handlers/outgoing_response_filter_test.go new file mode 100644 index 000000000..2d0932f56 --- /dev/null +++ b/internal/servers/controller/handlers/outgoing_response_filter_test.go @@ -0,0 +1,72 @@ +package handlers + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" + + emptypb "github.com/hashicorp/boundary/internal/gen/controller/api" + + "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" + pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services" + pb "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/authtokens" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/grpc/metadata" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/structpb" +) + +func TestOutgoingSplitCookie(t *testing.T) { + rec := httptest.NewRecorder() + attrs, err := ProtoToStruct(&pb.AuthToken{Token: "t_abc_1234567890"}) + require.NoError(t, err) + attrs.GetFields()["token_type"] = structpb.NewStringValue("cookie") + require.NoError(t, OutgoingResponseFilter(context.Background(), rec, &pbs.AuthenticateResponse{Attributes: attrs})) + assert.ElementsMatch(t, rec.Result().Cookies(), []*http.Cookie{ + {Name: HttpOnlyCookieName, Value: "34567890", HttpOnly: true, Raw: "wt-http-token-cookie=34567890; HttpOnly"}, + {Name: JsVisibleCookieName, Value: "t_abc_12", Raw: "wt-js-token-cookie=t_abc_12"}, + }) +} + +func TestOutgoingResponseFilter_StatusCode(t *testing.T) { + tests := []struct { + name string + md runtime.ServerMetadata + msg proto.Message + wantErr bool + wantStatusCode int + wantContent bool + }{ + { + name: "204", + md: runtime.ServerMetadata{HeaderMD: metadata.Pairs(StatusCodeHeader, "204")}, + wantStatusCode: 204, + }, + { + name: "418", + md: runtime.ServerMetadata{HeaderMD: metadata.Pairs(StatusCodeHeader, "418")}, + wantStatusCode: 418, + wantContent: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + rec := httptest.NewRecorder() + ctx := runtime.NewServerMetadataContext(context.Background(), tt.md) + err := OutgoingResponseFilter(ctx, rec, &emptypb.EmptyResponse{}) + if tt.wantErr { + require.Error(err) + return + } + require.NoError(err) + resp := rec.Result() + if tt.wantStatusCode != 0 { + assert.Equal(tt.wantStatusCode, resp.StatusCode) + } + }) + } + +} diff --git a/internal/servers/controller/handlers/sessions/session_service_test.go b/internal/servers/controller/handlers/sessions/session_service_test.go index a997d8d65..4fa363167 100644 --- a/internal/servers/controller/handlers/sessions/session_service_test.go +++ b/internal/servers/controller/handlers/sessions/session_service_test.go @@ -12,6 +12,7 @@ import ( "github.com/hashicorp/boundary/internal/db" "github.com/hashicorp/boundary/internal/errors" pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services" + authpb "github.com/hashicorp/boundary/internal/gen/controller/auth" "github.com/hashicorp/boundary/internal/host/static" "github.com/hashicorp/boundary/internal/iam" "github.com/hashicorp/boundary/internal/kms" @@ -211,15 +212,15 @@ func TestList_Self(t *testing.T) { t.Run(tc.name, func(t *testing.T) { // Setup the auth request information req := httptest.NewRequest("GET", fmt.Sprintf("http://127.0.0.1/v1/sessions?scope_id=%s", pWithSessions.GetPublicId()), nil) - requestInfo := auth.RequestInfo{ + requestInfo := authpb.RequestInfo{ Path: req.URL.Path, Method: req.Method, - TokenFormat: auth.AuthTokenTypeBearer, + TokenFormat: uint32(auth.AuthTokenTypeBearer), PublicId: tc.requester.GetPublicId(), Token: tc.requester.GetToken(), } - ctx := auth.NewVerifierContext(context.Background(), iamRepoFn, tokenRepoFn, serversRepoFn, kms, requestInfo) + ctx := auth.NewVerifierContext(context.Background(), iamRepoFn, tokenRepoFn, serversRepoFn, kms, &requestInfo) got, err := s.ListSessions(ctx, &pbs.ListSessionsRequest{ScopeId: pWithSessions.GetPublicId()}) require.NoError(t, err) assert.Equal(t, tc.count, len(got.GetItems()), got.GetItems()) diff --git a/internal/servers/controller/handlers/targets/target_service_test.go b/internal/servers/controller/handlers/targets/target_service_test.go index f5b087d50..7dd69c1ed 100644 --- a/internal/servers/controller/handlers/targets/target_service_test.go +++ b/internal/servers/controller/handlers/targets/target_service_test.go @@ -17,6 +17,7 @@ import ( "github.com/hashicorp/boundary/internal/credential/vault" "github.com/hashicorp/boundary/internal/db" pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services" + authpb "github.com/hashicorp/boundary/internal/gen/controller/auth" spbs "github.com/hashicorp/boundary/internal/gen/controller/servers/services" "github.com/hashicorp/boundary/internal/host/static" "github.com/hashicorp/boundary/internal/iam" @@ -2500,9 +2501,9 @@ func TestAuthorizeSession(t *testing.T) { atRepoFn, serversRepoFn, kms, - auth.RequestInfo{ + &authpb.RequestInfo{ Token: at.GetToken(), - TokenFormat: auth.AuthTokenTypeBearer, + TokenFormat: uint32(auth.AuthTokenTypeBearer), PublicId: at.GetPublicId(), }) @@ -2674,9 +2675,9 @@ func TestAuthorizeSession_Errors(t *testing.T) { atRepoFn, serversRepoFn, kms, - auth.RequestInfo{ + &authpb.RequestInfo{ Token: at.GetToken(), - TokenFormat: auth.AuthTokenTypeBearer, + TokenFormat: uint32(auth.AuthTokenTypeBearer), PublicId: at.GetPublicId(), }) r := iam.TestRole(t, conn, proj.GetPublicId()) diff --git a/internal/servers/controller/handlers/verifiers_test.go b/internal/servers/controller/handlers/verifiers_test.go index d1717c2d6..7fd291b12 100644 --- a/internal/servers/controller/handlers/verifiers_test.go +++ b/internal/servers/controller/handlers/verifiers_test.go @@ -15,10 +15,10 @@ import ( func errorIncludesFields(t *testing.T, err error, wantFields []string) { t.Helper() - var apiErr *apiError + var apiErr *ApiError require.True(t, errors.As(err, &apiErr)) var gotFields []string - for _, d := range apiErr.inner.GetDetails().GetRequestFields() { + for _, d := range apiErr.Inner.GetDetails().GetRequestFields() { gotFields = append(gotFields, d.GetName()) } assert.ElementsMatch(t, gotFields, wantFields) diff --git a/internal/servers/controller/interceptor.go b/internal/servers/controller/interceptor.go new file mode 100644 index 000000000..4dfaeab98 --- /dev/null +++ b/internal/servers/controller/interceptor.go @@ -0,0 +1,207 @@ +package controller + +import ( + "context" + "fmt" + "net/http" + "reflect" + + "github.com/hashicorp/boundary/internal/errors" + pb "github.com/hashicorp/boundary/internal/gen/controller/api" + authpb "github.com/hashicorp/boundary/internal/gen/controller/auth" + pberrors "github.com/hashicorp/boundary/internal/gen/errors" + "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/requests" + "github.com/hashicorp/boundary/internal/servers/controller/auth" + "github.com/hashicorp/boundary/internal/servers/controller/common" + "github.com/hashicorp/boundary/internal/servers/controller/handlers" + + "github.com/mr-tron/base58" + + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/proto" +) + +const ( + requestInfoMdKey = "request-info" + + // domainErrHeader defines an http header for encoded domain errors from the + // grpc server. + domainErrHeader = "x-domain-err" + + // apiErrHeader defines an http header for encoded api errors from the + // grpc server. + apiErrHeader = "x-api-err" +) + +// requestCtxInterceptor creates an unary server interceptor that pulls grpc +// metadata into a ctx for the request. The metadata must be set in an upstream +// http handler/middleware by marshalling a RequestInfo protobuf into the +// requestInfoMdKey header (see: controller.wrapHandlerWithCommonFuncs). +func requestCtxInterceptor( + ctx context.Context, + iamRepoFn common.IamRepoFactory, + authTokenRepoFn common.AuthTokenRepoFactory, + serversRepoFn common.ServersRepoFactory, + kms *kms.Kms, + ticket string, +) (grpc.UnaryServerInterceptor, error) { + const op = "controller.requestCtxInterceptor" + if iamRepoFn == nil { + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing iam repo function") + } + if authTokenRepoFn == nil { + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing auth token repo function") + } + if serversRepoFn == nil { + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing servers repo function") + } + if kms == nil { + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing kms") + } + if ticket == "" { + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing ticket") + } + // Authorization unary interceptor function to handle authorize per RPC call + return func(interceptorCtx context.Context, + req interface{}, + _ *grpc.UnaryServerInfo, + handler grpc.UnaryHandler) (interface{}, error) { + md, ok := metadata.FromIncomingContext(interceptorCtx) + if !ok { + return nil, errors.New(interceptorCtx, errors.Internal, op, "No metadata") + } + + values := md.Get(requestInfoMdKey) + if len(values) == 0 { + return nil, errors.New(interceptorCtx, errors.Internal, op, "Missing request metadata") + } + if len(values) > 1 { + return nil, errors.New(interceptorCtx, errors.Internal, op, fmt.Sprintf("expected 1 value for %s metadata and got %d", requestInfoMdKey, len(values))) + } + + decoded, err := base58.FastBase58Decoding(values[0]) + if err != nil { + return nil, errors.Wrap(interceptorCtx, err, op, errors.WithCode(errors.Internal), errors.WithMsg("unable to decode request info")) + } + var requestInfo authpb.RequestInfo + if err := proto.Unmarshal(decoded, &requestInfo); err != nil { + return nil, errors.Wrap(interceptorCtx, err, op, errors.WithCode(errors.Internal), errors.WithMsg("unable to unmarshal request info")) + } + switch { + case requestInfo.Ticket == "": + return nil, errors.New(interceptorCtx, errors.Internal, op, "Invalid context (missing ticket)") + case requestInfo.Ticket != ticket: + return nil, errors.New(interceptorCtx, errors.Internal, op, "Invalid context (bad ticket)") + } + + interceptorCtx = auth.NewVerifierContext(interceptorCtx, iamRepoFn, authTokenRepoFn, serversRepoFn, kms, &requestInfo) + + // Add general request information to the context. The information from + // the auth verifier context is pretty specifically curated to + // authentication/authorization verification so this is more + // general-purpose. + // + // We could use requests.NewRequestContext but this saves an immediate + // lookup. + interceptorCtx = context.WithValue(interceptorCtx, requests.ContextRequestInformationKey, &requests.RequestContext{ + Path: requestInfo.Path, + Method: requestInfo.Method, + }) + + // Calls the handler + h, err := handler(interceptorCtx, req) + + return h, err // not convinced we want to wrap every error and turn them into domain errors... + }, nil +} + +func errorInterceptor( + _ context.Context, +) grpc.UnaryServerInterceptor { + const op = "controller.errorInterceptor" + return func(interceptorCtx context.Context, + req interface{}, + _ *grpc.UnaryServerInfo, + handler grpc.UnaryHandler) (interface{}, error) { + + // call the handler... + h, handlerErr := handler(interceptorCtx, req) + + // if there's an error and it's a domain err, then encode it into the + if handlerErr != nil { + var domainErr *errors.Err + isDomainErr := errors.As(handlerErr, &domainErr) + if isDomainErr { + pbErr := errors.ToPbErrors(domainErr) + var buf []byte + var err error + if buf, err = proto.Marshal(pbErr); err != nil { + return h, status.Errorf(codes.Internal, "unable to marshal domain error: %s", err) + } + if err := grpc.SetHeader(interceptorCtx, metadata.Pairs(domainErrHeader, base58.FastBase58Encoding(buf))); err != nil { + return h, status.Errorf(codes.Internal, "unable to set domain error header: %s", err) + } + return h, handlerErr + } + + var apiErr *handlers.ApiError + isApiError := errors.As(handlerErr, &apiErr) + if isApiError { + pbErr := &pberrors.ApiError{ + ApiError: apiErr.Inner, + Status: apiErr.Status, + } + var buf []byte + var err error + if buf, err = proto.Marshal(pbErr); err != nil { + return h, status.Errorf(codes.Internal, "unable to marshal api error: %s", err) + } + if err := grpc.SetHeader(interceptorCtx, metadata.Pairs(apiErrHeader, base58.FastBase58Encoding(buf))); err != nil { + return h, status.Errorf(codes.Internal, "unable to set api error header: %s", err) + } + return h, handlerErr + } + } + return h, handlerErr + } +} + +func statusCodeInterceptor( + _ context.Context, +) grpc.UnaryServerInterceptor { + const op = "controller.statusCodeInterceptor" + return func(interceptorCtx context.Context, + req interface{}, + _ *grpc.UnaryServerInfo, + handler grpc.UnaryHandler) (interface{}, error) { + + // call the handler... + h, handlerErr := handler(interceptorCtx, req) + + // if a service handler returns nil, nil then we want to single a 204 + // response to the proxy with no resp msg + if isNil(h) && handlerErr == nil { + if err := handlers.SetStatusCode(interceptorCtx, http.StatusNoContent); err != nil { + return &pb.EmptyResponse{}, err + } + return &pb.EmptyResponse{}, nil + } + + return h, handlerErr + } +} + +func isNil(i interface{}) bool { + if i == nil { + return true + } + switch reflect.TypeOf(i).Kind() { + case reflect.Ptr, reflect.Map, reflect.Array, reflect.Chan, reflect.Slice: + return reflect.ValueOf(i).IsNil() + } + return false +} diff --git a/internal/servers/controller/interceptor_test.go b/internal/servers/controller/interceptor_test.go new file mode 100644 index 000000000..8553f2498 --- /dev/null +++ b/internal/servers/controller/interceptor_test.go @@ -0,0 +1,455 @@ +package controller + +import ( + "context" + "fmt" + "net/http" + "net/http/httptest" + "strconv" + "testing" + + "github.com/hashicorp/boundary/internal/authtoken" + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/errors" + pb_api "github.com/hashicorp/boundary/internal/gen/controller/api" + 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" + "github.com/hashicorp/boundary/internal/servers" + "github.com/hashicorp/boundary/internal/servers/controller/auth" + "github.com/hashicorp/boundary/internal/servers/controller/common" + "github.com/hashicorp/boundary/internal/servers/controller/handlers" + "github.com/mr-tron/base58" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/grpc" + "google.golang.org/grpc/metadata" + "google.golang.org/protobuf/proto" +) + +func Test_requestCtxInterceptor(t *testing.T) { + 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) { + return authtoken.NewRepository(rw, rw, kmsCache) + } + serversRepoFn := func() (*servers.Repository, error) { + return servers.NewRepository(rw, rw, kmsCache) + } + + 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{ + Path: req.URL.Path, + Method: req.Method, + } + 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() + + returnCtxHandler := func(ctx context.Context, req interface{}) (interface{}, error) { + return ctx, nil + } + + tests := []struct { + name string + requestCtx context.Context + iamRepoFn common.IamRepoFactory + authTokenRepoFn common.AuthTokenRepoFactory + serversRepoFn common.ServersRepoFactory + kms *kms.Kms + 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, + wantFactoryErr: true, + wantFactoryErrMatch: errors.T(errors.InvalidParameter), + wantFactoryErrContains: "missing iam repo", + }, + { + name: "missing-at-repo", + requestCtx: newReqCtx(validGatewayTicket), + iamRepoFn: iamRepoFn, + serversRepoFn: serversRepoFn, + kms: kmsCache, + 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, + wantFactoryErr: true, + wantFactoryErrMatch: errors.T(errors.InvalidParameter), + wantFactoryErrContains: "missing servers repo function", + }, + { + name: "missing-kms", + 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, + wantFactoryErr: true, + wantFactoryErrMatch: errors.T(errors.InvalidParameter), + wantFactoryErrContains: "missing ticket", + }, + { + name: "missing-metadata", + requestCtx: context.Background(), + iamRepoFn: iamRepoFn, + authTokenRepoFn: atRepoFn, + serversRepoFn: serversRepoFn, + kms: kmsCache, + 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, + 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, + 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, + 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, + 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, + 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, + 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, + 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, + ticket: validGatewayTicket, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + interceptor, err := requestCtxInterceptor(factoryCtx, tt.iamRepoFn, tt.authTokenRepoFn, tt.serversRepoFn, tt.kms, tt.ticket) + 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) + verifyResults := auth.Verify(retCtx.(context.Context)) + assert.NotEmpty(verifyResults) + }) + } +} + +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) + } + }) + } + +} + +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 + } +} diff --git a/internal/servers/controller/listeners.go b/internal/servers/controller/listeners.go index 9a0d2feae..ad62e2822 100644 --- a/internal/servers/controller/listeners.go +++ b/internal/servers/controller/listeners.go @@ -12,6 +12,7 @@ import ( "sync" "time" + "github.com/hashicorp/boundary/globals" "github.com/hashicorp/boundary/internal/cmd/base" pbs "github.com/hashicorp/boundary/internal/gen/controller/servers/services" "github.com/hashicorp/boundary/internal/libs/alpnmux" @@ -20,10 +21,16 @@ import ( "google.golang.org/grpc" ) -func (c *Controller) startListeners() error { +func (c *Controller) startListeners(ctx context.Context) error { servers := make([]func(), 0, len(c.conf.Listeners)) configureForAPI := func(ln *base.ServerListener) error { + var err error + if c.gatewayServer, c.gatewayTicket, err = newGatewayServer(ctx, c.IamRepoFn, c.AuthTokenRepoFn, c.ServersRepoFn, c.kms); err != nil { + return err + } + c.gatewayMux = newGatewayMux() + handler, err := c.handler(HandlerProperties{ ListenerConfig: ln.Config, CancelCtx: c.baseContext, @@ -134,6 +141,11 @@ func (c *Controller) startListeners() error { return nil } + c.gatewayListener, _ = newGatewayListener() + servers = append(servers, func() { + go c.gatewayServer.Serve(c.gatewayListener) + }) + for _, ln := range c.conf.Listeners { var err error for _, purpose := range ln.Config.Purpose { @@ -184,6 +196,21 @@ func (c *Controller) stopListeners(serversOnly bool) error { } }() } + + if c.gatewayServer != nil { + serverWg.Add(1) + go func() { + defer serverWg.Done() + shutdownKill, shutdownKillCancel := context.WithTimeout(c.baseContext, globals.DefaultMaxRequestDuration) + defer shutdownKillCancel() + go func() { + <-shutdownKill.Done() + c.gatewayServer.Stop() + }() + c.gatewayServer.GracefulStop() + }() + } + serverWg.Wait() if serversOnly { return nil diff --git a/internal/servers/controller/testing.go b/internal/servers/controller/testing.go index 85c62ace7..1d48a6461 100644 --- a/internal/servers/controller/testing.go +++ b/internal/servers/controller/testing.go @@ -10,6 +10,7 @@ import ( "testing" "time" + grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware" "github.com/hashicorp/boundary/api" "github.com/hashicorp/boundary/api/authmethods" "github.com/hashicorp/boundary/api/authtokens" @@ -18,6 +19,7 @@ import ( "github.com/hashicorp/boundary/internal/cmd/config" "github.com/hashicorp/boundary/internal/db" "github.com/hashicorp/boundary/internal/db/schema" + "github.com/hashicorp/boundary/internal/gen/testing/interceptor" "github.com/hashicorp/boundary/internal/iam" "github.com/hashicorp/boundary/internal/intglobals" "github.com/hashicorp/boundary/internal/kms" @@ -27,6 +29,9 @@ import ( wrapping "github.com/hashicorp/go-kms-wrapping" "github.com/hashicorp/go-secure-stdlib/base62" "github.com/hashicorp/go-secure-stdlib/strutil" + "github.com/stretchr/testify/require" + "google.golang.org/grpc" + "google.golang.org/grpc/test/bufconn" ) const ( @@ -716,3 +721,39 @@ func (tc *TestController) WaitForNextWorkerStatusUpdate(workerId string) error { event.WriteSysEvent(ctx, op, "waiting for next status report from worker received successfully", "worker", workerId) return nil } + +// startTestGreeterService is intended to facilitate the testing of +// interceptors. You provide a greeter service that produces appropriate +// responses to test your interceptor(s). This test function will start up a +// test greeter service wrapped with the specified interceptors and return an +// initialized client for the service. The test service will be stopped/cleaned +// up after the test (or subtests) have completed. +func startTestGreeterService(t *testing.T, greeter interceptor.GreeterServiceServer, interceptors ...grpc.UnaryServerInterceptor) interceptor.GreeterServiceClient { + t.Helper() + require := require.New(t) + dialCtx := context.Background() + + buffer := 1024 * 1024 + listener := bufconn.Listen(buffer) + + s := grpc.NewServer( + grpc.UnaryInterceptor(grpc_middleware.ChainUnaryServer(interceptors...)), + ) + interceptor.RegisterGreeterServiceServer(s, greeter) + go func() { + err := s.Serve(listener) + require.NoError(err) + }() + + conn, _ := grpc.DialContext(dialCtx, "", grpc.WithContextDialer(func(context.Context, string) (net.Conn, error) { + return listener.Dial() + }), grpc.WithInsecure()) + + t.Cleanup(func() { + listener.Close() + s.Stop() + }) + + client := interceptor.NewGreeterServiceClient(conn) + return client +} diff --git a/internal/tests/cli/boundary/_helpers.bash b/internal/tests/cli/boundary/_helpers.bash index ec87630ae..3b65f5de1 100644 --- a/internal/tests/cli/boundary/_helpers.bash +++ b/internal/tests/cli/boundary/_helpers.bash @@ -22,3 +22,11 @@ function strip_all() { echo "$1" | tr -d '"' | tr -d '\'\' } +function has_status_code() { + local json=$1 + local code=$2 + if [ echo "$json"|jq -c ".status_code == $code" ]; then + return 1 + fi + +} \ No newline at end of file diff --git a/internal/tests/cli/boundary/groups.bats b/internal/tests/cli/boundary/groups.bats index 498b95da3..798dc1719 100644 --- a/internal/tests/cli/boundary/groups.bats +++ b/internal/tests/cli/boundary/groups.bats @@ -59,6 +59,7 @@ export NEW_GROUP='test' local gid=$(group_id $NEW_GROUP) run delete_group $gid echo "$output" + run has_status_code "$output" "204" [ "$status" -eq 0 ] } diff --git a/internal/tests/cli/boundary/host_catalogs.bats b/internal/tests/cli/boundary/host_catalogs.bats index 3e3630429..376b0afe4 100644 --- a/internal/tests/cli/boundary/host_catalogs.bats +++ b/internal/tests/cli/boundary/host_catalogs.bats @@ -44,6 +44,7 @@ export NEW_HOST_CATALOG='test' local hid=$(host_catalog_id $NEW_HOST_CATALOG $DEFAULT_P_ID) run delete_host_catalog $hid echo "$output" + run has_status_code "$output" "204" [ "$status" -eq 0 ] } diff --git a/internal/tests/cli/boundary/host_sets.bats b/internal/tests/cli/boundary/host_sets.bats index 4f1a17667..bd095bfce 100644 --- a/internal/tests/cli/boundary/host_sets.bats +++ b/internal/tests/cli/boundary/host_sets.bats @@ -58,6 +58,7 @@ export NEW_HOST_SET='test' local hsid=$(host_set_id $NEW_HOST_SET $DEFAULT_HOST_CATALOG) run delete_host_set $hsid echo "$output" + run has_status_code "$output" "204" [ "$status" -eq 0 ] } diff --git a/internal/tests/cli/boundary/hosts.bats b/internal/tests/cli/boundary/hosts.bats index f25e15880..91ae9ef3e 100644 --- a/internal/tests/cli/boundary/hosts.bats +++ b/internal/tests/cli/boundary/hosts.bats @@ -44,6 +44,7 @@ export NEW_HOST='test' local hid=$(host_id $NEW_HOST $DEFAULT_HOST_CATALOG) run delete_host $hid echo "$output" + run has_status_code "$output" "204" [ "$status" -eq 0 ] } diff --git a/internal/tests/cli/boundary/roles.bats b/internal/tests/cli/boundary/roles.bats index 6546f9eff..3555c0011 100644 --- a/internal/tests/cli/boundary/roles.bats +++ b/internal/tests/cli/boundary/roles.bats @@ -102,6 +102,7 @@ export NEW_GRANT='id=*;type=*;actions=create,read,update,delete,list' local rid=$(role_id $NEW_ROLE $DEFAULT_GLOBAL) run delete_role $rid echo "$output" + run has_status_code "$output" "204" [ "$status" -eq 0 ] } diff --git a/internal/tests/cli/boundary/scopes.bats b/internal/tests/cli/boundary/scopes.bats index af7ece028..6d27495a3 100644 --- a/internal/tests/cli/boundary/scopes.bats +++ b/internal/tests/cli/boundary/scopes.bats @@ -64,6 +64,7 @@ export NEW_PROJECT='test_project' local sid=$(scope_id $NEW_PROJECT $parent) run delete_scope $sid echo "$output" + run has_status_code "$output" "204" [ "$status" -eq 0 ] } @@ -79,6 +80,7 @@ export NEW_PROJECT='test_project' local sid=$(scope_id $NEW_ORG $DEFAULT_GLOBAL) run delete_scope $sid echo "$output" + run has_status_code "$output" "204" [ "$status" -eq 0 ] } diff --git a/internal/tests/cli/boundary/target.bats b/internal/tests/cli/boundary/target.bats index f53f2d4be..812ce0d9b 100644 --- a/internal/tests/cli/boundary/target.bats +++ b/internal/tests/cli/boundary/target.bats @@ -92,6 +92,7 @@ load _helpers @test "boundary/target: default user can delete target" { local id=$(target_id_from_name $DEFAULT_P_ID $TGT_NAME) run delete_target $id + run has_status_code "$output" "204" [ "$status" -eq 0 ] } diff --git a/internal/tests/cli/boundary/user.bats b/internal/tests/cli/boundary/user.bats index 9451ed655..8fac3eb86 100644 --- a/internal/tests/cli/boundary/user.bats +++ b/internal/tests/cli/boundary/user.bats @@ -68,7 +68,9 @@ export NEW_USER='test' @test "boundary/user: can delete $NEW_USER user" { login $DEFAULT_LOGIN local uid=$(user_id $NEW_USER) - run delete_user $uid + run delete_user $uid + echo $output + run has_status_code "$output" "204" [ "$status" -eq 0 ] } @@ -82,4 +84,4 @@ export NEW_USER='test' local aid=$(account_id $NEW_USER) run delete_account $aid [ "$status" -eq 0 ] -} \ No newline at end of file +}