diff --git a/api/client/client.go b/api/client/client.go index 5e4380cce2d06..3bfc51c054f89 100644 --- a/api/client/client.go +++ b/api/client/client.go @@ -3431,6 +3431,18 @@ func (c *Client) DeleteAllIntegrations(ctx context.Context) error { return trail.FromGRPC(err) } +// GenerateAWSOIDCToken generates a token to be used when executing an AWS OIDC Integration action. +func (c *Client) GenerateAWSOIDCToken(ctx context.Context, req types.GenerateAWSOIDCTokenRequest) (string, error) { + resp, err := c.integrationsClient().GenerateAWSOIDCToken(ctx, &integrationpb.GenerateAWSOIDCTokenRequest{ + Issuer: req.Issuer, + }) + if err != nil { + return "", trail.FromGRPC(err) + } + + return resp.GetToken(), nil +} + // GetLoginRule retrieves a login rule described by name. func (c *Client) GetLoginRule(ctx context.Context, name string) (*loginrulepb.LoginRule, error) { rule, err := c.LoginRuleClient().GetLoginRule(ctx, &loginrulepb.GetLoginRuleRequest{ diff --git a/api/gen/proto/go/teleport/integration/v1/integration_service.pb.go b/api/gen/proto/go/teleport/integration/v1/integration_service.pb.go index c6c19a792bee6..03a0406a5f661 100644 --- a/api/gen/proto/go/teleport/integration/v1/integration_service.pb.go +++ b/api/gen/proto/go/teleport/integration/v1/integration_service.pb.go @@ -396,6 +396,106 @@ func (*DeleteAllIntegrationsRequest) Descriptor() ([]byte, []int) { return file_teleport_integration_v1_integration_service_proto_rawDescGZIP(), []int{6} } +// GenerateAWSOIDCTokenRequest are the parameters used to request an AWS OIDC +// Integration token. +type GenerateAWSOIDCTokenRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Issuer is the entity that is signing the JWT. + // This value must contain the AWS OIDC Integration configured provider (Teleport Proxy's Public URL) + Issuer string `protobuf:"bytes,1,opt,name=issuer,proto3" json:"issuer,omitempty"` +} + +func (x *GenerateAWSOIDCTokenRequest) Reset() { + *x = GenerateAWSOIDCTokenRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_teleport_integration_v1_integration_service_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GenerateAWSOIDCTokenRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GenerateAWSOIDCTokenRequest) ProtoMessage() {} + +func (x *GenerateAWSOIDCTokenRequest) ProtoReflect() protoreflect.Message { + mi := &file_teleport_integration_v1_integration_service_proto_msgTypes[7] + 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 GenerateAWSOIDCTokenRequest.ProtoReflect.Descriptor instead. +func (*GenerateAWSOIDCTokenRequest) Descriptor() ([]byte, []int) { + return file_teleport_integration_v1_integration_service_proto_rawDescGZIP(), []int{7} +} + +func (x *GenerateAWSOIDCTokenRequest) GetIssuer() string { + if x != nil { + return x.Issuer + } + return "" +} + +// GenerateAWSOIDCTokenResponse contains a signed AWS OIDC Integration token. +type GenerateAWSOIDCTokenResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Token is the signed JWT ready to be used + Token string `protobuf:"bytes,1,opt,name=token,proto3" json:"token,omitempty"` +} + +func (x *GenerateAWSOIDCTokenResponse) Reset() { + *x = GenerateAWSOIDCTokenResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_teleport_integration_v1_integration_service_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *GenerateAWSOIDCTokenResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GenerateAWSOIDCTokenResponse) ProtoMessage() {} + +func (x *GenerateAWSOIDCTokenResponse) ProtoReflect() protoreflect.Message { + mi := &file_teleport_integration_v1_integration_service_proto_msgTypes[8] + 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 GenerateAWSOIDCTokenResponse.ProtoReflect.Descriptor instead. +func (*GenerateAWSOIDCTokenResponse) Descriptor() ([]byte, []int) { + return file_teleport_integration_v1_integration_service_proto_rawDescGZIP(), []int{8} +} + +func (x *GenerateAWSOIDCTokenResponse) GetToken() string { + if x != nil { + return x.Token + } + return "" +} + var File_teleport_integration_v1_integration_service_proto protoreflect.FileDescriptor var file_teleport_integration_v1_integration_service_proto_rawDesc = []byte{ @@ -440,51 +540,67 @@ var file_teleport_integration_v1_integration_service_proto_rawDesc = []byte{ 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x1e, 0x0a, 0x1c, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x6c, 0x6c, 0x49, 0x6e, 0x74, 0x65, 0x67, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x32, 0xe9, 0x04, 0x0a, 0x12, 0x49, 0x6e, 0x74, 0x65, 0x67, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x77, 0x0a, 0x10, 0x4c, 0x69, 0x73, 0x74, 0x49, - 0x6e, 0x74, 0x65, 0x67, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x30, 0x2e, 0x74, 0x65, - 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x67, 0x72, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x49, 0x6e, 0x74, 0x65, 0x67, 0x72, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x31, 0x2e, - 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x67, 0x72, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x49, 0x6e, 0x74, 0x65, - 0x67, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x12, 0x56, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x49, 0x6e, 0x74, 0x65, 0x67, 0x72, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x12, 0x2e, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, - 0x74, 0x65, 0x67, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, - 0x49, 0x6e, 0x74, 0x65, 0x67, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, - 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x49, 0x6e, 0x74, 0x65, 0x67, - 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x56, 0x31, 0x12, 0x5c, 0x0a, 0x11, 0x43, 0x72, 0x65, 0x61, - 0x74, 0x65, 0x49, 0x6e, 0x74, 0x65, 0x67, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x31, 0x2e, - 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x67, 0x72, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x49, 0x6e, - 0x74, 0x65, 0x67, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x14, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x49, 0x6e, 0x74, 0x65, 0x67, 0x72, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x56, 0x31, 0x12, 0x5c, 0x0a, 0x11, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, - 0x49, 0x6e, 0x74, 0x65, 0x67, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x31, 0x2e, 0x74, 0x65, - 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x67, 0x72, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x49, 0x6e, 0x74, 0x65, - 0x67, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, - 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x49, 0x6e, 0x74, 0x65, 0x67, 0x72, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x56, 0x31, 0x12, 0x5e, 0x0a, 0x11, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x49, 0x6e, - 0x74, 0x65, 0x67, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x31, 0x2e, 0x74, 0x65, 0x6c, 0x65, - 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x67, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x49, 0x6e, 0x74, 0x65, 0x67, 0x72, - 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, - 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, - 0x6d, 0x70, 0x74, 0x79, 0x12, 0x66, 0x0a, 0x15, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x6c, - 0x6c, 0x49, 0x6e, 0x74, 0x65, 0x67, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x35, 0x2e, - 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x67, 0x72, 0x61, - 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x6c, - 0x6c, 0x49, 0x6e, 0x74, 0x65, 0x67, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x42, 0x5a, 0x5a, 0x58, - 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x72, 0x61, 0x76, 0x69, - 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x2f, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, - 0x74, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, - 0x67, 0x6f, 0x2f, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2f, 0x69, 0x6e, 0x74, 0x65, - 0x67, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x76, 0x31, 0x3b, 0x69, 0x6e, 0x74, 0x65, 0x67, - 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x22, 0x35, 0x0a, 0x1b, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x41, 0x57, 0x53, 0x4f, + 0x49, 0x44, 0x43, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, + 0x16, 0x0a, 0x06, 0x69, 0x73, 0x73, 0x75, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x06, 0x69, 0x73, 0x73, 0x75, 0x65, 0x72, 0x22, 0x34, 0x0a, 0x1c, 0x47, 0x65, 0x6e, 0x65, 0x72, + 0x61, 0x74, 0x65, 0x41, 0x57, 0x53, 0x4f, 0x49, 0x44, 0x43, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x32, 0xef, 0x05, + 0x0a, 0x12, 0x49, 0x6e, 0x74, 0x65, 0x67, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x53, 0x65, 0x72, + 0x76, 0x69, 0x63, 0x65, 0x12, 0x77, 0x0a, 0x10, 0x4c, 0x69, 0x73, 0x74, 0x49, 0x6e, 0x74, 0x65, + 0x67, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x30, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, + 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x67, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, + 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x49, 0x6e, 0x74, 0x65, 0x67, 0x72, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x31, 0x2e, 0x74, 0x65, 0x6c, + 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x67, 0x72, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x49, 0x6e, 0x74, 0x65, 0x67, 0x72, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x56, 0x0a, + 0x0e, 0x47, 0x65, 0x74, 0x49, 0x6e, 0x74, 0x65, 0x67, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, + 0x2e, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x67, + 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x49, 0x6e, 0x74, + 0x65, 0x67, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x14, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x49, 0x6e, 0x74, 0x65, 0x67, 0x72, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x56, 0x31, 0x12, 0x5c, 0x0a, 0x11, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x49, + 0x6e, 0x74, 0x65, 0x67, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x31, 0x2e, 0x74, 0x65, 0x6c, + 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x67, 0x72, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x49, 0x6e, 0x74, 0x65, 0x67, + 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, + 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x49, 0x6e, 0x74, 0x65, 0x67, 0x72, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x56, 0x31, 0x12, 0x5c, 0x0a, 0x11, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x49, 0x6e, 0x74, + 0x65, 0x67, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x31, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, + 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x67, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, + 0x76, 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x49, 0x6e, 0x74, 0x65, 0x67, 0x72, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x74, 0x79, + 0x70, 0x65, 0x73, 0x2e, 0x49, 0x6e, 0x74, 0x65, 0x67, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x56, + 0x31, 0x12, 0x5e, 0x0a, 0x11, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x49, 0x6e, 0x74, 0x65, 0x67, + 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x31, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, + 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x67, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, + 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x49, 0x6e, 0x74, 0x65, 0x67, 0x72, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, + 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, + 0x79, 0x12, 0x66, 0x0a, 0x15, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x6c, 0x6c, 0x49, 0x6e, + 0x74, 0x65, 0x67, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x35, 0x2e, 0x74, 0x65, 0x6c, + 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x67, 0x72, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x6c, 0x6c, 0x49, 0x6e, + 0x74, 0x65, 0x67, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x62, 0x75, 0x66, 0x2e, 0x45, 0x6d, 0x70, 0x74, 0x79, 0x12, 0x83, 0x01, 0x0a, 0x14, 0x47, 0x65, + 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x41, 0x57, 0x53, 0x4f, 0x49, 0x44, 0x43, 0x54, 0x6f, 0x6b, + 0x65, 0x6e, 0x12, 0x34, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, + 0x74, 0x65, 0x67, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x6e, + 0x65, 0x72, 0x61, 0x74, 0x65, 0x41, 0x57, 0x53, 0x4f, 0x49, 0x44, 0x43, 0x54, 0x6f, 0x6b, 0x65, + 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x35, 0x2e, 0x74, 0x65, 0x6c, 0x65, 0x70, + 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x67, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, + 0x76, 0x31, 0x2e, 0x47, 0x65, 0x6e, 0x65, 0x72, 0x61, 0x74, 0x65, 0x41, 0x57, 0x53, 0x4f, 0x49, + 0x44, 0x43, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, + 0x5a, 0x5a, 0x58, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x72, + 0x61, 0x76, 0x69, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x2f, 0x74, 0x65, 0x6c, 0x65, + 0x70, 0x6f, 0x72, 0x74, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x2f, 0x67, 0x6f, 0x2f, 0x74, 0x65, 0x6c, 0x65, 0x70, 0x6f, 0x72, 0x74, 0x2f, 0x69, + 0x6e, 0x74, 0x65, 0x67, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x76, 0x31, 0x3b, 0x69, 0x6e, + 0x74, 0x65, 0x67, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x33, } var ( @@ -499,7 +615,7 @@ func file_teleport_integration_v1_integration_service_proto_rawDescGZIP() []byte return file_teleport_integration_v1_integration_service_proto_rawDescData } -var file_teleport_integration_v1_integration_service_proto_msgTypes = make([]protoimpl.MessageInfo, 7) +var file_teleport_integration_v1_integration_service_proto_msgTypes = make([]protoimpl.MessageInfo, 9) var file_teleport_integration_v1_integration_service_proto_goTypes = []interface{}{ (*ListIntegrationsRequest)(nil), // 0: teleport.integration.v1.ListIntegrationsRequest (*ListIntegrationsResponse)(nil), // 1: teleport.integration.v1.ListIntegrationsResponse @@ -508,30 +624,34 @@ var file_teleport_integration_v1_integration_service_proto_goTypes = []interface (*UpdateIntegrationRequest)(nil), // 4: teleport.integration.v1.UpdateIntegrationRequest (*DeleteIntegrationRequest)(nil), // 5: teleport.integration.v1.DeleteIntegrationRequest (*DeleteAllIntegrationsRequest)(nil), // 6: teleport.integration.v1.DeleteAllIntegrationsRequest - (*types.IntegrationV1)(nil), // 7: types.IntegrationV1 - (*emptypb.Empty)(nil), // 8: google.protobuf.Empty + (*GenerateAWSOIDCTokenRequest)(nil), // 7: teleport.integration.v1.GenerateAWSOIDCTokenRequest + (*GenerateAWSOIDCTokenResponse)(nil), // 8: teleport.integration.v1.GenerateAWSOIDCTokenResponse + (*types.IntegrationV1)(nil), // 9: types.IntegrationV1 + (*emptypb.Empty)(nil), // 10: google.protobuf.Empty } var file_teleport_integration_v1_integration_service_proto_depIdxs = []int32{ - 7, // 0: teleport.integration.v1.ListIntegrationsResponse.integrations:type_name -> types.IntegrationV1 - 7, // 1: teleport.integration.v1.CreateIntegrationRequest.integration:type_name -> types.IntegrationV1 - 7, // 2: teleport.integration.v1.UpdateIntegrationRequest.integration:type_name -> types.IntegrationV1 - 0, // 3: teleport.integration.v1.IntegrationService.ListIntegrations:input_type -> teleport.integration.v1.ListIntegrationsRequest - 2, // 4: teleport.integration.v1.IntegrationService.GetIntegration:input_type -> teleport.integration.v1.GetIntegrationRequest - 3, // 5: teleport.integration.v1.IntegrationService.CreateIntegration:input_type -> teleport.integration.v1.CreateIntegrationRequest - 4, // 6: teleport.integration.v1.IntegrationService.UpdateIntegration:input_type -> teleport.integration.v1.UpdateIntegrationRequest - 5, // 7: teleport.integration.v1.IntegrationService.DeleteIntegration:input_type -> teleport.integration.v1.DeleteIntegrationRequest - 6, // 8: teleport.integration.v1.IntegrationService.DeleteAllIntegrations:input_type -> teleport.integration.v1.DeleteAllIntegrationsRequest - 1, // 9: teleport.integration.v1.IntegrationService.ListIntegrations:output_type -> teleport.integration.v1.ListIntegrationsResponse - 7, // 10: teleport.integration.v1.IntegrationService.GetIntegration:output_type -> types.IntegrationV1 - 7, // 11: teleport.integration.v1.IntegrationService.CreateIntegration:output_type -> types.IntegrationV1 - 7, // 12: teleport.integration.v1.IntegrationService.UpdateIntegration:output_type -> types.IntegrationV1 - 8, // 13: teleport.integration.v1.IntegrationService.DeleteIntegration:output_type -> google.protobuf.Empty - 8, // 14: teleport.integration.v1.IntegrationService.DeleteAllIntegrations:output_type -> google.protobuf.Empty - 9, // [9:15] is the sub-list for method output_type - 3, // [3:9] is the sub-list for method input_type - 3, // [3:3] is the sub-list for extension type_name - 3, // [3:3] is the sub-list for extension extendee - 0, // [0:3] is the sub-list for field type_name + 9, // 0: teleport.integration.v1.ListIntegrationsResponse.integrations:type_name -> types.IntegrationV1 + 9, // 1: teleport.integration.v1.CreateIntegrationRequest.integration:type_name -> types.IntegrationV1 + 9, // 2: teleport.integration.v1.UpdateIntegrationRequest.integration:type_name -> types.IntegrationV1 + 0, // 3: teleport.integration.v1.IntegrationService.ListIntegrations:input_type -> teleport.integration.v1.ListIntegrationsRequest + 2, // 4: teleport.integration.v1.IntegrationService.GetIntegration:input_type -> teleport.integration.v1.GetIntegrationRequest + 3, // 5: teleport.integration.v1.IntegrationService.CreateIntegration:input_type -> teleport.integration.v1.CreateIntegrationRequest + 4, // 6: teleport.integration.v1.IntegrationService.UpdateIntegration:input_type -> teleport.integration.v1.UpdateIntegrationRequest + 5, // 7: teleport.integration.v1.IntegrationService.DeleteIntegration:input_type -> teleport.integration.v1.DeleteIntegrationRequest + 6, // 8: teleport.integration.v1.IntegrationService.DeleteAllIntegrations:input_type -> teleport.integration.v1.DeleteAllIntegrationsRequest + 7, // 9: teleport.integration.v1.IntegrationService.GenerateAWSOIDCToken:input_type -> teleport.integration.v1.GenerateAWSOIDCTokenRequest + 1, // 10: teleport.integration.v1.IntegrationService.ListIntegrations:output_type -> teleport.integration.v1.ListIntegrationsResponse + 9, // 11: teleport.integration.v1.IntegrationService.GetIntegration:output_type -> types.IntegrationV1 + 9, // 12: teleport.integration.v1.IntegrationService.CreateIntegration:output_type -> types.IntegrationV1 + 9, // 13: teleport.integration.v1.IntegrationService.UpdateIntegration:output_type -> types.IntegrationV1 + 10, // 14: teleport.integration.v1.IntegrationService.DeleteIntegration:output_type -> google.protobuf.Empty + 10, // 15: teleport.integration.v1.IntegrationService.DeleteAllIntegrations:output_type -> google.protobuf.Empty + 8, // 16: teleport.integration.v1.IntegrationService.GenerateAWSOIDCToken:output_type -> teleport.integration.v1.GenerateAWSOIDCTokenResponse + 10, // [10:17] is the sub-list for method output_type + 3, // [3:10] is the sub-list for method input_type + 3, // [3:3] is the sub-list for extension type_name + 3, // [3:3] is the sub-list for extension extendee + 0, // [0:3] is the sub-list for field type_name } func init() { file_teleport_integration_v1_integration_service_proto_init() } @@ -624,6 +744,30 @@ func file_teleport_integration_v1_integration_service_proto_init() { return nil } } + file_teleport_integration_v1_integration_service_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GenerateAWSOIDCTokenRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_teleport_integration_v1_integration_service_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*GenerateAWSOIDCTokenResponse); 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{ @@ -631,7 +775,7 @@ func file_teleport_integration_v1_integration_service_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_teleport_integration_v1_integration_service_proto_rawDesc, NumEnums: 0, - NumMessages: 7, + NumMessages: 9, NumExtensions: 0, NumServices: 1, }, diff --git a/api/gen/proto/go/teleport/integration/v1/integration_service_grpc.pb.go b/api/gen/proto/go/teleport/integration/v1/integration_service_grpc.pb.go index afcdf351e4227..9e618654b278b 100644 --- a/api/gen/proto/go/teleport/integration/v1/integration_service_grpc.pb.go +++ b/api/gen/proto/go/teleport/integration/v1/integration_service_grpc.pb.go @@ -41,6 +41,7 @@ const ( IntegrationService_UpdateIntegration_FullMethodName = "/teleport.integration.v1.IntegrationService/UpdateIntegration" IntegrationService_DeleteIntegration_FullMethodName = "/teleport.integration.v1.IntegrationService/DeleteIntegration" IntegrationService_DeleteAllIntegrations_FullMethodName = "/teleport.integration.v1.IntegrationService/DeleteAllIntegrations" + IntegrationService_GenerateAWSOIDCToken_FullMethodName = "/teleport.integration.v1.IntegrationService/GenerateAWSOIDCToken" ) // IntegrationServiceClient is the client API for IntegrationService service. @@ -59,6 +60,8 @@ type IntegrationServiceClient interface { DeleteIntegration(ctx context.Context, in *DeleteIntegrationRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) // DeleteAllIntegrations removes all Integrations. DeleteAllIntegrations(ctx context.Context, in *DeleteAllIntegrationsRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) + // GenerateAWSOIDCToken generates a token to be used when executing an AWS OIDC Integration action. + GenerateAWSOIDCToken(ctx context.Context, in *GenerateAWSOIDCTokenRequest, opts ...grpc.CallOption) (*GenerateAWSOIDCTokenResponse, error) } type integrationServiceClient struct { @@ -123,6 +126,15 @@ func (c *integrationServiceClient) DeleteAllIntegrations(ctx context.Context, in return out, nil } +func (c *integrationServiceClient) GenerateAWSOIDCToken(ctx context.Context, in *GenerateAWSOIDCTokenRequest, opts ...grpc.CallOption) (*GenerateAWSOIDCTokenResponse, error) { + out := new(GenerateAWSOIDCTokenResponse) + err := c.cc.Invoke(ctx, IntegrationService_GenerateAWSOIDCToken_FullMethodName, in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + // IntegrationServiceServer is the server API for IntegrationService service. // All implementations must embed UnimplementedIntegrationServiceServer // for forward compatibility @@ -139,6 +151,8 @@ type IntegrationServiceServer interface { DeleteIntegration(context.Context, *DeleteIntegrationRequest) (*emptypb.Empty, error) // DeleteAllIntegrations removes all Integrations. DeleteAllIntegrations(context.Context, *DeleteAllIntegrationsRequest) (*emptypb.Empty, error) + // GenerateAWSOIDCToken generates a token to be used when executing an AWS OIDC Integration action. + GenerateAWSOIDCToken(context.Context, *GenerateAWSOIDCTokenRequest) (*GenerateAWSOIDCTokenResponse, error) mustEmbedUnimplementedIntegrationServiceServer() } @@ -164,6 +178,9 @@ func (UnimplementedIntegrationServiceServer) DeleteIntegration(context.Context, func (UnimplementedIntegrationServiceServer) DeleteAllIntegrations(context.Context, *DeleteAllIntegrationsRequest) (*emptypb.Empty, error) { return nil, status.Errorf(codes.Unimplemented, "method DeleteAllIntegrations not implemented") } +func (UnimplementedIntegrationServiceServer) GenerateAWSOIDCToken(context.Context, *GenerateAWSOIDCTokenRequest) (*GenerateAWSOIDCTokenResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GenerateAWSOIDCToken not implemented") +} func (UnimplementedIntegrationServiceServer) mustEmbedUnimplementedIntegrationServiceServer() {} // UnsafeIntegrationServiceServer may be embedded to opt out of forward compatibility for this service. @@ -285,6 +302,24 @@ func _IntegrationService_DeleteAllIntegrations_Handler(srv interface{}, ctx cont return interceptor(ctx, in, info, handler) } +func _IntegrationService_GenerateAWSOIDCToken_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GenerateAWSOIDCTokenRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(IntegrationServiceServer).GenerateAWSOIDCToken(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: IntegrationService_GenerateAWSOIDCToken_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(IntegrationServiceServer).GenerateAWSOIDCToken(ctx, req.(*GenerateAWSOIDCTokenRequest)) + } + return interceptor(ctx, in, info, handler) +} + // IntegrationService_ServiceDesc is the grpc.ServiceDesc for IntegrationService service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -316,6 +351,10 @@ var IntegrationService_ServiceDesc = grpc.ServiceDesc{ MethodName: "DeleteAllIntegrations", Handler: _IntegrationService_DeleteAllIntegrations_Handler, }, + { + MethodName: "GenerateAWSOIDCToken", + Handler: _IntegrationService_GenerateAWSOIDCToken_Handler, + }, }, Streams: []grpc.StreamDesc{}, Metadata: "teleport/integration/v1/integration_service.proto", diff --git a/api/proto/teleport/integration/v1/integration_service.proto b/api/proto/teleport/integration/v1/integration_service.proto index f74809529640a..b982dfa08140e 100644 --- a/api/proto/teleport/integration/v1/integration_service.proto +++ b/api/proto/teleport/integration/v1/integration_service.proto @@ -40,6 +40,9 @@ service IntegrationService { // DeleteAllIntegrations removes all Integrations. rpc DeleteAllIntegrations(DeleteAllIntegrationsRequest) returns (google.protobuf.Empty); + + // GenerateAWSOIDCToken generates a token to be used when executing an AWS OIDC Integration action. + rpc GenerateAWSOIDCToken(GenerateAWSOIDCTokenRequest) returns (GenerateAWSOIDCTokenResponse); } // ListIntegrationsRequest is a request for a paginated list of Integrations. @@ -86,3 +89,17 @@ message DeleteIntegrationRequest { // DeleteAllIntegrationsRequest is the request for deleting all integrations. message DeleteAllIntegrationsRequest {} + +// GenerateAWSOIDCTokenRequest are the parameters used to request an AWS OIDC +// Integration token. +message GenerateAWSOIDCTokenRequest { + // Issuer is the entity that is signing the JWT. + // This value must contain the AWS OIDC Integration configured provider (Teleport Proxy's Public URL) + string issuer = 1; +} + +// GenerateAWSOIDCTokenResponse contains a signed AWS OIDC Integration token. +message GenerateAWSOIDCTokenResponse { + // Token is the signed JWT ready to be used + string token = 1; +} diff --git a/api/types/integration_awsoidc.go b/api/types/integration_awsoidc.go new file mode 100644 index 0000000000000..2e8adb598c4a1 --- /dev/null +++ b/api/types/integration_awsoidc.go @@ -0,0 +1,43 @@ +/* +Copyright 2023 Gravitational, Inc. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package types + +import "github.com/gravitational/trace" + +const ( + // IntegrationAWSOIDCAudience is the client id used to generate the JWT. + // This value must match the Audience defined in the IAM Identity Provider of the Integration. + IntegrationAWSOIDCAudience = "discover.teleport" + + // IntegrationAWSOIDCSubject identifies the system that is going to use the token. + IntegrationAWSOIDCSubject = "system:proxy" +) + +// GenerateAWSOIDCTokenRequest are the parameters used to request an AWS OIDC Integration token. +type GenerateAWSOIDCTokenRequest struct { + // Issuer is the entity that is signing the JWT. + // This value must contain the AWS OIDC Integration configured provider (Proxy's Public URL). + Issuer string `json:"issuer"` +} + +// CheckAndSetDefaults checks if the required fields are present. +func (req *GenerateAWSOIDCTokenRequest) CheckAndSetDefaults() error { + if req.Issuer == "" { + return trace.BadParameter("issuer is required") + } + + return nil +} diff --git a/api/types/integration_awsoidc_test.go b/api/types/integration_awsoidc_test.go new file mode 100644 index 0000000000000..8bcaa6bb160f8 --- /dev/null +++ b/api/types/integration_awsoidc_test.go @@ -0,0 +1,42 @@ +/* +Copyright 2023 Gravitational, Inc. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package types + +import ( + "testing" + + "github.com/gravitational/trace" + "github.com/stretchr/testify/require" +) + +// TestGenerateAWSOIDCTokenRequest validates that the required fields are checked. +func TestGenerateAWSOIDCTokenRequest(t *testing.T) { + t.Run("error when no issuer is provided", func(t *testing.T) { + req := GenerateAWSOIDCTokenRequest{} + + err := req.CheckAndSetDefaults() + require.True(t, trace.IsBadParameter(err), "expected a bad parameter error, got %+v", err) + }) + + t.Run("success when issuer is provided", func(t *testing.T) { + req := GenerateAWSOIDCTokenRequest{ + Issuer: "https://example.com", + } + + err := req.CheckAndSetDefaults() + require.NoError(t, err) + }) +} diff --git a/api/types/trust.go b/api/types/trust.go index 62c24cf5335af..7b971354f5f66 100644 --- a/api/types/trust.go +++ b/api/types/trust.go @@ -47,10 +47,14 @@ const ( // DEPRECATED, DELETE IN 13.0.0. For more information see: // https://github.com/gravitational/teleport/issues/17493 CertAuthTypeAll CertAuthType = "all" + // OIDCIdPCA (OpenID Connect Identity Provider Certificate Authority) identifies + // the certificate authority that will be used by the OIDC Identity Provider. + // Similar to JWTSigner, it doesn't issue Certificates but signs JSON Web Tokens. + OIDCIdPCA CertAuthType = "oidc_idp" ) // CertAuthTypes lists all certificate authority types. -var CertAuthTypes = []CertAuthType{HostCA, UserCA, DatabaseCA, OpenSSHCA, JWTSigner, SAMLIDPCA} +var CertAuthTypes = []CertAuthType{HostCA, UserCA, DatabaseCA, OpenSSHCA, JWTSigner, SAMLIDPCA, OIDCIdPCA} // Check checks if certificate authority type value is correct func (c CertAuthType) Check() error { diff --git a/go.mod b/go.mod index 4a0c2dc0e093b..7c82bd4e73ac6 100644 --- a/go.mod +++ b/go.mod @@ -25,17 +25,18 @@ require ( github.com/aquasecurity/libbpfgo v0.4.5-libbpf-1.0.1 github.com/armon/go-radix v1.0.0 github.com/aws/aws-sdk-go v1.44.180 - github.com/aws/aws-sdk-go-v2 v1.17.3 + github.com/aws/aws-sdk-go-v2 v1.17.8 github.com/aws/aws-sdk-go-v2/config v1.18.8 github.com/aws/aws-sdk-go-v2/credentials v1.13.8 github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.21 github.com/aws/aws-sdk-go-v2/service/ec2 v1.78.0 + github.com/aws/aws-sdk-go-v2/service/rds v1.42.3 github.com/aws/aws-sdk-go-v2/service/sts v1.18.0 github.com/aws/aws-sigv4-auth-cassandra-gocql-driver-plugin v0.0.0-20220331165046-e4d000c0d6a6 github.com/beevik/etree v1.1.0 github.com/bufbuild/connect-go v1.4.1 github.com/coreos/go-oidc v2.1.0+incompatible // replaced - github.com/coreos/go-semver v0.3.0 + github.com/coreos/go-semver v0.3.1 github.com/creack/pty v1.1.18 github.com/crewjam/saml v0.4.14-0.20230420111643-34930b26d33b github.com/datastax/go-cassandra-native-protocol v0.0.0-20220706104457-5e8aad05cf90 @@ -202,13 +203,13 @@ require ( github.com/apache/arrow/go/arrow v0.0.0-20211112161151-bc219186db40 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10 // indirect github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.45 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.27 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.21 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.32 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.26 // indirect github.com/aws/aws-sdk-go-v2/internal/ini v1.3.28 // indirect github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.18 // indirect github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11 // indirect github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.22 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.21 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.26 // indirect github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.21 // indirect github.com/aws/aws-sdk-go-v2/service/s3 v1.29.6 // indirect github.com/aws/aws-sdk-go-v2/service/sso v1.12.0 // indirect diff --git a/go.sum b/go.sum index 196cf613c8654..ceaecb704f0ee 100644 --- a/go.sum +++ b/go.sum @@ -196,8 +196,9 @@ github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN github.com/aws/aws-sdk-go v1.44.180 h1:VLZuAHI9fa/3WME5JjpVjcPCNfpGHVMiHx8sLHWhMgI= github.com/aws/aws-sdk-go v1.44.180/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= -github.com/aws/aws-sdk-go-v2 v1.17.3 h1:shN7NlnVzvDUgPQ+1rLMSxY8OWRNDRYtiqe0p/PgrhY= github.com/aws/aws-sdk-go-v2 v1.17.3/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw= +github.com/aws/aws-sdk-go-v2 v1.17.8 h1:GMupCNNI7FARX27L7GjCJM8NgivWbRgpjNI/hOQjFS8= +github.com/aws/aws-sdk-go-v2 v1.17.8/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10 h1:dK82zF6kkPeCo8J1e+tGx4JdvDIQzj7ygIoLg8WMuGs= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.10/go.mod h1:VeTZetY5KRJLuD/7fkQXMU6Mw7H5m/KP2J5Iy9osMno= github.com/aws/aws-sdk-go-v2/config v1.18.6/go.mod h1:qyjgnyqpKnNGT+C62zMsrZ/Mn2OodYqwIH0DpXiW8f8= @@ -210,10 +211,12 @@ github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.21 h1:j9wi1kQ8b+e0FBVHxCqCGo github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.21/go.mod h1:ugwW57Z5Z48bpvUyZuaPy4Kv+vEfJWnIrky7RmkBvJg= github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.45 h1:ckFtXy51PT613d/KLKPxFiwRqgGIxDhVbNLof6x/XLo= github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.45/go.mod h1:xar61xizdVU4pQygvQrNdZY1VCLNcOIvm87KzdZmWrE= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.27 h1:I3cakv2Uy1vNmmhRQmFptYDxOvBnwCdNwyw63N0RaRU= github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.27/go.mod h1:a1/UpzeyBBerajpnP5nGZa9mGzsBn5cOKxm6NWQsvoI= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.21 h1:5NbbMrIzmUn/TXFqAle6mgrH5m9cOvMLRGL7pnG8tRE= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.32 h1:dpbVNUjczQ8Ae3QKHbpHBpfvaVkRdesxpTOe9pTouhU= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.32/go.mod h1:RudqOgadTWdcS3t/erPQo24pcVEoYyqj/kKW5Vya21I= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.21/go.mod h1:+Gxn8jYn5k9ebfHEqlhrMirFjSW0v0C9fI+KN5vk2kE= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.26 h1:QH2kOS3Ht7x+u0gHCh06CXL/h6G8LQJFpZfFBYBNboo= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.26/go.mod h1:vq86l7956VgFr0/FWQ2BWnK07QC3WYsepKzy33qqY5U= github.com/aws/aws-sdk-go-v2/internal/ini v1.3.28 h1:KeTxcGdNnQudb46oOl4d90f2I33DF/c6q3RnZAmvQdQ= github.com/aws/aws-sdk-go-v2/internal/ini v1.3.28/go.mod h1:yRZVr/iT0AqyHeep00SZ4YfBAKojXz08w3XMBscdi0c= github.com/aws/aws-sdk-go-v2/internal/v4a v1.0.18 h1:H/mF2LNWwX00lD6FlYfKpLLZgUW7oIzCBkig78x4Xok= @@ -224,10 +227,13 @@ github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11 h1:y2+VQzC github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.9.11/go.mod h1:iV4q2hsqtNECrfmlXyord9u4zyuFEJX9eLgLpSPzWA8= github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.22 h1:kv5vRAl00tozRxSnI0IszPWGXsJOyA7hmEUHFYqsyvw= github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.1.22/go.mod h1:Od+GU5+Yx41gryN/ZGZzAJMZ9R1yn6lgA0fD5Lo5SkQ= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.21 h1:5C6XgTViSb0bunmU57b3CT+MhxULqHH2721FVA+/kDM= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.21/go.mod h1:lRToEJsn+DRA9lW4O9L9+/3hjTkUzlzyzHqn8MTds5k= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.26 h1:uUt4XctZLhl9wBE1L8lobU3bVN8SNUP7T+olb0bWBO4= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.9.26/go.mod h1:Bd4C/4PkVGubtNe5iMXu5BNnaBi/9t/UsFspPt4ram8= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.21 h1:vY5siRXvW5TrOKm2qKEf9tliBfdLxdfy0i02LOcmqUo= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.13.21/go.mod h1:WZvNXT1XuH8dnJM0HvOlvk+RNn7NbAPvA/ACO0QarSc= +github.com/aws/aws-sdk-go-v2/service/rds v1.42.3 h1:6fwUZilITdPTrgPn2rLz8sF9/GhSjrwKR/ys8K/xvUk= +github.com/aws/aws-sdk-go-v2/service/rds v1.42.3/go.mod h1:MsNKuqHhTJrmI6A0TBdhSYiQ7SYkKncIWRIp9KfzRfs= github.com/aws/aws-sdk-go-v2/service/s3 v1.29.6 h1:W8pLcSn6Uy0eXgDBUUl8M8Kxv7JCoP68ZKTD04OXLEA= github.com/aws/aws-sdk-go-v2/service/s3 v1.29.6/go.mod h1:L2l2/q76teehcW7YEsgsDjqdsDTERJeX3nOMIFlgGUE= github.com/aws/aws-sdk-go-v2/service/sso v1.11.27/go.mod h1:wo/B7uUm/7zw/dWhBJ4FXuw1sySU5lyIhVg1Bu2yL9A= @@ -300,8 +306,9 @@ github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= -github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4= +github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd v0.0.0-20190620071333-e64a0ec8b42a/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= diff --git a/lib/auth/auth.go b/lib/auth/auth.go index 8205a7d829142..65d158d1b8d02 100644 --- a/lib/auth/auth.go +++ b/lib/auth/auth.go @@ -4740,7 +4740,7 @@ func newKeySet(ctx context.Context, keyStore *keystore.Manager, caID types.CertA return keySet, trace.Wrap(err) } keySet.SSH = append(keySet.SSH, sshKeyPair) - case types.JWTSigner: + case types.JWTSigner, types.OIDCIdPCA: jwtKeyPair, err := keyStore.NewJWTKeyPair(ctx) if err != nil { return keySet, trace.Wrap(err) diff --git a/lib/auth/auth_with_roles.go b/lib/auth/auth_with_roles.go index 649c42b6f726a..4d4aaace13f42 100644 --- a/lib/auth/auth_with_roles.go +++ b/lib/auth/auth_with_roles.go @@ -416,6 +416,27 @@ func (a *ServerWithRoles) DeleteIntegration(ctx context.Context, name string) er return trace.Wrap(err) } +// GenerateAWSOIDCToken generates a token to be used when executing an AWS OIDC Integration action. +func (a *ServerWithRoles) GenerateAWSOIDCToken(ctx context.Context, req types.GenerateAWSOIDCTokenRequest) (string, error) { + igSvc, err := a.integrationsService() + if err != nil { + return "", trace.Wrap(err) + } + + if err := req.CheckAndSetDefaults(); err != nil { + return "", trace.Wrap(err) + } + + resp, err := igSvc.GenerateAWSOIDCToken(ctx, &integrationpb.GenerateAWSOIDCTokenRequest{ + Issuer: req.Issuer, + }) + if err != nil { + return "", trace.Wrap(err) + } + + return resp.Token, nil +} + // CreateSessionTracker creates a tracker resource for an active session. func (a *ServerWithRoles) CreateSessionTracker(ctx context.Context, tracker types.SessionTracker) (types.SessionTracker, error) { if err := a.serverAction(); err != nil { diff --git a/lib/auth/clt.go b/lib/auth/clt.go index 4ef5dc00343d6..49e24bccebd63 100644 --- a/lib/auth/clt.go +++ b/lib/auth/clt.go @@ -722,6 +722,9 @@ type ClientI interface { // Implements ReadAccessPoint. GetWebToken(ctx context.Context, req types.GetWebTokenRequest) (types.WebToken, error) + // GenerateAWSOIDCToken generates a token to be used to execute an AWS OIDC Integration action. + GenerateAWSOIDCToken(ctx context.Context, req types.GenerateAWSOIDCTokenRequest) (string, error) + // ResetAuthPreference resets cluster auth preference to defaults. ResetAuthPreference(ctx context.Context) error diff --git a/lib/auth/grpcserver.go b/lib/auth/grpcserver.go index 877d2bb2f9d01..9726ee09bcfc6 100644 --- a/lib/auth/grpcserver.go +++ b/lib/auth/grpcserver.go @@ -5096,6 +5096,8 @@ func NewGRPCServer(cfg GRPCServerConfig) (*GRPCServer, error) { Authorizer: cfg.Authorizer, Backend: cfg.AuthServer.Services, Cache: cfg.AuthServer.Cache, + Clock: cfg.AuthServer.clock, + CAGetter: cfg.AuthServer, }) if err != nil { return nil, trace.Wrap(err) diff --git a/lib/auth/init.go b/lib/auth/init.go index 1fc863d95bfb8..fa1bb8b0f3c4e 100644 --- a/lib/auth/init.go +++ b/lib/auth/init.go @@ -667,7 +667,7 @@ func checkResourceConsistency(ctx context.Context, keyStore *keystore.Manager, c _, signerErr = keyStore.GetSSHSigner(ctx, r) case types.DatabaseCA, types.SAMLIDPCA: _, _, signerErr = keyStore.GetTLSCertAndSigner(ctx, r) - case types.JWTSigner: + case types.JWTSigner, types.OIDCIdPCA: _, signerErr = keyStore.GetJWTSigner(ctx, r) default: return trace.BadParameter("unexpected cert_authority type %s for cluster %v", r.GetType(), clusterName) diff --git a/lib/auth/integration/integrationv1/awsoidc.go b/lib/auth/integration/integrationv1/awsoidc.go new file mode 100644 index 0000000000000..28c6a0827b8a1 --- /dev/null +++ b/lib/auth/integration/integrationv1/awsoidc.go @@ -0,0 +1,82 @@ +/* +Copyright 2023 Gravitational, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package integrationv1 + +import ( + "context" + "time" + + "github.com/gravitational/trace" + + integrationpb "github.com/gravitational/teleport/api/gen/proto/go/teleport/integration/v1" + "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/lib/authz" + "github.com/gravitational/teleport/lib/jwt" + "github.com/gravitational/teleport/lib/services" +) + +// GenerateAWSOIDCToken generates a token to be used when executing an AWS OIDC Integration action. +func (s *Service) GenerateAWSOIDCToken(ctx context.Context, req *integrationpb.GenerateAWSOIDCTokenRequest) (*integrationpb.GenerateAWSOIDCTokenResponse, error) { + _, err := authz.AuthorizeWithVerbs(ctx, s.logger, s.authorizer, true, types.KindIntegration, types.VerbUse) + if err != nil { + return nil, trace.Wrap(err) + } + + username, err := authz.GetClientUsername(ctx) + if err != nil { + return nil, trace.Wrap(err) + } + + clusterName, err := s.caGetter.GetDomainName() + if err != nil { + return nil, trace.Wrap(err) + } + + ca, err := s.caGetter.GetCertAuthority(ctx, types.CertAuthID{ + Type: types.OIDCIdPCA, + DomainName: clusterName, + }, true) + if err != nil { + return nil, trace.Wrap(err) + } + + // Extract the JWT signing key and sign the claims. + signer, err := s.caGetter.GetKeyStore().GetJWTSigner(ctx, ca) + if err != nil { + return nil, trace.Wrap(err) + } + + privateKey, err := services.GetJWTSigner(signer, ca.GetClusterName(), s.clock) + if err != nil { + return nil, trace.Wrap(err) + } + + token, err := privateKey.SignAWSOIDC(jwt.SignParams{ + Username: username, + Audience: types.IntegrationAWSOIDCAudience, + Subject: types.IntegrationAWSOIDCSubject, + Issuer: req.Issuer, + Expires: s.clock.Now().Add(time.Minute), + }) + if err != nil { + return nil, trace.Wrap(err) + } + + return &integrationpb.GenerateAWSOIDCTokenResponse{ + Token: token, + }, nil +} diff --git a/lib/auth/integration/integrationv1/awsoidc_test.go b/lib/auth/integration/integrationv1/awsoidc_test.go new file mode 100644 index 0000000000000..1d2566b5a1d1f --- /dev/null +++ b/lib/auth/integration/integrationv1/awsoidc_test.go @@ -0,0 +1,79 @@ +/* +Copyright 2023 Gravitational, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package integrationv1 + +import ( + "testing" + + "github.com/stretchr/testify/require" + + integrationv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/integration/v1" + "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/lib/defaults" + "github.com/gravitational/teleport/lib/jwt" + "github.com/gravitational/teleport/lib/utils" +) + +func TestGenerateAWSOIDCToken(t *testing.T) { + t.Parallel() + clusterName := "test-cluster" + + publicURL := "https://example.com" + + ca := newCertAuthority(t, types.HostCA, clusterName) + ctx, localClient, resourceSvc := initSvc(t, types.KindIntegration, ca, clusterName) + + ctx = authorizerForDummyUser(t, ctx, types.RoleSpecV6{ + Allow: types.RoleConditions{Rules: []types.Rule{ + {Resources: []string{types.KindIntegration}, Verbs: []string{types.VerbUse}}, + }}, + }, localClient) + + resp, err := resourceSvc.GenerateAWSOIDCToken(ctx, &integrationv1.GenerateAWSOIDCTokenRequest{ + Issuer: publicURL, + }) + require.NoError(t, err) + + // Get Public Key + require.NotEmpty(t, ca.GetActiveKeys().JWT) + jwtPubKey := ca.GetActiveKeys().JWT[0].PublicKey + + publicKey, err := utils.ParsePublicKey(jwtPubKey) + require.NoError(t, err) + + // Validate JWT against public key + key, err := jwt.New(&jwt.Config{ + Algorithm: defaults.ApplicationTokenAlgorithm, + ClusterName: clusterName, + Clock: resourceSvc.clock, + PublicKey: publicKey, + }) + require.NoError(t, err) + + _, err = key.VerifyAWSOIDC(jwt.AWSOIDCVerifyParams{ + RawToken: resp.GetToken(), + Issuer: publicURL, + }) + require.NoError(t, err) + + // Fails if the issuer is different + _, err = key.VerifyAWSOIDC(jwt.AWSOIDCVerifyParams{ + RawToken: resp.GetToken(), + Issuer: publicURL + "3", + }) + require.Error(t, err) +} diff --git a/lib/auth/integration/integrationv1/service.go b/lib/auth/integration/integrationv1/service.go index d6fa6a5b9aa49..3f9ffd56e1c6c 100644 --- a/lib/auth/integration/integrationv1/service.go +++ b/lib/auth/integration/integrationv1/service.go @@ -20,22 +20,39 @@ import ( "context" "github.com/gravitational/trace" + "github.com/jonboulle/clockwork" "github.com/sirupsen/logrus" "google.golang.org/protobuf/types/known/emptypb" integrationpb "github.com/gravitational/teleport/api/gen/proto/go/teleport/integration/v1" "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/lib/auth/keystore" "github.com/gravitational/teleport/lib/authz" "github.com/gravitational/teleport/lib/services" ) +// CAGetter describes the required methods to sign a JWT to be used for AWS OIDC Integration. +type CAGetter interface { + // GetDomainName returns local auth domain of the current auth server + GetDomainName() (string, error) + + // GetCertAuthority returns certificate authority by given id. Parameter loadSigningKeys + // controls if signing keys are loaded + GetCertAuthority(ctx context.Context, id types.CertAuthID, loadSigningKeys bool, opts ...services.MarshalOption) (types.CertAuthority, error) + + // GetKeyStore returns the KeyStore used by the auth server + GetKeyStore() *keystore.Manager +} + // ServiceConfig holds configuration options for // the Integration gRPC service. type ServiceConfig struct { Authorizer authz.Authorizer Cache services.IntegrationsGetter Backend services.Integrations + CAGetter CAGetter Logger *logrus.Entry + Clock clockwork.Clock } // CheckAndSetDefaults checks the ServiceConfig fields and returns an error if @@ -54,10 +71,18 @@ func (s *ServiceConfig) CheckAndSetDefaults() error { return trace.BadParameter("authorizer is required") } + if s.CAGetter == nil { + return trace.BadParameter("ca getter is required") + } + if s.Logger == nil { s.Logger = logrus.WithField(trace.Component, "integrations.service") } + if s.Clock == nil { + s.Clock = clockwork.NewRealClock() + } + return nil } @@ -67,7 +92,9 @@ type Service struct { authorizer authz.Authorizer cache services.IntegrationsGetter backend services.Integrations + caGetter CAGetter logger *logrus.Entry + clock clockwork.Clock } // NewService returns a new Integrations gRPC service. @@ -81,6 +108,8 @@ func NewService(cfg *ServiceConfig) (*Service, error) { authorizer: cfg.Authorizer, cache: cfg.Cache, backend: cfg.Backend, + caGetter: cfg.CAGetter, + clock: cfg.Clock, }, nil } diff --git a/lib/auth/integration/integrationv1/service_test.go b/lib/auth/integration/integrationv1/service_test.go index 70d7f103b1c73..d3ac6bb4edfe9 100644 --- a/lib/auth/integration/integrationv1/service_test.go +++ b/lib/auth/integration/integrationv1/service_test.go @@ -26,6 +26,8 @@ import ( integrationpb "github.com/gravitational/teleport/api/gen/proto/go/teleport/integration/v1" "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/lib/auth/keystore" + "github.com/gravitational/teleport/lib/auth/testauthority" "github.com/gravitational/teleport/lib/authz" "github.com/gravitational/teleport/lib/backend/memory" "github.com/gravitational/teleport/lib/services" @@ -35,7 +37,10 @@ import ( func TestIntegrationCRUD(t *testing.T) { t.Parallel() - ctx, localClient, resourceSvc := initSvc(t, types.KindIntegration) + clusterName := "test-cluster" + + ca := newCertAuthority(t, types.HostCA, clusterName) + ctx, localClient, resourceSvc := initSvc(t, types.KindIntegration, ca, clusterName) noError := func(err error) bool { return err == nil @@ -316,7 +321,7 @@ type localClient interface { CreateIntegration(ctx context.Context, ig types.Integration) (types.Integration, error) } -func initSvc(t *testing.T, kind string) (context.Context, localClient, *Service) { +func initSvc(t *testing.T, kind string, ca types.CertAuthority, clusterName string) (context.Context, localClient, *Service) { ctx := context.Background() backend, err := memory.New(memory.Config{}) require.NoError(t, err) @@ -356,7 +361,7 @@ func initSvc(t *testing.T, kind string) (context.Context, localClient, *Service) require.NoError(t, err) authorizer, err := authz.NewAuthorizer(authz.AuthorizerOpts{ - ClusterName: "test-cluster", + ClusterName: clusterName, AccessPoint: accessPoint, LockWatcher: lockWatcher, }) @@ -365,10 +370,24 @@ func initSvc(t *testing.T, kind string) (context.Context, localClient, *Service) localResourceService, err := local.NewIntegrationsService(backend) require.NoError(t, err) + keystoreManager, err := keystore.NewManager(ctx, keystore.Config{ + Software: keystore.SoftwareConfig{ + RSAKeyPairSource: testauthority.New().GenerateKeyPair, + }, + }) + require.NoError(t, err) + + caGetter := &mockCAGetter{ + domainName: clusterName, + ca: ca, + keystore: keystoreManager, + } + resourceSvc, err := NewService(&ServiceConfig{ Backend: localResourceService, Authorizer: authorizer, Cache: localResourceService, + CAGetter: caGetter, }) require.NoError(t, err) @@ -382,3 +401,49 @@ func initSvc(t *testing.T, kind string) (context.Context, localClient, *Service) IntegrationsService: localResourceService, }, resourceSvc } + +// mockCAGetter implements CAGetter. +type mockCAGetter struct { + domainName string + ca types.CertAuthority + keystore *keystore.Manager +} + +// GetDomainName returns local auth domain of the current auth server +func (m *mockCAGetter) GetDomainName() (string, error) { + return m.domainName, nil +} + +// GetCertAuthority returns certificate authority by given id. Parameter loadSigningKeys +// controls if signing keys are loaded +func (m *mockCAGetter) GetCertAuthority(ctx context.Context, id types.CertAuthID, loadSigningKeys bool, opts ...services.MarshalOption) (types.CertAuthority, error) { + return m.ca, nil +} + +// GetKeyStore returns the KeyStore used by the auth server +func (m *mockCAGetter) GetKeyStore() *keystore.Manager { + return m.keystore +} + +func newCertAuthority(t *testing.T, caType types.CertAuthType, domain string) types.CertAuthority { + t.Helper() + + ta := testauthority.New() + pub, priv, err := ta.GenerateJWT() + require.NoError(t, err) + + ca, err := types.NewCertAuthority(types.CertAuthoritySpecV2{ + Type: caType, + ClusterName: domain, + ActiveKeys: types.CAKeySet{ + JWT: []*types.JWTKeyPair{{ + PublicKey: pub, + PrivateKey: priv, + PrivateKeyType: types.PrivateKeyType_RAW, + }}, + }, + }) + require.NoError(t, err) + + return ca +} diff --git a/lib/auth/rotate.go b/lib/auth/rotate.go index 326786e9c84d8..76e591f8079d8 100644 --- a/lib/auth/rotate.go +++ b/lib/auth/rotate.go @@ -77,6 +77,8 @@ func (r *RotateRequest) Types() []types.CertAuthType { return []types.CertAuthType{types.JWTSigner} case types.SAMLIDPCA: return []types.CertAuthType{types.SAMLIDPCA} + case types.OIDCIdPCA: + return []types.CertAuthType{types.OIDCIdPCA} } return nil } diff --git a/lib/auth/tls_test.go b/lib/auth/tls_test.go index bddd953dba70f..2306cec269d9e 100644 --- a/lib/auth/tls_test.go +++ b/lib/auth/tls_test.go @@ -868,6 +868,150 @@ func TestAppTokenRotation(t *testing.T) { require.NoError(t, err) } +// TestOIDCIdPTokenRotation checks that OIDC IdP JWT tokens can be rotated and tokens can +// be validated at the appropriate phase. +func TestOIDCIdPTokenRotation(t *testing.T) { + t.Parallel() + + ctx := context.Background() + tt := setupAuthContext(ctx, t) + + clt, err := tt.server.NewClient(TestAdmin()) + require.NoError(t, err) + + user1, _, err := CreateUserAndRole(clt, "user1", nil) + require.NoError(t, err) + + client, err := tt.server.NewClient(TestUser(user1.GetName())) + require.NoError(t, err) + + // Create a JWT using the current CA, this will become the "old" CA during + // rotation. + oldJWT, err := client.GenerateAWSOIDCToken(ctx, + types.GenerateAWSOIDCTokenRequest{ + Issuer: "http://localhost:8080", + }, + ) + require.NoError(t, err) + + // Check that the "old" CA can be used to verify tokens. + oldCA, err := tt.server.Auth().GetCertAuthority(ctx, types.CertAuthID{ + DomainName: tt.server.ClusterName(), + Type: types.OIDCIdPCA, + }, true) + require.NoError(t, err) + require.Len(t, oldCA.GetTrustedJWTKeyPairs(), 1) + + // Verify that the JWT token validates with the JWT authority. + _, err = verifyJWTAWSOIDC(tt.clock, tt.server.ClusterName(), oldCA.GetTrustedJWTKeyPairs(), oldJWT) + require.NoError(t, err, tt.clock.Now()) + + // Start rotation and move to initial phase. A new CA will be added (for + // verification), but requests will continue to be signed by the old CA. + gracePeriod := time.Hour + err = tt.server.Auth().RotateCertAuthority(ctx, RotateRequest{ + Type: types.OIDCIdPCA, + GracePeriod: &gracePeriod, + TargetPhase: types.RotationPhaseInit, + Mode: types.RotationModeManual, + }) + require.NoError(t, err) + + // At this point in rotation, two JWT key pairs should exist. + oldCA, err = tt.server.Auth().GetCertAuthority(ctx, types.CertAuthID{ + DomainName: tt.server.ClusterName(), + Type: types.OIDCIdPCA, + }, true) + require.NoError(t, err) + require.Equal(t, oldCA.GetRotation().Phase, types.RotationPhaseInit) + require.Len(t, oldCA.GetTrustedJWTKeyPairs(), 2) + + // Verify that the JWT token validates with the JWT authority. + _, err = verifyJWTAWSOIDC(tt.clock, tt.server.ClusterName(), oldCA.GetTrustedJWTKeyPairs(), oldJWT) + require.NoError(t, err) + + // Move rotation into the update client phase. In this phase, requests will + // be signed by the new CA, but the old CA will be around to verify requests. + err = tt.server.Auth().RotateCertAuthority(ctx, RotateRequest{ + Type: types.OIDCIdPCA, + GracePeriod: &gracePeriod, + TargetPhase: types.RotationPhaseUpdateClients, + Mode: types.RotationModeManual, + }) + require.NoError(t, err) + + // New tokens should now fail to validate with the old key. + newJWT, err := client.GenerateAWSOIDCToken(ctx, + types.GenerateAWSOIDCTokenRequest{ + Issuer: "http://localhost:8080", + }, + ) + require.NoError(t, err) + + // New tokens will validate with the new key. + newCA, err := tt.server.Auth().GetCertAuthority(ctx, types.CertAuthID{ + DomainName: tt.server.ClusterName(), + Type: types.OIDCIdPCA, + }, true) + require.NoError(t, err) + require.Equal(t, newCA.GetRotation().Phase, types.RotationPhaseUpdateClients) + require.Len(t, newCA.GetTrustedJWTKeyPairs(), 2) + + // Both JWT should now validate. + _, err = verifyJWTAWSOIDC(tt.clock, tt.server.ClusterName(), newCA.GetTrustedJWTKeyPairs(), oldJWT) + require.NoError(t, err) + _, err = verifyJWTAWSOIDC(tt.clock, tt.server.ClusterName(), newCA.GetTrustedJWTKeyPairs(), newJWT) + require.NoError(t, err) + + // Move rotation into update servers phase. + err = tt.server.Auth().RotateCertAuthority(ctx, RotateRequest{ + Type: types.OIDCIdPCA, + GracePeriod: &gracePeriod, + TargetPhase: types.RotationPhaseUpdateServers, + Mode: types.RotationModeManual, + }) + require.NoError(t, err) + + // At this point only the phase on the CA should have changed. + newCA, err = tt.server.Auth().GetCertAuthority(ctx, types.CertAuthID{ + DomainName: tt.server.ClusterName(), + Type: types.OIDCIdPCA, + }, true) + require.NoError(t, err) + require.Equal(t, newCA.GetRotation().Phase, types.RotationPhaseUpdateServers) + require.Len(t, newCA.GetTrustedJWTKeyPairs(), 2) + + // Both JWT should continue to validate. + _, err = verifyJWTAWSOIDC(tt.clock, tt.server.ClusterName(), newCA.GetTrustedJWTKeyPairs(), oldJWT) + require.NoError(t, err) + _, err = verifyJWTAWSOIDC(tt.clock, tt.server.ClusterName(), newCA.GetTrustedJWTKeyPairs(), newJWT) + require.NoError(t, err) + + // Complete rotation. The old CA will be removed. + err = tt.server.Auth().RotateCertAuthority(ctx, RotateRequest{ + Type: types.OIDCIdPCA, + GracePeriod: &gracePeriod, + TargetPhase: types.RotationPhaseStandby, + Mode: types.RotationModeManual, + }) + require.NoError(t, err) + + // The new CA should now only have a single key. + newCA, err = tt.server.Auth().GetCertAuthority(ctx, types.CertAuthID{ + DomainName: tt.server.ClusterName(), + Type: types.OIDCIdPCA, + }, true) + require.NoError(t, err) + require.Equal(t, newCA.GetRotation().Phase, types.RotationPhaseStandby) + require.Len(t, newCA.GetTrustedJWTKeyPairs(), 1) + + // Old token should no longer validate. + _, err = verifyJWTAWSOIDC(tt.clock, tt.server.ClusterName(), newCA.GetTrustedJWTKeyPairs(), oldJWT) + require.Error(t, err) + _, err = verifyJWTAWSOIDC(tt.clock, tt.server.ClusterName(), newCA.GetTrustedJWTKeyPairs(), newJWT) + require.NoError(t, err) +} + // TestRemoteUser tests scenario when remote user connects to the local // auth server and some edge cases. func TestRemoteUser(t *testing.T) { @@ -1764,6 +1908,12 @@ func TestGetCertAuthority(t *testing.T) { }, true) require.Error(t, err) + _, err = proxyClt.GetCertAuthority(ctx, types.CertAuthID{ + DomainName: tt.server.ClusterName(), + Type: types.OIDCIdPCA, + }, true) + require.True(t, trace.IsAccessDenied(err)) + // non-admin users are not allowed to get access to private key material user, err := types.NewUser("bob") require.NoError(t, err) @@ -3594,6 +3744,39 @@ func verifyJWT(clock clockwork.Clock, clusterName string, pairs []*types.JWTKeyP return nil, trace.NewAggregate(errs...) } +// verifyJWTAWSOIDC verifies that the token was signed by one the passed in key pair. +func verifyJWTAWSOIDC(clock clockwork.Clock, clusterName string, pairs []*types.JWTKeyPair, token string) (*jwt.Claims, error) { + errs := []error{} + for _, pair := range pairs { + publicKey, err := utils.ParsePublicKey(pair.PublicKey) + if err != nil { + errs = append(errs, trace.Wrap(err)) + continue + } + + key, err := jwt.New(&jwt.Config{ + Clock: clock, + PublicKey: publicKey, + Algorithm: defaults.ApplicationTokenAlgorithm, + ClusterName: clusterName, + }) + if err != nil { + errs = append(errs, trace.Wrap(err)) + continue + } + claims, err := key.VerifyAWSOIDC(jwt.AWSOIDCVerifyParams{ + RawToken: token, + Issuer: "http://localhost:8080", + }) + if err != nil { + errs = append(errs, trace.Wrap(err)) + continue + } + return claims, nil + } + return nil, trace.NewAggregate(errs...) +} + func newTestTLSServer(t testing.TB) *TestTLSServer { as, err := NewTestAuthServer(TestAuthServerConfig{ Dir: t.TempDir(), diff --git a/lib/cloud/aws/tags_helpers.go b/lib/cloud/aws/tags_helpers.go index 1b86bcf7f3c9b..9408fb1282a91 100644 --- a/lib/cloud/aws/tags_helpers.go +++ b/lib/cloud/aws/tags_helpers.go @@ -17,6 +17,7 @@ limitations under the License. package aws import ( + rdsTypesV2 "github.com/aws/aws-sdk-go-v2/service/rds/types" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/elasticache" "github.com/aws/aws-sdk-go/service/memorydb" @@ -34,11 +35,11 @@ import ( type ResourceTag interface { // TODO Go generic does not allow access common fields yet. List all types // here and use a type switch for now. - rds.Tag | redshift.Tag | elasticache.Tag | memorydb.Tag | redshiftserverless.Tag + rdsTypesV2.Tag | *rds.Tag | *redshift.Tag | *elasticache.Tag | *memorydb.Tag | *redshiftserverless.Tag } // TagsToLabels converts a list of AWS resource tags to a label map. -func TagsToLabels[Tag ResourceTag](tags []*Tag) map[string]string { +func TagsToLabels[Tag ResourceTag](tags []Tag) map[string]string { if len(tags) == 0 { return nil } @@ -56,7 +57,7 @@ func TagsToLabels[Tag ResourceTag](tags []*Tag) map[string]string { return labels } -func resourceTagToKeyValue[Tag ResourceTag](tag *Tag) (string, string) { +func resourceTagToKeyValue[Tag ResourceTag](tag Tag) (string, string) { switch v := any(tag).(type) { case *rds.Tag: return aws.StringValue(v.Key), aws.StringValue(v.Value) @@ -68,6 +69,8 @@ func resourceTagToKeyValue[Tag ResourceTag](tag *Tag) (string, string) { return aws.StringValue(v.Key), aws.StringValue(v.Value) case *redshiftserverless.Tag: return aws.StringValue(v.Key), aws.StringValue(v.Value) + case rdsTypesV2.Tag: + return aws.StringValue(v.Key), aws.StringValue(v.Value) default: return "", "" } @@ -95,3 +98,22 @@ func LabelsToTags[T any, PT SettableTag[T]](labels map[string]string) (tags []*T } return } + +// LabelsToRDSV2Tags converts labels into [rdsTypesV2.Tag] list. +func LabelsToRDSV2Tags(labels map[string]string) []rdsTypesV2.Tag { + keys := maps.Keys(labels) + slices.Sort(keys) + + ret := make([]rdsTypesV2.Tag, 0, len(keys)) + for _, key := range keys { + key := key + value := labels[key] + + ret = append(ret, rdsTypesV2.Tag{ + Key: &key, + Value: &value, + }) + } + + return ret +} diff --git a/lib/cloud/aws/tags_helpers_test.go b/lib/cloud/aws/tags_helpers_test.go index e4fb94963236d..5ca063c561a15 100644 --- a/lib/cloud/aws/tags_helpers_test.go +++ b/lib/cloud/aws/tags_helpers_test.go @@ -19,6 +19,7 @@ package aws import ( "testing" + rdsTypesV2 "github.com/aws/aws-sdk-go-v2/service/rds/types" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/elasticache" "github.com/aws/aws-sdk-go/service/rds" @@ -28,51 +29,104 @@ import ( func TestTagsToLabels(t *testing.T) { t.Parallel() - inputTags := []*rds.Tag{ - { - Key: aws.String("Env"), - Value: aws.String("dev"), - }, - { - Key: aws.String("aws:cloudformation:stack-id"), - Value: aws.String("some-id"), - }, - { - Key: aws.String("Name"), - Value: aws.String("test"), - }, - } - t.Log(inputTags) - - expectLabels := map[string]string{ - "Name": "test", - "Env": "dev", - "aws:cloudformation:stack-id": "some-id", - } - - actuallabels := TagsToLabels(inputTags) - require.Equal(t, expectLabels, actuallabels) + t.Run("rds", func(t *testing.T) { + inputTags := []*rds.Tag{ + { + Key: aws.String("Env"), + Value: aws.String("dev"), + }, + { + Key: aws.String("aws:cloudformation:stack-id"), + Value: aws.String("some-id"), + }, + { + Key: aws.String("Name"), + Value: aws.String("test"), + }, + } + t.Log(inputTags) + + expectLabels := map[string]string{ + "Name": "test", + "Env": "dev", + "aws:cloudformation:stack-id": "some-id", + } + + actuallabels := TagsToLabels(inputTags) + require.Equal(t, expectLabels, actuallabels) + }) + + t.Run("rdsV2", func(t *testing.T) { + inputTags := []rdsTypesV2.Tag{ + { + Key: aws.String("Env"), + Value: aws.String("dev"), + }, + { + Key: aws.String("aws:cloudformation:stack-id"), + Value: aws.String("some-id"), + }, + { + Key: aws.String("Name"), + Value: aws.String("test"), + }, + } + t.Log(inputTags) + + expectLabels := map[string]string{ + "Name": "test", + "Env": "dev", + "aws:cloudformation:stack-id": "some-id", + } + + actuallabels := TagsToLabels(inputTags) + require.Equal(t, expectLabels, actuallabels) + }) + } func TestLabelsToTags(t *testing.T) { t.Parallel() - inputLabels := map[string]string{ - "labelB": "valueB", - "labelA": "valueA", - } - - expectTags := []*elasticache.Tag{ - { - Key: aws.String("labelA"), - Value: aws.String("valueA"), - }, - { - Key: aws.String("labelB"), - Value: aws.String("valueB"), - }, - } - - actualTags := LabelsToTags[elasticache.Tag](inputLabels) - require.Equal(t, expectTags, actualTags) + t.Run("elasticcache", func(t *testing.T) { + inputLabels := map[string]string{ + "labelB": "valueB", + "labelA": "valueA", + } + + expectTags := []*elasticache.Tag{ + { + Key: aws.String("labelA"), + Value: aws.String("valueA"), + }, + { + Key: aws.String("labelB"), + Value: aws.String("valueB"), + }, + } + + actualTags := LabelsToTags[elasticache.Tag](inputLabels) + require.Equal(t, expectTags, actualTags) + }) + + t.Run("rdsV2", func(t *testing.T) { + inputLabels := map[string]string{ + "labelB": "valueB", + "labelA": "valueA", + } + + expectTags := []rdsTypesV2.Tag{ + { + Key: aws.String("labelA"), + Value: aws.String("valueA"), + }, + { + Key: aws.String("labelB"), + Value: aws.String("valueB"), + }, + } + + actualTags := LabelsToRDSV2Tags(inputLabels) + require.EqualValues(t, expectTags, actualTags) + }) } diff --git a/lib/defaults/defaults.go b/lib/defaults/defaults.go index 56318f4fcbc26..ccadb64c3e969 100644 --- a/lib/defaults/defaults.go +++ b/lib/defaults/defaults.go @@ -683,6 +683,10 @@ const ( // ApplicationTokenAlgorithm is the default algorithm used to sign // application access tokens. ApplicationTokenAlgorithm = jose.RS256 + + // JWTUse is the default usage of the JWT. + // See https://www.rfc-editor.org/rfc/rfc7517#section-4.2 for more information. + JWTUse = "sig" ) var ( diff --git a/lib/integrations/awsoidc/clients.go b/lib/integrations/awsoidc/clients.go new file mode 100644 index 0000000000000..eb33d56b8d762 --- /dev/null +++ b/lib/integrations/awsoidc/clients.go @@ -0,0 +1,80 @@ +/* +Copyright 2023 Gravitational, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package awsoidc + +import ( + "context" + + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/credentials/stscreds" + "github.com/aws/aws-sdk-go-v2/service/rds" + "github.com/aws/aws-sdk-go-v2/service/sts" + "github.com/gravitational/trace" +) + +// RDSClientRequest contains the required fields to generate an Authenticated [rds.Client]. +type RDSClientRequest struct { + // Token is the token used to issue the API Call. + Token string + + // RoleARN is the IAM Role ARN to assume. + RoleARN string + + // Region where the API call should be made. + Region string +} + +// CheckAndSetDefaults checks if the required fields are present. +func (req *RDSClientRequest) CheckAndSetDefaults() error { + if req.Token == "" { + return trace.BadParameter("token is required") + } + + if req.RoleARN == "" { + return trace.BadParameter("role arn is required") + } + + if req.Region == "" { + return trace.BadParameter("region is required") + } + + return nil +} + +// NewRDSClient creates an [rds.Client] using the provided Token, RoleARN, Region and, optionally, a custom HTTP Client. +func NewRDSClient(ctx context.Context, req RDSClientRequest) (*rds.Client, error) { + cfg, err := config.LoadDefaultConfig(ctx, config.WithRegion(req.Region)) + if err != nil { + return nil, trace.Wrap(err) + } + + cfg.Credentials = stscreds.NewWebIdentityRoleProvider( + sts.NewFromConfig(cfg), + req.RoleARN, + IdentityToken(req.Token), + ) + + return rds.NewFromConfig(cfg), nil +} + +// IdentityToken is an implementation of [stscreds.IdentityTokenRetriever] for returning a static token. +type IdentityToken string + +// GetIdentityToken returns the token configured. +func (j IdentityToken) GetIdentityToken() ([]byte, error) { + return []byte(j), nil +} diff --git a/lib/integrations/awsoidc/listdatabases.go b/lib/integrations/awsoidc/listdatabases.go new file mode 100644 index 0000000000000..73b20595b9b20 --- /dev/null +++ b/lib/integrations/awsoidc/listdatabases.go @@ -0,0 +1,195 @@ +/* +Copyright 2023 Gravitational, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package awsoidc + +import ( + "context" + + "github.com/aws/aws-sdk-go-v2/service/rds" + rdsTypes "github.com/aws/aws-sdk-go-v2/service/rds/types" + "github.com/gravitational/trace" + + "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/lib/services" +) + +var ( + // filterEngine is the filter name for filtering Databses based on their engine. + filterEngine = "engine" +) + +const ( + // rdsTypeInstance identifies RDS DBs of type Instance. + rdsTypeInstance = "instance" + + // rdsTypeCluster identifies RDS DBs of type Cluster (Aurora). + rdsTypeCluster = "cluster" +) + +// ListDatabasesRequest contains the required fields to list AWS Databases. +type ListDatabasesRequest struct { + // Region is the AWS Region + Region string + // RDSType is either `instance` or `cluster`. + RDSType string + // Engines filters the returned Databases based on their engine. + // Eg, mysql, postgres, mariadb, aurora, aurora-mysql, aurora-postgresql + Engines []string + // NextToken is the token to be used to fetch the next page. + // If empty, the first page is fetched. + NextToken string +} + +// CheckAndSetDefaults checks if the required fields are present. +func (req *ListDatabasesRequest) CheckAndSetDefaults() error { + if req.Region == "" { + return trace.BadParameter("region is required") + } + + if !(req.RDSType == rdsTypeCluster || req.RDSType == rdsTypeInstance) { + return trace.BadParameter("invalid rds type, supported values: instance, cluster") + } + + if len(req.Engines) == 0 { + return trace.BadParameter("a list of engines is required") + } + + return nil +} + +// ListDatabasesResponse contains a page of AWS Databases. +type ListDatabasesResponse struct { + // Databases contains the page of Databases + Databases []types.Database + + // NextToken is used for pagination. + // If non-empty, it can be used to request the next page. + NextToken string +} + +// ListDatabasesClient describes the required methods to List Databases (Instances and Clusters) using a 3rd Party API. +type ListDatabasesClient interface { + // Returns information about provisioned RDS instances. + // This API supports pagination. + DescribeDBInstances(ctx context.Context, params *rds.DescribeDBInstancesInput, optFns ...func(*rds.Options)) (*rds.DescribeDBInstancesOutput, error) + + // Returns information about Amazon Aurora DB clusters and Multi-AZ DB clusters. + // This API supports pagination + DescribeDBClusters(ctx context.Context, params *rds.DescribeDBClustersInput, optFns ...func(*rds.Options)) (*rds.DescribeDBClustersOutput, error) +} + +// ListDatabases calls the following AWS API: +// https://docs.aws.amazon.com/AmazonRDS/latest/APIReference/API_DescribeDBClusters.html +// https://docs.aws.amazon.com/AmazonRDS/latest/APIReference/API_DescribeDBInstances.html +// It returns a list of Databases and an optional NextToken that can be used to fetch the next page +func ListDatabases(ctx context.Context, clt ListDatabasesClient, req ListDatabasesRequest) (*ListDatabasesResponse, error) { + if err := req.CheckAndSetDefaults(); err != nil { + return nil, trace.Wrap(err) + } + + // Uses https://docs.aws.amazon.com/AmazonRDS/latest/APIReference/API_DescribeDBInstances.html + if req.RDSType == rdsTypeInstance { + ret, err := listDBInstances(ctx, clt, req) + if err != nil { + return nil, trace.Wrap(err) + } + return ret, nil + } + + // Uses https://docs.aws.amazon.com/AmazonRDS/latest/APIReference/API_DescribeDBClusters.html + ret, err := listDBClusters(ctx, clt, req) + if err != nil { + return nil, trace.Wrap(err) + } + return ret, nil +} + +func listDBInstances(ctx context.Context, clt ListDatabasesClient, req ListDatabasesRequest) (*ListDatabasesResponse, error) { + describeDBInstanceInput := &rds.DescribeDBInstancesInput{ + Filters: []rdsTypes.Filter{ + {Name: &filterEngine, Values: req.Engines}, + }, + } + if req.NextToken != "" { + describeDBInstanceInput.Marker = &req.NextToken + } + + rdsDBs, err := clt.DescribeDBInstances(ctx, describeDBInstanceInput) + if err != nil { + return nil, trace.Wrap(err) + } + + ret := &ListDatabasesResponse{} + + if rdsDBs.Marker != nil && *rdsDBs.Marker != "" { + ret.NextToken = *rdsDBs.Marker + } + + ret.Databases = make([]types.Database, 0, len(rdsDBs.DBInstances)) + for _, db := range rdsDBs.DBInstances { + if !services.IsRDSInstanceAvailable(db.DBInstanceStatus, db.DBInstanceIdentifier) { + continue + } + + dbServer, err := services.NewDatabaseFromRDSV2Instance(&db) + if err != nil { + return nil, trace.Wrap(err) + } + + ret.Databases = append(ret.Databases, dbServer) + } + + return ret, nil +} + +func listDBClusters(ctx context.Context, clt ListDatabasesClient, req ListDatabasesRequest) (*ListDatabasesResponse, error) { + describeDBClusterInput := &rds.DescribeDBClustersInput{ + Filters: []rdsTypes.Filter{ + {Name: &filterEngine, Values: req.Engines}, + }, + } + if req.NextToken != "" { + describeDBClusterInput.Marker = &req.NextToken + } + + rdsDBs, err := clt.DescribeDBClusters(ctx, describeDBClusterInput) + if err != nil { + return nil, trace.Wrap(err) + } + + ret := &ListDatabasesResponse{} + + if rdsDBs.Marker != nil && *rdsDBs.Marker != "" { + ret.NextToken = *rdsDBs.Marker + } + + ret.Databases = make([]types.Database, 0, len(rdsDBs.DBClusters)) + for _, db := range rdsDBs.DBClusters { + if !services.IsRDSClusterAvailable(db.Status, db.DBClusterIdentifier) { + continue + } + + awsDB, err := services.NewDatabaseFromRDSV2Cluster(&db) + if err != nil { + return nil, trace.Wrap(err) + } + + ret.Databases = append(ret.Databases, awsDB) + } + + return ret, nil +} diff --git a/lib/integrations/awsoidc/listdatabases_test.go b/lib/integrations/awsoidc/listdatabases_test.go new file mode 100644 index 0000000000000..72f72e6dfa534 --- /dev/null +++ b/lib/integrations/awsoidc/listdatabases_test.go @@ -0,0 +1,385 @@ +/* +Copyright 2023 Gravitational, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package awsoidc + +import ( + "context" + "fmt" + "strconv" + "testing" + + "github.com/aws/aws-sdk-go-v2/service/rds" + rdsTypes "github.com/aws/aws-sdk-go-v2/service/rds/types" + "github.com/google/uuid" + "github.com/gravitational/trace" + "github.com/stretchr/testify/require" + + "github.com/gravitational/teleport/api/types" +) + +func stringPointer(s string) *string { + return &s +} + +type mockListDatabasesClient struct { + pageSize int + dbInstances []rdsTypes.DBInstance + dbClusters []rdsTypes.DBCluster +} + +// Returns information about provisioned RDS instances. +// This API supports pagination. +func (m mockListDatabasesClient) DescribeDBInstances(ctx context.Context, params *rds.DescribeDBInstancesInput, optFns ...func(*rds.Options)) (*rds.DescribeDBInstancesOutput, error) { + requestedPage := 1 + + totalInstances := len(m.dbInstances) + + if params.Marker != nil { + currentMarker, err := strconv.Atoi(*params.Marker) + if err != nil { + return nil, trace.Wrap(err) + } + requestedPage = currentMarker + } + + sliceStart := m.pageSize * (requestedPage - 1) + sliceEnd := m.pageSize * requestedPage + if sliceEnd > totalInstances { + sliceEnd = totalInstances + } + + ret := &rds.DescribeDBInstancesOutput{ + DBInstances: m.dbInstances[sliceStart:sliceEnd], + } + + if sliceEnd < totalInstances { + nextToken := fmt.Sprintf("%d", requestedPage+1) + ret.Marker = stringPointer(nextToken) + } + + return ret, nil +} + +// Returns information about Amazon Aurora DB clusters and Multi-AZ DB clusters. +// This API supports pagination +func (m mockListDatabasesClient) DescribeDBClusters(ctx context.Context, params *rds.DescribeDBClustersInput, optFns ...func(*rds.Options)) (*rds.DescribeDBClustersOutput, error) { + return &rds.DescribeDBClustersOutput{ + DBClusters: m.dbClusters, + }, nil +} + +func TestListDatabases(t *testing.T) { + ctx := context.Background() + + noErrorFunc := func(err error) bool { + return err == nil + } + + clusterPort := int32(5432) + + pageSize := 100 + t.Run("pagination", func(t *testing.T) { + totalDBs := 203 + + allInstances := make([]rdsTypes.DBInstance, 0, totalDBs) + for i := 0; i < totalDBs; i++ { + allInstances = append(allInstances, rdsTypes.DBInstance{ + DBInstanceStatus: stringPointer("available"), + DBInstanceIdentifier: stringPointer(uuid.NewString()), + DbiResourceId: stringPointer("db-123"), + DBInstanceArn: stringPointer("arn:aws:iam::123456789012:role/MyARN"), + Engine: stringPointer("postgres"), + Endpoint: &rdsTypes.Endpoint{ + Address: stringPointer("endpoint.amazonaws.com"), + Port: 5432, + }, + }) + } + + mockListClient := mockListDatabasesClient{ + pageSize: pageSize, + dbInstances: allInstances, + } + + // First page must return pageSize number of DBs + resp, err := ListDatabases(ctx, mockListClient, ListDatabasesRequest{ + Region: "us-east-1", + RDSType: "instance", + Engines: []string{"postgres"}, + NextToken: "", + }) + require.NoError(t, err) + require.NotEmpty(t, resp.NextToken) + require.Len(t, resp.Databases, pageSize) + nextPageToken := resp.NextToken + + // Second page must return pageSize number of DBs + resp, err = ListDatabases(ctx, mockListClient, ListDatabasesRequest{ + Region: "us-east-1", + RDSType: "instance", + Engines: []string{"postgres"}, + NextToken: nextPageToken, + }) + require.NoError(t, err) + require.NotEmpty(t, resp.NextToken) + require.Len(t, resp.Databases, pageSize) + nextPageToken = resp.NextToken + + // Third page must return only the remaining DBs and an empty nextToken + resp, err = ListDatabases(ctx, mockListClient, ListDatabasesRequest{ + Region: "us-east-1", + RDSType: "instance", + Engines: []string{"postgres"}, + NextToken: nextPageToken, + }) + require.NoError(t, err) + require.Empty(t, resp.NextToken) + require.Len(t, resp.Databases, 3) + }) + + for _, tt := range []struct { + name string + req ListDatabasesRequest + mockInstances []rdsTypes.DBInstance + mockClusters []rdsTypes.DBCluster + errCheck func(error) bool + respCheck func(*testing.T, *ListDatabasesResponse) + }{ + { + name: "valid for listing instances", + req: ListDatabasesRequest{ + Region: "us-east-1", + RDSType: "instance", + Engines: []string{"postgres"}, + NextToken: "", + }, + mockInstances: []rdsTypes.DBInstance{{ + DBInstanceStatus: stringPointer("available"), + DBInstanceIdentifier: stringPointer("my-db"), + Engine: stringPointer("postgres"), + DbiResourceId: stringPointer("db-123"), + DBInstanceArn: stringPointer("arn:aws:iam::123456789012:role/MyARN"), + Endpoint: &rdsTypes.Endpoint{ + Address: stringPointer("endpoint.amazonaws.com"), + Port: 5432, + }, + }, + }, + respCheck: func(t *testing.T, ldr *ListDatabasesResponse) { + require.Len(t, ldr.Databases, 1, "expected 1 database, got %d", len(ldr.Databases)) + require.Empty(t, ldr.NextToken, "expected an empty NextToken") + + expectedDB, err := types.NewDatabaseV3( + types.Metadata{ + Name: "my-db", + Description: "RDS instance in ", + Labels: map[string]string{ + "account-id": "123456789012", + "endpoint-type": "instance", + "engine": "postgres", + "engine-version": "", + "region": "", + "teleport.dev/origin": "cloud", + "status": "available", + }, + }, + types.DatabaseSpecV3{ + Protocol: "postgres", + URI: "endpoint.amazonaws.com:5432", + AWS: types.AWS{ + AccountID: "123456789012", + RDS: types.RDS{ + InstanceID: "my-db", + ResourceID: "db-123", + }, + }, + }, + ) + require.NoError(t, err) + require.Equal(t, expectedDB, ldr.Databases[0]) + }, + errCheck: noErrorFunc, + }, + { + name: "listing instances returns all valid instances and ignores the others", + req: ListDatabasesRequest{ + Region: "us-east-1", + RDSType: "instance", + Engines: []string{"postgres"}, + NextToken: "", + }, + mockInstances: []rdsTypes.DBInstance{ + { + DBInstanceStatus: stringPointer("available"), + DBInstanceIdentifier: stringPointer("my-db"), + Engine: stringPointer("postgres"), + DbiResourceId: stringPointer("db-123"), + DBInstanceArn: stringPointer("arn:aws:iam::123456789012:role/MyARN"), + Endpoint: &rdsTypes.Endpoint{ + Address: stringPointer("endpoint.amazonaws.com"), + Port: 5432, + }, + }, + { + DBInstanceStatus: stringPointer("creating"), + DBInstanceIdentifier: stringPointer("db-without-endpoint"), + Engine: stringPointer("postgres"), + DbiResourceId: stringPointer("db-123"), + DBInstanceArn: stringPointer("arn:aws:iam::123456789012:role/MyARN"), + Endpoint: nil, + }, + }, + respCheck: func(t *testing.T, ldr *ListDatabasesResponse) { + require.Len(t, ldr.Databases, 1, "expected 1 database, got %d", len(ldr.Databases)) + require.Empty(t, ldr.NextToken, "expected an empty NextToken") + + expectedDB, err := types.NewDatabaseV3( + types.Metadata{ + Name: "my-db", + Description: "RDS instance in ", + Labels: map[string]string{ + "account-id": "123456789012", + "endpoint-type": "instance", + "engine": "postgres", + "engine-version": "", + "region": "", + "teleport.dev/origin": "cloud", + "status": "available", + }, + }, + types.DatabaseSpecV3{ + Protocol: "postgres", + URI: "endpoint.amazonaws.com:5432", + AWS: types.AWS{ + AccountID: "123456789012", + RDS: types.RDS{ + InstanceID: "my-db", + ResourceID: "db-123", + }, + }, + }, + ) + require.NoError(t, err) + require.Equal(t, expectedDB, ldr.Databases[0]) + }, + errCheck: noErrorFunc, + }, + { + name: "valid for listing clusters", + req: ListDatabasesRequest{ + Region: "us-east-1", + RDSType: "cluster", + Engines: []string{"postgres"}, + NextToken: "", + }, + mockClusters: []rdsTypes.DBCluster{{ + Status: stringPointer("available"), + DBClusterIdentifier: stringPointer("my-dbc"), + DbClusterResourceId: stringPointer("db-123"), + Engine: stringPointer("aurora-postgresql"), + Endpoint: stringPointer("aurora-instance-1.abcdefghijklmnop.us-west-1.rds.amazonaws.com"), + Port: &clusterPort, + DBClusterArn: stringPointer("arn:aws:iam::123456789012:role/MyARN"), + }}, + respCheck: func(t *testing.T, ldr *ListDatabasesResponse) { + require.Len(t, ldr.Databases, 1, "expected 1 database, got %d", len(ldr.Databases)) + require.Empty(t, ldr.NextToken, "expected an empty NextToken") + expectedDB, err := types.NewDatabaseV3( + types.Metadata{ + Name: "my-dbc", + Description: "Aurora cluster in ", + Labels: map[string]string{ + "account-id": "123456789012", + "endpoint-type": "primary", + "engine": "aurora-postgresql", + "engine-version": "", + "region": "", + "teleport.dev/origin": "cloud", + "status": "available", + }, + }, + types.DatabaseSpecV3{ + Protocol: "postgres", + URI: "aurora-instance-1.abcdefghijklmnop.us-west-1.rds.amazonaws.com:5432", + AWS: types.AWS{ + AccountID: "123456789012", + RDS: types.RDS{ + ClusterID: "my-dbc", + InstanceID: "aurora-instance-1", + ResourceID: "db-123", + }, + }, + }, + ) + require.NoError(t, err) + require.Equal(t, expectedDB, ldr.Databases[0]) + }, + errCheck: noErrorFunc, + }, + { + name: "no region", + req: ListDatabasesRequest{ + Region: "", + RDSType: "instance", + Engines: []string{"postgres"}, + NextToken: "", + }, + errCheck: func(err error) bool { + return trace.IsBadParameter(err) + }, + }, + { + name: "invalid rds type", + req: ListDatabasesRequest{ + Region: "us-east-1", + RDSType: "aurora", + Engines: []string{"postgres"}, + NextToken: "", + }, + errCheck: func(err error) bool { + return trace.IsBadParameter(err) + }, + }, + { + name: "empty engines list", + req: ListDatabasesRequest{ + Region: "us-east-1", + RDSType: "instance", + Engines: []string{}, + NextToken: "", + }, + errCheck: func(err error) bool { + return trace.IsBadParameter(err) + }, + }, + } { + t.Run(tt.name, func(t *testing.T) { + mockListClient := mockListDatabasesClient{ + pageSize: pageSize, + dbInstances: tt.mockInstances, + dbClusters: tt.mockClusters, + } + resp, err := ListDatabases(ctx, mockListClient, tt.req) + require.True(t, tt.errCheck(err), "unexpected err: %v", err) + if err != nil { + return + } + + tt.respCheck(t, resp) + }) + } +} diff --git a/lib/jwt/jwk.go b/lib/jwt/jwk.go index 4a07fb57eb332..6df079b9d45af 100644 --- a/lib/jwt/jwk.go +++ b/lib/jwt/jwk.go @@ -38,6 +38,14 @@ type JWK struct { N string `json:"n"` // E is the exponent of the public key. E string `json:"e"` + // Use identifies the intended use of the public key. + // This field is required for the AWS OIDC Integration. + // https://www.rfc-editor.org/rfc/rfc7517#section-4.2 + Use string `json:"use"` + // KeyID identifies the key to use. + // This field is required (even if empty) for the AWS OIDC Integration. + // https://www.rfc-editor.org/rfc/rfc7517#section-4.5 + KeyID string `json:"kid"` } // MarshalJWK will marshal a supported public key into JWK format. @@ -58,6 +66,8 @@ func MarshalJWK(bytes []byte) (JWK, error) { Algorithm: string(defaults.ApplicationTokenAlgorithm), N: base64.RawURLEncoding.EncodeToString(publicKey.N.Bytes()), E: base64.RawURLEncoding.EncodeToString(big.NewInt(int64(publicKey.E)).Bytes()), + Use: defaults.JWTUse, + KeyID: "", }, nil } diff --git a/lib/jwt/jwk_test.go b/lib/jwt/jwk_test.go new file mode 100644 index 0000000000000..43d3f62f4e4e2 --- /dev/null +++ b/lib/jwt/jwk_test.go @@ -0,0 +1,34 @@ +/* +Copyright 2022 Gravitational, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package jwt + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestMarshalJWK(t *testing.T) { + pubBytes, _, err := GenerateKeyPair() + require.NoError(t, err) + + jwk, err := MarshalJWK(pubBytes) + require.NoError(t, err) + + // Required for integrating with AWS OpenID Connect Identity Provider. + require.Equal(t, jwk.Use, "sig") +} diff --git a/lib/jwt/jwt.go b/lib/jwt/jwt.go index 89a0a3a48bd6f..483448864846e 100644 --- a/lib/jwt/jwt.go +++ b/lib/jwt/jwt.go @@ -28,6 +28,7 @@ import ( "strings" "time" + "github.com/google/uuid" "github.com/gravitational/trace" "github.com/jonboulle/clockwork" "gopkg.in/square/go-jose.v2" @@ -35,6 +36,7 @@ import ( "gopkg.in/square/go-jose.v2/jwt" "github.com/gravitational/teleport/api/constants" + "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/api/types/wrappers" "github.com/gravitational/teleport/lib/utils" ) @@ -112,6 +114,15 @@ type SignParams struct { // URI is the URI of the recipient application. URI string + + // Audience is the Audience for the Token. + Audience string + + // Issuer is the issuer of the token. + Issuer string + + // Subject is the system that is going to use the token. + Subject string } // Check verifies all the values are valid. @@ -133,7 +144,7 @@ func (p *SignParams) Check() error { } // sign will return a signed JWT with the passed in claims embedded within. -func (k *Key) sign(claims Claims) (string, error) { +func (k *Key) sign(claims any) (string, error) { return k.signAny(claims) } @@ -190,6 +201,39 @@ func (k *Key) Sign(p SignParams) (string, error) { return k.sign(claims) } +// awsOIDCCustomClaims defines the require claims for the JWT token used in AWS OIDC Integration. +type awsOIDCCustomClaims struct { + jwt.Claims + + // OnBehalfOf identifies the user that is started the request. + OnBehalfOf string `json:"obo,omitempty"` +} + +// SignAWSOIDC signs a JWT with claims specific to AWS OIDC Integration. +// Required Params: +// - Username: stored as OnBehalfOf (obo) claim with `user:` prefix +// - Issuer: stored as Issuer (iss) claim +// - Subject: stored as Subject (sub) claim +// - Audience: stored as Audience (aud) claim +// - Expiries: stored as Expiry (exp) claim +func (k *Key) SignAWSOIDC(p SignParams) (string, error) { + // Sign the claims and create a JWT token. + claims := awsOIDCCustomClaims{ + OnBehalfOf: "user:" + p.Username, + Claims: jwt.Claims{ + Issuer: p.Issuer, + Subject: p.Subject, + Audience: jwt.Audience{p.Audience}, + ID: uuid.NewString(), + NotBefore: jwt.NewNumericDate(k.config.Clock.Now().Add(-10 * time.Second)), + Expiry: jwt.NewNumericDate(p.Expires), + IssuedAt: jwt.NewNumericDate(k.config.Clock.Now().Add(-10 * time.Second)), + }, + } + + return k.sign(claims) +} + func (k *Key) SignSnowflake(p SignParams, issuer string) (string, error) { // Sign the claims and create a JWT token. claims := Claims{ @@ -252,6 +296,9 @@ type VerifyParams struct { // URI is the URI of the recipient application. URI string + + // Audience is the Audience for the token + Audience string } // Check verifies all the values are valid. @@ -349,6 +396,41 @@ func (k *Key) Verify(p VerifyParams) (*Claims, error) { return k.verify(p.RawToken, expectedClaims) } +// AWSOIDCVerifyParams are the params required to verify an AWS OIDC Token. +type AWSOIDCVerifyParams struct { + RawToken string + Issuer string +} + +// Check ensures all the required fields are present. +func (p *AWSOIDCVerifyParams) Check() error { + if p.RawToken == "" { + return trace.BadParameter("raw token is missing") + } + + if p.Issuer == "" { + return trace.BadParameter("issuer is missing") + } + + return nil +} + +// VerifyAWSOIDC will validate the passed in JWT token for the AWS OIDC Integration +func (k *Key) VerifyAWSOIDC(p AWSOIDCVerifyParams) (*Claims, error) { + if err := p.Check(); err != nil { + return nil, trace.Wrap(err) + } + + expectedClaims := jwt.Expected{ + Issuer: p.Issuer, + Subject: types.IntegrationAWSOIDCSubject, + Audience: jwt.Audience{types.IntegrationAWSOIDCAudience}, + Time: k.config.Clock.Now(), + } + + return k.verify(p.RawToken, expectedClaims) +} + // VerifyPROXY will validate the passed JWT for signed PROXY header func (k *Key) VerifyPROXY(p PROXYVerifyParams) (*Claims, error) { if err := p.Check(); err != nil { diff --git a/lib/jwt/jwt_test.go b/lib/jwt/jwt_test.go index 576a58d7a9bde..b1a6347071617 100644 --- a/lib/jwt/jwt_test.go +++ b/lib/jwt/jwt_test.go @@ -245,6 +245,69 @@ func TestKey_SignAndVerifyPROXY(t *testing.T) { require.ErrorContains(t, err, "token is expired") } +func TestKey_SignAndVerifyAWSOIDC(t *testing.T) { + _, privateBytes, err := GenerateKeyPair() + require.NoError(t, err) + privateKey, err := utils.ParsePrivateKey(privateBytes) + require.NoError(t, err) + + clock := clockwork.NewFakeClockAt(time.Now()) + const clusterName = "teleport-test" + + // Create a new key that can sign and verify tokens. + key, err := New(&Config{ + PrivateKey: privateKey, + Algorithm: defaults.ApplicationTokenAlgorithm, + ClusterName: clusterName, + Clock: clock, + }) + require.NoError(t, err) + + // Sign a token with the new key. + expiresIn := time.Minute * 5 + token, err := key.SignAWSOIDC(SignParams{ + Username: "user", + Issuer: "https://localhost/", + URI: "https://localhost/", + Subject: "system:proxy", + Audience: "discover.teleport", + Expires: clock.Now().Add(expiresIn), + }) + require.NoError(t, err) + + // Successfully verify + _, err = key.VerifyAWSOIDC(AWSOIDCVerifyParams{ + RawToken: token, + Issuer: "https://localhost/", + }) + require.NoError(t, err, token) + + // Check that if params don't match verification fails + _, err = key.VerifyAWSOIDC(AWSOIDCVerifyParams{ + RawToken: token, + Issuer: "https://localhost/" + "1", + }) + require.ErrorContains(t, err, "invalid issuer") + + // Rewind clock backward and verify that token is not valid yet + clock.Advance(time.Minute * -2) + _, err = key.VerifyAWSOIDC(AWSOIDCVerifyParams{ + RawToken: token, + Issuer: "https://localhost/", + }) + require.ErrorContains(t, err, "token not valid yet") + // Revert time to before this sub-test. + clock.Advance(time.Minute * 2) + + // Advance clock and verify that token is expired now + clock.Advance(expiresIn + time.Minute) + _, err = key.VerifyAWSOIDC(AWSOIDCVerifyParams{ + RawToken: token, + Issuer: "https://localhost/", + }) + require.ErrorContains(t, err, "token is expired") +} + // TestExpiry checks that token expiration works. func TestExpiry(t *testing.T) { _, privateBytes, err := GenerateKeyPair() diff --git a/lib/services/authority.go b/lib/services/authority.go index 14c8e84086cf2..0167895dbecb5 100644 --- a/lib/services/authority.go +++ b/lib/services/authority.go @@ -71,7 +71,7 @@ func ValidateCertAuthority(ca types.CertAuthority) (err error) { err = checkDatabaseCA(ca) case types.OpenSSHCA: err = checkOpenSSHCA(ca) - case types.JWTSigner: + case types.JWTSigner, types.OIDCIdPCA: err = checkJWTKeys(ca) case types.SAMLIDPCA: err = checkSAMLIDPCA(ca) diff --git a/lib/services/authority_test.go b/lib/services/authority_test.go index cf9bd15248906..10873f23ddfe0 100644 --- a/lib/services/authority_test.go +++ b/lib/services/authority_test.go @@ -282,6 +282,96 @@ func TestCheckSAMLIDPCA(t *testing.T) { } } +func TestCheckOIDCIdP(t *testing.T) { + ta := testauthority.New() + + pub, priv, err := ta.GenerateJWT() + require.NoError(t, err) + + pub2, priv2, err := ta.GenerateJWT() + require.NoError(t, err) + + tests := []struct { + name string + keyset types.CAKeySet + errAssertionFunc require.ErrorAssertionFunc + }{ + { + name: "no active keys", + keyset: types.CAKeySet{}, + errAssertionFunc: require.Error, + }, + { + name: "multiple active keys", + keyset: types.CAKeySet{ + JWT: []*types.JWTKeyPair{ + { + PublicKey: pub, + PrivateKey: priv, + }, + { + PublicKey: pub2, + PrivateKey: priv2, + }, + }, + }, + errAssertionFunc: require.NoError, + }, + { + name: "empty private key", + keyset: types.CAKeySet{ + JWT: []*types.JWTKeyPair{{ + PublicKey: pub, + PrivateKey: []byte{}, + }}, + }, + errAssertionFunc: require.NoError, + }, + { + name: "unparseable private key", + keyset: types.CAKeySet{ + JWT: []*types.JWTKeyPair{{ + PublicKey: pub, + PrivateKey: bytes.Repeat([]byte{49}, 1222), + }}, + }, + errAssertionFunc: require.Error, + }, + { + name: "unparseable public key", + keyset: types.CAKeySet{ + JWT: []*types.JWTKeyPair{{ + PublicKey: bytes.Repeat([]byte{49}, 1222), + PrivateKey: priv, + }}, + }, + errAssertionFunc: require.Error, + }, + { + name: "valid key pair", + keyset: types.CAKeySet{ + JWT: []*types.JWTKeyPair{{ + PublicKey: pub, + PrivateKey: priv, + }}, + }, + errAssertionFunc: require.NoError, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + ca, err := types.NewCertAuthority(types.CertAuthoritySpecV2{ + Type: types.OIDCIdPCA, + ClusterName: "cluster1", + ActiveKeys: test.keyset, + }) + require.NoError(t, err) + test.errAssertionFunc(t, ValidateCertAuthority(ca)) + }) + } +} + func BenchmarkCertAuthoritiesEquivalent(b *testing.B) { ca1, err := types.NewCertAuthority(types.CertAuthoritySpecV2{ Type: types.HostCA, diff --git a/lib/services/database.go b/lib/services/database.go index 9b8149d6e102e..8b02948496898 100644 --- a/lib/services/database.go +++ b/lib/services/database.go @@ -29,6 +29,7 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/redis/armredis/v2" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/redisenterprise/armredisenterprise" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/sql/armsql" + rdsTypesV2 "github.com/aws/aws-sdk-go-v2/service/rds/types" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/arn" "github.com/aws/aws-sdk-go/service/elasticache" @@ -530,6 +531,126 @@ func NewDatabaseFromRDSInstance(instance *rds.DBInstance) (types.Database, error }) } +// NewDatabaseFromRDSV2Instance creates a database resource from an RDS instance. +// It uses aws sdk v2. +func NewDatabaseFromRDSV2Instance(instance *rdsTypesV2.DBInstance) (types.Database, error) { + endpoint := instance.Endpoint + if endpoint == nil { + return nil, trace.BadParameter("empty endpoint") + } + metadata, err := MetadataFromRDSV2Instance(instance) + if err != nil { + return nil, trace.Wrap(err) + } + protocol, err := rdsEngineToProtocol(aws.StringValue(instance.Engine)) + if err != nil { + return nil, trace.Wrap(err) + } + + uri := "" + if instance.Endpoint != nil && instance.Endpoint.Address != nil { + uri = fmt.Sprintf("%s:%d", aws.StringValue(instance.Endpoint.Address), instance.Endpoint.Port) + } + + return types.NewDatabaseV3( + setDBName(types.Metadata{ + Description: fmt.Sprintf("RDS instance in %v", metadata.Region), + Labels: labelsFromRDSV2Instance(instance, metadata), + }, aws.StringValue(instance.DBInstanceIdentifier)), + types.DatabaseSpecV3{ + Protocol: protocol, + URI: uri, + AWS: *metadata, + }) +} + +// MetadataFromRDSInstance creates AWS metadata from the provided RDS instance. +// It uses aws sdk v2. +func MetadataFromRDSV2Instance(rdsInstance *rdsTypesV2.DBInstance) (*types.AWS, error) { + parsedARN, err := arn.Parse(aws.StringValue(rdsInstance.DBInstanceArn)) + if err != nil { + return nil, trace.Wrap(err) + } + return &types.AWS{ + Region: parsedARN.Region, + AccountID: parsedARN.AccountID, + RDS: types.RDS{ + InstanceID: aws.StringValue(rdsInstance.DBInstanceIdentifier), + ClusterID: aws.StringValue(rdsInstance.DBClusterIdentifier), + ResourceID: aws.StringValue(rdsInstance.DbiResourceId), + IAMAuth: rdsInstance.IAMDatabaseAuthenticationEnabled, + }, + }, nil +} + +// labelsFromRDSV2Instance creates database labels for the provided RDS instance. +// It uses aws sdk v2. +func labelsFromRDSV2Instance(rdsInstance *rdsTypesV2.DBInstance, meta *types.AWS) map[string]string { + labels := labelsFromAWSMetadata(meta) + labels[labelEngine] = aws.StringValue(rdsInstance.Engine) + labels[labelEngineVersion] = aws.StringValue(rdsInstance.EngineVersion) + labels[labelEndpointType] = string(RDSEndpointTypeInstance) + labels[labelStatus] = aws.StringValue(rdsInstance.DBInstanceStatus) + return addLabels(labels, libcloudaws.TagsToLabels(rdsInstance.TagList)) +} + +// NewDatabaseFromRDSV2Cluster creates a database resource from an RDS cluster (Aurora). +// It uses aws sdk v2. +func NewDatabaseFromRDSV2Cluster(cluster *rdsTypesV2.DBCluster) (types.Database, error) { + metadata, err := MetadataFromRDSV2Cluster(cluster) + if err != nil { + return nil, trace.Wrap(err) + } + protocol, err := rdsEngineToProtocol(aws.StringValue(cluster.Engine)) + if err != nil { + return nil, trace.Wrap(err) + } + + uri := "" + if cluster.Endpoint != nil && cluster.Port != nil { + uri = fmt.Sprintf("%v:%v", aws.StringValue(cluster.Endpoint), *cluster.Port) + } + return types.NewDatabaseV3( + setDBName(types.Metadata{ + Description: fmt.Sprintf("Aurora cluster in %v", metadata.Region), + Labels: labelsFromRDSV2Cluster(cluster, metadata, RDSEndpointTypePrimary), + }, aws.StringValue(cluster.DBClusterIdentifier)), + types.DatabaseSpecV3{ + Protocol: protocol, + URI: uri, + AWS: *metadata, + }) +} + +// MetadataFromRDSV2Cluster creates AWS metadata from the provided RDS cluster. +// It uses aws sdk v2. +func MetadataFromRDSV2Cluster(rdsCluster *rdsTypesV2.DBCluster) (*types.AWS, error) { + parsedARN, err := arn.Parse(aws.StringValue(rdsCluster.DBClusterArn)) + if err != nil { + return nil, trace.Wrap(err) + } + return &types.AWS{ + Region: parsedARN.Region, + AccountID: parsedARN.AccountID, + RDS: types.RDS{ + ClusterID: aws.StringValue(rdsCluster.DBClusterIdentifier), + ResourceID: aws.StringValue(rdsCluster.DbClusterResourceId), + IAMAuth: aws.BoolValue(rdsCluster.IAMDatabaseAuthenticationEnabled), + }, + }, nil +} + +// labelsFromRDSV2Cluster creates database labels for the provided RDS cluster. +// It uses aws sdk v2. +func labelsFromRDSV2Cluster(rdsCluster *rdsTypesV2.DBCluster, meta *types.AWS, endpointType RDSEndpointType) map[string]string { + labels := labelsFromAWSMetadata(meta) + labels[labelEngine] = aws.StringValue(rdsCluster.Engine) + labels[labelEngineVersion] = aws.StringValue(rdsCluster.EngineVersion) + labels[labelEndpointType] = string(endpointType) + labels[labelStatus] = aws.StringValue(rdsCluster.Status) + return addLabels(labels, libcloudaws.TagsToLabels(rdsCluster.TagList)) +} + // NewDatabaseFromRDSCluster creates a database resource from an RDS cluster (Aurora). func NewDatabaseFromRDSCluster(cluster *rds.DBCluster) (types.Database, error) { metadata, err := MetadataFromRDSCluster(cluster) @@ -1334,10 +1455,10 @@ func IsMemoryDBClusterSupported(cluster *memorydb.Cluster) bool { } // IsRDSInstanceAvailable checks if the RDS instance is available. -func IsRDSInstanceAvailable(instance *rds.DBInstance) bool { +func IsRDSInstanceAvailable(instanceStatus, instanceIdentifier *string) bool { // For a full list of status values, see: // https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/accessing-monitoring.html - switch aws.StringValue(instance.DBInstanceStatus) { + switch aws.StringValue(instanceStatus) { // Statuses marked as "Billed" in the above guide. case "available", "backing-up", "configuring-enhanced-monitoring", "configuring-iam-database-auth", "configuring-log-exports", @@ -1365,18 +1486,18 @@ func IsRDSInstanceAvailable(instance *rds.DBInstance) bool { default: log.Warnf("Unknown status type: %q. Assuming RDS instance %q is available.", - aws.StringValue(instance.DBInstanceStatus), - aws.StringValue(instance.DBInstanceIdentifier), + aws.StringValue(instanceStatus), + aws.StringValue(instanceIdentifier), ) return true } } // IsRDSClusterAvailable checks if the RDS cluster is available. -func IsRDSClusterAvailable(cluster *rds.DBCluster) bool { +func IsRDSClusterAvailable(clusterStatus, clusterIndetifier *string) bool { // For a full list of status values, see: // https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/accessing-monitoring.html - switch aws.StringValue(cluster.Status) { + switch aws.StringValue(clusterStatus) { // Statuses marked as "Billed" in the above guide. case "available", "backing-up", "backtracking", "failing-over", "maintenance", "migrating", "modifying", "promoting", "renaming", @@ -1394,8 +1515,8 @@ func IsRDSClusterAvailable(cluster *rds.DBCluster) bool { default: log.Warnf("Unknown status type: %q. Assuming Aurora cluster %q is available.", - aws.StringValue(cluster.Status), - aws.StringValue(cluster.DBClusterIdentifier), + aws.StringValue(clusterStatus), + aws.StringValue(clusterIndetifier), ) return true } @@ -1569,6 +1690,8 @@ const ( // labelSourceServer is the source server for replica Azure DB Flexible servers. // This is the source (primary) database resource name. labelSourceServer = "source-server" + // labelStatus is the label key containing the database status, e.g. "available" + labelStatus = "status" ) const ( diff --git a/lib/services/database_test.go b/lib/services/database_test.go index 165c1dd28f54e..66af57a0062fc 100644 --- a/lib/services/database_test.go +++ b/lib/services/database_test.go @@ -29,6 +29,7 @@ import ( "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/redis/armredis/v2" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/redisenterprise/armredisenterprise" "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/sql/armsql" + rdsTypesV2 "github.com/aws/aws-sdk-go-v2/service/rds/types" "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/service/elasticache" "github.com/aws/aws-sdk-go/service/memorydb" @@ -510,6 +511,75 @@ func TestDatabaseFromRDSInstance(t *testing.T) { require.Empty(t, cmp.Diff(expected, actual)) } +// TestDatabaseFromRDSV2Instance tests converting an RDS instance (from aws sdk v2/rds) to a database resource. +func TestDatabaseFromRDSV2Instance(t *testing.T) { + instance := &rdsTypesV2.DBInstance{ + DBInstanceArn: aws.String("arn:aws:rds:us-west-1:123456789012:db:instance-1"), + DBInstanceIdentifier: aws.String("instance-1"), + DBClusterIdentifier: aws.String("cluster-1"), + DBInstanceStatus: aws.String("available"), + DbiResourceId: aws.String("resource-1"), + IAMDatabaseAuthenticationEnabled: true, + Engine: aws.String(RDSEnginePostgres), + EngineVersion: aws.String("13.0"), + Endpoint: &rdsTypesV2.Endpoint{ + Address: aws.String("localhost"), + Port: 5432, + }, + TagList: []rdsTypesV2.Tag{{ + Key: aws.String("key"), + Value: aws.String("val"), + }}, + } + expected, err := types.NewDatabaseV3(types.Metadata{ + Name: "instance-1", + Description: "RDS instance in us-west-1", + Labels: map[string]string{ + types.OriginLabel: types.OriginCloud, + labelAccountID: "123456789012", + labelRegion: "us-west-1", + labelEngine: RDSEnginePostgres, + labelEngineVersion: "13.0", + labelEndpointType: "instance", + labelStatus: "available", + "key": "val", + }, + }, types.DatabaseSpecV3{ + Protocol: defaults.ProtocolPostgres, + URI: "localhost:5432", + AWS: types.AWS{ + AccountID: "123456789012", + Region: "us-west-1", + RDS: types.RDS{ + InstanceID: "instance-1", + ClusterID: "cluster-1", + ResourceID: "resource-1", + IAMAuth: true, + }, + }, + }) + require.NoError(t, err) + actual, err := NewDatabaseFromRDSV2Instance(instance) + require.NoError(t, err) + require.Empty(t, cmp.Diff(expected, actual)) + + t.Run("with name override", func(t *testing.T) { + newName := "override-1" + + instance.TagList = append(instance.TagList, + rdsTypesV2.Tag{ + Key: aws.String(labelTeleportDBName), + Value: aws.String(newName), + }, + ) + expected.Metadata.Name = newName + + actual, err := NewDatabaseFromRDSV2Instance(instance) + require.NoError(t, err) + require.Equal(t, actual.GetName(), newName) + }) +} + // TestDatabaseFromRDSInstance tests converting an RDS instance to a database resource. func TestDatabaseFromRDSInstanceNameOverride(t *testing.T) { instance := &rds.DBInstance{ @@ -697,6 +767,82 @@ func TestDatabaseFromRDSCluster(t *testing.T) { }) } +// TestDatabaseFromRDSV2Cluster tests converting an RDS cluster to a database resource. +// It uses the V2 of the aws sdk. +func TestDatabaseFromRDSV2Cluster(t *testing.T) { + cluster := &rdsTypesV2.DBCluster{ + DBClusterArn: aws.String("arn:aws:rds:us-east-1:123456789012:cluster:cluster-1"), + DBClusterIdentifier: aws.String("cluster-1"), + DbClusterResourceId: aws.String("resource-1"), + IAMDatabaseAuthenticationEnabled: aws.Bool(true), + Engine: aws.String(RDSEngineAuroraMySQL), + EngineVersion: aws.String("8.0.0"), + Status: aws.String("available"), + Endpoint: aws.String("localhost"), + ReaderEndpoint: aws.String("reader.host"), + Port: aws.Int32(3306), + CustomEndpoints: []string{ + "myendpoint1.cluster-custom-example.us-east-1.rds.amazonaws.com", + "myendpoint2.cluster-custom-example.us-east-1.rds.amazonaws.com", + }, + TagList: []rdsTypesV2.Tag{{ + Key: aws.String("key"), + Value: aws.String("val"), + }}, + } + + expectedAWS := types.AWS{ + AccountID: "123456789012", + Region: "us-east-1", + RDS: types.RDS{ + ClusterID: "cluster-1", + ResourceID: "resource-1", + IAMAuth: true, + }, + } + + t.Run("primary", func(t *testing.T) { + expected, err := types.NewDatabaseV3(types.Metadata{ + Name: "cluster-1", + Description: "Aurora cluster in us-east-1", + Labels: map[string]string{ + types.OriginLabel: types.OriginCloud, + labelAccountID: "123456789012", + labelRegion: "us-east-1", + labelEngine: RDSEngineAuroraMySQL, + labelEngineVersion: "8.0.0", + labelEndpointType: "primary", + labelStatus: "available", + "key": "val", + }, + }, types.DatabaseSpecV3{ + Protocol: defaults.ProtocolMySQL, + URI: "localhost:3306", + AWS: expectedAWS, + }) + require.NoError(t, err) + actual, err := NewDatabaseFromRDSV2Cluster(cluster) + require.NoError(t, err) + require.Empty(t, cmp.Diff(expected, actual)) + + t.Run("with name override", func(t *testing.T) { + newName := "override-1" + + cluster.TagList = append(cluster.TagList, + rdsTypesV2.Tag{ + Key: aws.String(labelTeleportDBName), + Value: aws.String(newName), + }, + ) + expected.Metadata.Name = newName + + actual, err := NewDatabaseFromRDSV2Cluster(cluster) + require.NoError(t, err) + require.Equal(t, actual.GetName(), newName) + }) + }) +} + // TestDatabaseFromRDSClusterNameOverride tests converting an RDS cluster to a database resource with overridden name. func TestDatabaseFromRDSClusterNameOverride(t *testing.T) { cluster := &rds.DBCluster{ diff --git a/lib/services/role.go b/lib/services/role.go index 0f49568f75118..9d60f18ed6ff1 100644 --- a/lib/services/role.go +++ b/lib/services/role.go @@ -170,6 +170,7 @@ func RoleForUser(u types.User) types.Role { types.NewRule(types.KindKubernetesCluster, RW()), types.NewRule(types.KindSessionTracker, RO()), types.NewRule(types.KindUserGroup, RW()), + types.NewRule(types.KindIntegration, []string{types.VerbUse}), }, JoinSessions: []*types.SessionJoinPolicy{ { diff --git a/lib/services/suite/suite.go b/lib/services/suite/suite.go index 60a5d801f0b12..a7261e85caaa6 100644 --- a/lib/services/suite/suite.go +++ b/lib/services/suite/suite.go @@ -117,7 +117,7 @@ func NewTestCAWithConfig(config TestCAConfig) *types.CertAuthorityV2 { switch config.Type { case types.DatabaseCA: ca.Spec.ActiveKeys.TLS = []*types.TLSKeyPair{{Cert: cert, Key: keyBytes}} - case types.KindJWT: + case types.KindJWT, types.OIDCIdPCA: // Generating keys is CPU intensive operation. Generate JWT keys only // when needed. publicKey, privateKey, err := testauthority.New().GenerateJWT() diff --git a/lib/srv/discovery/fetchers/db/aws_rds.go b/lib/srv/discovery/fetchers/db/aws_rds.go index f59e393d9fcea..5808fb6c69073 100644 --- a/lib/srv/discovery/fetchers/db/aws_rds.go +++ b/lib/srv/discovery/fetchers/db/aws_rds.go @@ -106,7 +106,7 @@ func (f *rdsDBInstancesFetcher) getRDSDatabases(ctx context.Context) (types.Data continue } - if !services.IsRDSInstanceAvailable(instance) { + if !services.IsRDSInstanceAvailable(instance.DBInstanceStatus, instance.DBInstanceIdentifier) { f.log.Debugf("The current status of RDS instance %q is %q. Skipping.", aws.StringValue(instance.DBInstanceIdentifier), aws.StringValue(instance.DBInstanceStatus)) @@ -202,7 +202,7 @@ func (f *rdsAuroraClustersFetcher) getAuroraDatabases(ctx context.Context) (type continue } - if !services.IsRDSClusterAvailable(cluster) { + if !services.IsRDSClusterAvailable(cluster.Status, cluster.DBClusterIdentifier) { f.log.Debugf("The current status of Aurora cluster %q is %q. Skipping.", aws.StringValue(cluster.DBClusterIdentifier), aws.StringValue(cluster.Status)) diff --git a/lib/web/apiserver.go b/lib/web/apiserver.go index 35588626c9115..de873df3ab444 100644 --- a/lib/web/apiserver.go +++ b/lib/web/apiserver.go @@ -711,6 +711,13 @@ func (h *Handler) bindDefaultEndpoints() { h.PUT("/webapi/sites/:site/integrations/:name", h.WithClusterAuth(h.integrationsUpdate)) h.DELETE("/webapi/sites/:site/integrations/:name", h.WithClusterAuth(h.integrationsDelete)) + h.POST("/webapi/sites/:site/integrations/aws-oidc/:name/databases", h.WithClusterAuth(h.awsOIDCListDatabases)) + + // AWS OIDC Integration specific endpoints: + // Unauthenticated access to OpenID Configuration - used for AWS OIDC IdP integration + h.GET("/.well-known/openid-configuration", h.WithLimiter(h.openidConfiguration)) + h.GET(OIDCJWKWURI, h.WithLimiter(h.jwksOIDC)) + // Connection upgrades. h.GET("/webapi/connectionupgrade", httplib.MakeHandler(h.connectionUpgrade)) diff --git a/lib/web/apiserver_test.go b/lib/web/apiserver_test.go index fdbd20c300761..5f05941202c01 100644 --- a/lib/web/apiserver_test.go +++ b/lib/web/apiserver_test.go @@ -3444,6 +3444,8 @@ func authExportTestByEndpoint(t *testing.T, endpointExport, authType string, exp } func TestClusterDatabasesGet(t *testing.T) { + t.Parallel() + env := newWebPack(t, 1) proxy := env.proxies[0] @@ -3479,7 +3481,7 @@ func TestClusterDatabasesGet(t *testing.T) { }, Spec: types.DatabaseSpecV3{ Protocol: "test-protocol", - URI: "test-uri", + URI: "test-uri:1234", }, }, }) @@ -3521,12 +3523,14 @@ func TestClusterDatabasesGet(t *testing.T) { Type: types.DatabaseTypeSelfHosted, Labels: []ui.Label{{Name: "test-field", Value: "test-value"}}, Hostname: "test-uri", + URI: "test-uri:1234", }, { Name: "db2", Type: types.DatabaseTypeSelfHosted, Labels: []ui.Label{}, Protocol: "test-protocol", Hostname: "test-uri", + URI: "test-uri:1234", }}) // Test with a role that defines database names and users. @@ -3561,6 +3565,7 @@ func TestClusterDatabasesGet(t *testing.T) { Hostname: "test-uri", DatabaseUsers: []string{"user1"}, DatabaseNames: []string{"name1"}, + URI: "test-uri:1234", }, { Name: "db2", Type: types.DatabaseTypeSelfHosted, @@ -3569,6 +3574,7 @@ func TestClusterDatabasesGet(t *testing.T) { Hostname: "test-uri", DatabaseUsers: []string{"user1"}, DatabaseNames: []string{"name1"}, + URI: "test-uri:1234", }}) } @@ -6487,6 +6493,7 @@ func TestCreateDatabase(t *testing.T) { Hostname: "someuri", DatabaseUsers: []string{"user1"}, DatabaseNames: []string{"name1"}, + URI: "someuri:3306", }) } } @@ -6648,6 +6655,7 @@ func TestUpdateDatabase_NonErrors(t *testing.T) { Type: "self-hosted", Hostname: "someuri", Labels: []ui.Label{requiredOriginLabel}, + URI: "someuri:3306", }, }, { @@ -6661,6 +6669,7 @@ func TestUpdateDatabase_NonErrors(t *testing.T) { Type: "self-hosted", Hostname: "something-else", Labels: []ui.Label{requiredOriginLabel}, + URI: "something-else:3306", }, }, { @@ -6682,6 +6691,17 @@ func TestUpdateDatabase_NonErrors(t *testing.T) { Type: "rds", Hostname: "llama.cgi8.us-west-2.rds.amazonaws.com", Labels: []ui.Label{requiredOriginLabel}, + URI: "llama.cgi8.us-west-2.rds.amazonaws.com:3306", + AWS: &ui.AWS{ + AWS: types.AWS{ + Region: "us-west-2", + AccountID: "123123123123", + RDS: types.RDS{ + ResourceID: "db-1234", + InstanceID: "llama", + }, + }, + }, }, }, { @@ -6699,6 +6719,17 @@ func TestUpdateDatabase_NonErrors(t *testing.T) { Type: "rds", Hostname: "llama.cgi8.us-west-2.rds.amazonaws.com", Labels: []ui.Label{{Name: "env", Value: "prod"}, requiredOriginLabel}, + URI: "llama.cgi8.us-west-2.rds.amazonaws.com:3306", + AWS: &ui.AWS{ + AWS: types.AWS{ + Region: "us-west-2", + AccountID: "123123123123", + RDS: types.RDS{ + ResourceID: "db-1234", + InstanceID: "llama", + }, + }, + }, }, }, { @@ -6720,6 +6751,17 @@ func TestUpdateDatabase_NonErrors(t *testing.T) { Type: "rds", Hostname: "alpaca.cgi8.us-east-1.rds.amazonaws.com", Labels: []ui.Label{{Name: "env", Value: "prod"}, requiredOriginLabel}, + URI: "alpaca.cgi8.us-east-1.rds.amazonaws.com:3306", + AWS: &ui.AWS{ + AWS: types.AWS{ + Region: "us-east-1", + AccountID: "000000000000", + RDS: types.RDS{ + ResourceID: "db-0000", + InstanceID: "alpaca", + }, + }, + }, }, }, } { @@ -7266,6 +7308,7 @@ func createProxy(ctx context.Context, t *testing.T, proxyID string, node *regula handler.handler.cfg.ProxyKubeAddr = utils.FromAddr(kubeProxyAddr) url, err := url.Parse("https://" + webServer.Listener.Addr().String()) require.NoError(t, err) + handler.handler.cfg.PublicProxyAddr = url.String() return &testProxy{ clock: clock, diff --git a/lib/web/integrations_awsoidc.go b/lib/web/integrations_awsoidc.go new file mode 100644 index 0000000000000..e5af0b235aeb9 --- /dev/null +++ b/lib/web/integrations_awsoidc.go @@ -0,0 +1,106 @@ +/* +Copyright 2023 Gravitational, Inc. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package web + +import ( + "context" + "net/http" + + "github.com/gravitational/trace" + "github.com/julienschmidt/httprouter" + + "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/lib/httplib" + "github.com/gravitational/teleport/lib/integrations/awsoidc" + "github.com/gravitational/teleport/lib/reversetunnel" + "github.com/gravitational/teleport/lib/web/ui" +) + +// IntegrationAWSOIDCTokenGenerator describes the required methods to generate tokens for calling AWS OIDC Integration actions. +type IntegrationAWSOIDCTokenGenerator interface { + // GenerateAWSOIDCToken generates a token to be used to execute an AWS OIDC Integration action. + GenerateAWSOIDCToken(ctx context.Context, req types.GenerateAWSOIDCTokenRequest) (string, error) +} + +// awsOIDCListDatabases returns a list of databases using the ListDatabases action of the AWS OIDC Integration. +func (h *Handler) awsOIDCListDatabases(w http.ResponseWriter, r *http.Request, p httprouter.Params, sctx *SessionContext, site reversetunnel.RemoteSite) (interface{}, error) { + ctx := r.Context() + integrationName := p.ByName("name") + if integrationName == "" { + return nil, trace.BadParameter("an integration name is required") + } + + clt, err := sctx.GetUserClient(ctx, site) + if err != nil { + return nil, trace.Wrap(err) + } + + integration, err := clt.GetIntegration(ctx, integrationName) + if err != nil { + return nil, trace.Wrap(err) + } + + if integration.GetSubKind() != types.IntegrationSubKindAWSOIDC { + return nil, trace.BadParameter("integration subkind (%s) mismatch", integration.GetSubKind()) + } + + var req ui.AWSOIDCListDatabasesRequest + if err := httplib.ReadJSON(r, &req); err != nil { + return nil, trace.Wrap(err) + } + + issuer, err := h.issuerFromPublicAddr() + if err != nil { + return nil, trace.Wrap(err) + } + + token, err := clt.GenerateAWSOIDCToken(ctx, types.GenerateAWSOIDCTokenRequest{ + Issuer: issuer, + }) + if err != nil { + return nil, trace.Wrap(err) + } + + awsoidcSpec := integration.GetAWSOIDCIntegrationSpec() + if awsoidcSpec == nil { + return nil, trace.BadParameter("missing spec fields for %q (%q) integration", integration.GetName(), integration.GetSubKind()) + } + + rdsClient, err := awsoidc.NewRDSClient(ctx, awsoidc.RDSClientRequest{ + Token: token, + RoleARN: awsoidcSpec.RoleARN, + Region: req.Region, + }) + if err != nil { + return nil, trace.Wrap(err) + } + + resp, err := awsoidc.ListDatabases(ctx, + rdsClient, + awsoidc.ListDatabasesRequest{ + Region: req.Region, + NextToken: req.NextToken, + Engines: req.Engines, + RDSType: req.RDSType, + }, + ) + if err != nil { + return nil, trace.Wrap(err) + } + + return ui.AWSOIDCListDatabasesResponse{ + NextToken: resp.NextToken, + Databases: ui.MakeDatabases(resp.Databases, nil, nil), + }, nil +} diff --git a/lib/web/oidcidp.go b/lib/web/oidcidp.go new file mode 100644 index 0000000000000..4f14a28b23aca --- /dev/null +++ b/lib/web/oidcidp.go @@ -0,0 +1,114 @@ +/* +Copyright 2023 Gravitational, Inc. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package web + +import ( + "net/http" + "net/url" + "strings" + + "github.com/gravitational/trace" + "github.com/julienschmidt/httprouter" + + "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/lib/jwt" +) + +const ( + // OIDCJWKWURI is the relative path where the OIDC IdP JWKS is located + OIDCJWKWURI = "/.well-known/jwks-oidc" +) + +// openidConfiguration returns the openid-configuration for setting up the AWS OIDC Integration +func (h *Handler) openidConfiguration(_ http.ResponseWriter, _ *http.Request, _ httprouter.Params) (interface{}, error) { + issuer, err := h.issuerFromPublicAddr() + if err != nil { + return nil, trace.Wrap(err) + } + + return struct { + Issuer string `json:"issuer"` + JWKSURI string `json:"jwks_uri"` + Claims []string `json:"claims"` + IdTokenSigningAlgValuesSupported []string `json:"id_token_signing_alg_values_supported"` + ResponseTypesSupported []string `json:"response_types_supported"` + ScopesSupported []string `json:"scopes_supported"` + SubjectTypesSupported []string `json:"subject_types_supported"` + }{ + Issuer: issuer, + JWKSURI: issuer + OIDCJWKWURI, + Claims: []string{"iss", "sub", "obo", "aud", "jti", "iat", "exp", "nbf"}, + IdTokenSigningAlgValuesSupported: []string{"RS256"}, + ResponseTypesSupported: []string{"id_token"}, + ScopesSupported: []string{"openid"}, + SubjectTypesSupported: []string{"public", "pair-wise"}, + }, nil +} + +// jwksOIDC returns all public keys used to sign JWT tokens for this cluster. +func (h *Handler) jwksOIDC(_ http.ResponseWriter, r *http.Request, _ httprouter.Params) (interface{}, error) { + clusterName, err := h.GetProxyClient().GetDomainName(r.Context()) + if err != nil { + return nil, trace.Wrap(err) + } + + // Fetch the JWT public keys only. + ca, err := h.GetProxyClient().GetCertAuthority(r.Context(), types.CertAuthID{ + Type: types.OIDCIdPCA, + DomainName: clusterName, + }, false /* loadKeys */) + if err != nil { + return nil, trace.Wrap(err) + } + + pairs := ca.GetTrustedJWTKeyPairs() + + // Create response and allocate space for the keys. + var resp JWKSResponse + resp.Keys = make([]jwt.JWK, 0, len(pairs)) + + // Loop over and all add public keys in JWK format. + for _, key := range pairs { + jwk, err := jwt.MarshalJWK(key.PublicKey) + if err != nil { + return nil, trace.Wrap(err) + } + resp.Keys = append(resp.Keys, jwk) + } + return &resp, nil +} + +// issuerFromPublicAddr is the address for the AWS OIDC Provider. +// It must match exactly what was introduced in AWS IAM console. +// PublicProxyAddr does not come with the desired format: it misses the protocol and has a port +// This method adds the `https` protocol and removes the port if it is the default one for https (443) +func (h *Handler) issuerFromPublicAddr() (string, error) { + addr := h.cfg.PublicProxyAddr + + // Add protocol if not present. + if !strings.HasPrefix(addr, "https://") { + addr = "https://" + addr + } + + result, err := url.Parse(addr) + if err != nil { + return "", trace.Wrap(err) + } + + if result.Port() == "443" { + // Cut off redundant :443 + result.Host = result.Hostname() + } + return result.String(), nil +} diff --git a/lib/web/oidcidp_test.go b/lib/web/oidcidp_test.go new file mode 100644 index 0000000000000..1df653afa0e49 --- /dev/null +++ b/lib/web/oidcidp_test.go @@ -0,0 +1,71 @@ +/* +Copyright 2023 Gravitational, Inc. +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package web + +import ( + "context" + "encoding/json" + "testing" + + "github.com/stretchr/testify/require" +) + +// TestOIDCIdPPublicEndpoints ensures the public endpoints for the AWS OIDC integration are available. +// It also validates that the JWKS_URI points to a correct path. +func TestOIDCIdPPublicEndpoints(t *testing.T) { + t.Parallel() + ctx := context.Background() + env := newWebPack(t, 1) + proxy := env.proxies[0] + + // Request OpenID Configuration public endpoint. + publicClt := proxy.newClient(t) + resp, err := publicClt.Get(ctx, proxy.webURL.String()+"/.well-known/openid-configuration", nil) + require.NoError(t, err) + + jwksURI := struct { + JWKSURI string `json:"jwks_uri"` + Issuer string `json:"issuer"` + }{} + + err = json.Unmarshal(resp.Bytes(), &jwksURI) + require.NoError(t, err) + + // Proxy Public addr must match with Issuer + require.Equal(t, proxy.webURL.String(), jwksURI.Issuer) + + // Follow the `jwks_uri` endpoint and fetch the public keys + require.NotEmpty(t, jwksURI.JWKSURI) + resp, err = publicClt.Get(ctx, jwksURI.JWKSURI, nil) + require.NoError(t, err) + + jwksKeys := struct { + Keys []struct { + Use string `json:"use"` + KeyID *string `json:"kid"` + KeyType string `json:"kty"` + Alg string `json:"alg"` + } `json:"keys"` + }{} + + err = json.Unmarshal(resp.Bytes(), &jwksKeys) + require.NoError(t, err) + + require.NotEmpty(t, jwksKeys.Keys) + key := jwksKeys.Keys[0] + require.Equal(t, key.Use, "sig") + require.Equal(t, key.KeyType, "RSA") + require.Equal(t, key.Alg, "RS256") + require.NotNil(t, key.KeyID) // AWS requires this to be present (even if empty string). +} diff --git a/lib/web/ui/integration.go b/lib/web/ui/integration.go index 3078745dd7aae..113a94f2aaef2 100644 --- a/lib/web/ui/integration.go +++ b/lib/web/ui/integration.go @@ -102,3 +102,27 @@ func MakeIntegration(ig types.Integration) Integration { }, } } + +// AWSOIDCListDatabasesRequest is a request to ListDatabases using the AWS OIDC Integration. +type AWSOIDCListDatabasesRequest struct { + // RDSType is either `instance` or `cluster`. + RDSType string `json:"rdsType"` + // Engines filters the returned Databases based on their engine. + // Eg, mysql, postgres, mariadb, aurora, aurora-mysql, aurora-postgresql + Engines []string `json:"engines"` + // Region is the AWS Region. + Region string `json:"region"` + // NextToken is the token to be used to fetch the next page. + // If empty, the first page is fetched. + NextToken string `json:"nextToken"` +} + +// AWSOIDCListDatabasesResponse contains a list of databases and a next token is more pages are available. +type AWSOIDCListDatabasesResponse struct { + // Databases contains the page of Databases + Databases []Database `json:"databases"` + + // NextToken is used for pagination. + // If non-empty, it can be used to request the next page. + NextToken string `json:"nextToken,omitempty"` +} diff --git a/lib/web/ui/server.go b/lib/web/ui/server.go index 27d30d6214fd4..27c6273c02fb9 100644 --- a/lib/web/ui/server.go +++ b/lib/web/ui/server.go @@ -238,17 +238,35 @@ type Database struct { Labels []Label `json:"labels"` // Hostname is the database connection endpoint (URI) hostname (without port and protocol). Hostname string `json:"hostname"` + // URI of the database. + URI string `json:"uri"` // DatabaseUsers is the list of allowed Database RBAC users that the user can login. DatabaseUsers []string `json:"database_users,omitempty"` // DatabaseNames is the list of allowed Database RBAC names that the user can login. DatabaseNames []string `json:"database_names,omitempty"` + // AWS contains AWS specific fields. + AWS *AWS `json:"aws,omitempty"` } +// AWS contains AWS specific fields. +type AWS struct { + // embeds types.AWS fields into this struct when des/serializing. + types.AWS `json:""` + // Status describes the current server status as reported by AWS. + // Currently this field is populated for AWS RDS Databases when Listing Databases using the AWS OIDC Integration + Status string `json:"status,omitempty"` +} + +const ( + // LabelStatus is the label key containing the database status, e.g. "available" + LabelStatus = "status" +) + // MakeDatabase creates database objects. func MakeDatabase(database types.Database, dbUsers, dbNames []string) Database { uiLabels := makeLabels(database.GetAllLabels()) - return Database{ + db := Database{ Name: database.GetName(), Desc: database.GetDescription(), Protocol: database.GetProtocol(), @@ -257,7 +275,21 @@ func MakeDatabase(database types.Database, dbUsers, dbNames []string) Database { DatabaseUsers: dbUsers, DatabaseNames: dbNames, Hostname: stripProtocolAndPort(database.GetURI()), + URI: database.GetURI(), } + + if database.IsAWSHosted() { + dbStatus := "" + if statusLabel, ok := database.GetAllLabels()[LabelStatus]; ok { + dbStatus = statusLabel + } + db.AWS = &AWS{ + AWS: database.GetAWS(), + Status: dbStatus, + } + } + + return db } // MakeDatabases creates database objects.