Files
teleport/lib/auth/authclient/http_client.go
T
Noah Stride 1bb374784b Convert (Upsert/Delete)Proxy RPC from HTTP to gRPC (#66124)
* Convert DeleteProxy RPC from HTTP to gRPC

* Use `t.Context()`

* Add more greppable TODO

* Fix usage of deprecated rpc

* Upsert RPC

* Use wrapper message for UpsertResponse

* gofmt

* Simplify godoc

* Update response message

* Update method names on public api client

* fix tests

* Introduce UpsertProxyWithoutServer

* Edoardo comments

* separate

* Change to UpsertProxyServer RPC handler
2026-05-28 07:59:00 +00:00

667 lines
23 KiB
Go

/*
* Teleport
* Copyright (C) 2023 Gravitational, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package authclient
import (
"context"
"crypto/tls"
"encoding/json"
"net/http"
"net/url"
"time"
"github.com/gravitational/roundtrip"
"github.com/gravitational/trace"
"github.com/jonboulle/clockwork"
"github.com/gravitational/teleport"
"github.com/gravitational/teleport/api/breaker"
"github.com/gravitational/teleport/api/client"
"github.com/gravitational/teleport/api/constants"
apidefaults "github.com/gravitational/teleport/api/defaults"
tracehttp "github.com/gravitational/teleport/api/observability/tracing/http"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/defaults"
"github.com/gravitational/teleport/lib/httplib"
"github.com/gravitational/teleport/lib/services"
)
// HTTPClientConfig contains configuration for an HTTP client.
type HTTPClientConfig struct {
// TLS holds the TLS config for the http client.
TLS *tls.Config
// MaxIdleConns controls the maximum number of idle (keep-alive) connections across all hosts.
MaxIdleConns int
// MaxIdleConnsPerHost, if non-zero, controls the maximum idle (keep-alive) connections to keep per-host.
MaxIdleConnsPerHost int
// MaxConnsPerHost limits the total number of connections per host, including connections in the dialing,
// active, and idle states. On limit violation, dials will block.
MaxConnsPerHost int
// RequestTimeout specifies a time limit for requests made by this Client.
RequestTimeout time.Duration
// IdleConnTimeout defines the maximum amount of time before idle connections are closed.
IdleConnTimeout time.Duration
// ResponseHeaderTimeout specifies the amount of time to wait for a server's
// response headers after fully writing the request (including its body, if any).
// This time does not include the time to read the response body.
ResponseHeaderTimeout time.Duration
// Dialer is a custom dialer used to dial a server. The Dialer should
// have custom logic to provide an address to the dialer. If set, Dialer
// takes precedence over all other connection options.
Dialer client.ContextDialer
// ALPNSNIAuthDialClusterName if present the client will include ALPN SNI routing information in TLS Hello message
// allowing to dial auth service through Teleport Proxy directly without using SSH Tunnels.
ALPNSNIAuthDialClusterName string
// CircuitBreakerConfig defines how the circuit breaker should behave.
CircuitBreakerConfig breaker.Config
}
// CheckAndSetDefaults validates and sets defaults for HTTP configuration.
func (c *HTTPClientConfig) CheckAndSetDefaults() error {
if c.TLS == nil {
return trace.BadParameter("missing TLS config")
}
if c.Dialer == nil {
return trace.BadParameter("missing dialer")
}
// Set the next protocol. This is needed due to the Auth Server using a
// multiplexer for protocol detection. Unless next protocol is specified
// it will attempt to upgrade to HTTP2 and at that point there is no way
// to distinguish between HTTP2/JSON or gRPC.
c.TLS.NextProtos = []string{teleport.HTTPNextProtoTLS}
// Configure ALPN SNI direct dial TLS routing information used by ALPN SNI proxy in order to
// dial auth service without using SSH tunnels.
c.TLS = client.ConfigureALPN(c.TLS, c.ALPNSNIAuthDialClusterName)
if c.CircuitBreakerConfig.Trip == nil || c.CircuitBreakerConfig.IsSuccessful == nil {
c.CircuitBreakerConfig = breaker.DefaultBreakerConfig(clockwork.NewRealClock())
}
// One or both of these timeouts should be set to ensure there is a timeout in place.
if c.RequestTimeout == 0 && c.ResponseHeaderTimeout == 0 {
c.RequestTimeout = defaults.HTTPRequestTimeout
c.ResponseHeaderTimeout = apidefaults.DefaultIOTimeout
}
// Leaving this unset will lead to connections open forever and will cause memory leaks in a long running process.
if c.IdleConnTimeout == 0 {
c.IdleConnTimeout = defaults.HTTPIdleTimeout
}
// Increase the size of the connection pool. This substantially improves the
// performance of Teleport under load as it reduces the number of TLS
// handshakes performed.
if c.MaxIdleConns == 0 {
c.MaxIdleConns = defaults.HTTPMaxIdleConns
}
if c.MaxIdleConnsPerHost == 0 {
c.MaxIdleConnsPerHost = defaults.HTTPMaxIdleConnsPerHost
}
// Limit the total number of connections to the Auth Server. Some hosts allow a low
// number of connections per process (ulimit) to a host. This is a problem for
// enhanced session recording auditing which emits so many events to the
// Audit Log (using the Auth Client) that the connection pool often does not
// have a free connection to return, so just opens a new one. This quickly
// leads to hitting the OS limit and the client returning out of file
// descriptors error.
if c.MaxConnsPerHost == 0 {
c.MaxConnsPerHost = defaults.HTTPMaxConnsPerHost
}
return nil
}
// Clone creates a new client with the same configuration.
func (c *HTTPClientConfig) Clone() *HTTPClientConfig {
return &HTTPClientConfig{
TLS: c.TLS.Clone(),
MaxIdleConns: c.MaxIdleConns,
MaxIdleConnsPerHost: c.MaxIdleConnsPerHost,
MaxConnsPerHost: c.MaxConnsPerHost,
RequestTimeout: c.RequestTimeout,
IdleConnTimeout: c.IdleConnTimeout,
ResponseHeaderTimeout: c.ResponseHeaderTimeout,
Dialer: c.Dialer,
ALPNSNIAuthDialClusterName: c.ALPNSNIAuthDialClusterName,
CircuitBreakerConfig: c.CircuitBreakerConfig,
}
}
// HTTPClient is a teleport HTTP API client.
type HTTPClient struct {
*roundtrip.Client
// cfg is the http client configuration.
cfg *HTTPClientConfig
}
// NewHTTPClient creates a new HTTP client with TLS authentication and the given dialer.
func NewHTTPClient(cfg *HTTPClientConfig, params ...roundtrip.ClientParam) (*HTTPClient, error) {
if err := cfg.CheckAndSetDefaults(); err != nil {
return nil, err
}
transport := &http.Transport{
DialContext: cfg.Dialer.DialContext,
ResponseHeaderTimeout: cfg.ResponseHeaderTimeout,
TLSClientConfig: cfg.TLS,
MaxIdleConns: cfg.MaxIdleConns,
MaxIdleConnsPerHost: cfg.MaxIdleConnsPerHost,
MaxConnsPerHost: cfg.MaxConnsPerHost,
IdleConnTimeout: cfg.IdleConnTimeout,
}
roundtripClient, err := newRoundtripClient(cfg, transport)
if err != nil {
return nil, trace.Wrap(err)
}
return &HTTPClient{
cfg: cfg,
Client: roundtripClient,
}, nil
}
func newRoundtripClient(cfg *HTTPClientConfig, transport *http.Transport, params ...roundtrip.ClientParam) (*roundtrip.Client, error) {
cb, err := breaker.New(cfg.CircuitBreakerConfig)
if err != nil {
return nil, trace.Wrap(err)
}
clientParams := append(
[]roundtrip.ClientParam{
roundtrip.HTTPClient(&http.Client{
Timeout: cfg.RequestTimeout,
Transport: tracehttp.NewTransport(breaker.NewRoundTripper(cb, transport)),
}),
roundtrip.SanitizerEnabled(true),
},
params...,
)
// Since the client uses a custom dialer and SNI is used for TLS handshake, the address
// used here is arbitrary as it just needs to be set to pass http request validation.
roundtripClient, err := roundtrip.NewClient("https://"+constants.APIDomain, CurrentVersion, clientParams...)
if err != nil {
return nil, trace.Wrap(err)
}
return roundtripClient, nil
}
// CloneHTTPClient creates a new HTTP client with the same configuration.
func (c *HTTPClient) CloneHTTPClient(params ...roundtrip.ClientParam) (*HTTPClient, error) {
cfg := c.cfg.Clone()
// We copy the transport which may have had roundtrip.ClientParams applied on initial creation.
transport, err := c.getTransport()
if err != nil {
return nil, trace.Wrap(err)
}
roundtripClient, err := newRoundtripClient(c.cfg, transport, params...)
if err != nil {
return nil, trace.Wrap(err)
}
return &HTTPClient{
Client: roundtripClient,
cfg: cfg,
}, nil
}
// ClientParamRequestTimeout sets request timeout of the HTTP transport used by the client.
func ClientParamTimeout(timeout time.Duration) roundtrip.ClientParam {
return func(c *roundtrip.Client) error {
c.HTTPClient().Timeout = timeout
return nil
}
}
// ClientParamResponseHeaderTimeout sets response header timeout of the HTTP transport used by the client.
func ClientParamResponseHeaderTimeout(timeout time.Duration) roundtrip.ClientParam {
return func(c *roundtrip.Client) error {
if t, err := getHTTPTransport(c); err == nil {
t.ResponseHeaderTimeout = timeout
}
return nil
}
}
// ClientParamIdleConnTimeout sets idle connection header timeout of the HTTP transport used by the client.
func ClientParamIdleConnTimeout(timeout time.Duration) roundtrip.ClientParam {
return func(c *roundtrip.Client) error {
if t, err := getHTTPTransport(c); err == nil {
t.IdleConnTimeout = timeout
}
return nil
}
}
// Close closes the HTTP client connection to the auth server.
func (c *HTTPClient) Close() {
c.Client.HTTPClient().CloseIdleConnections()
}
// TLSConfig returns the HTTP client's TLS config.
func (c *HTTPClient) TLSConfig() *tls.Config {
return c.cfg.TLS
}
// GetTransport returns the HTTP client's transport.
func (c *HTTPClient) getTransport() (*http.Transport, error) {
return getHTTPTransport(c.Client)
}
func getHTTPTransport(c *roundtrip.Client) (*http.Transport, error) {
type wrapper interface {
Unwrap() http.RoundTripper
}
transport := c.HTTPClient().Transport
for {
switch t := transport.(type) {
case wrapper:
transport = t.Unwrap()
case *http.Transport:
return t, nil
default:
return nil, trace.BadParameter("unexpected transport type %T", t)
}
}
}
// PostJSON is a generic method that issues http POST request to the server
func (c *HTTPClient) PostJSON(ctx context.Context, endpoint string, val any) (*roundtrip.Response, error) {
return httplib.ConvertResponse(c.Client.PostJSON(ctx, endpoint, val))
}
// PutJSON is a generic method that issues http PUT request to the server
func (c *HTTPClient) PutJSON(ctx context.Context, endpoint string, val any) (*roundtrip.Response, error) {
return httplib.ConvertResponse(c.Client.PutJSON(ctx, endpoint, val))
}
// PostForm is a generic method that issues http POST request to the server
func (c *HTTPClient) PostForm(ctx context.Context, endpoint string, vals url.Values, files ...roundtrip.File) (*roundtrip.Response, error) {
return httplib.ConvertResponse(c.Client.PostForm(ctx, endpoint, vals, files...))
}
// Get issues http GET request to the server
func (c *HTTPClient) Get(ctx context.Context, u string, params url.Values) (*roundtrip.Response, error) {
return httplib.ConvertResponse(c.Client.Get(ctx, u, params))
}
// Delete issues http Delete Request to the server
func (c *HTTPClient) Delete(ctx context.Context, u string) (*roundtrip.Response, error) {
return httplib.ConvertResponse(c.Client.Delete(ctx, u))
}
type upsertTunnelConnectionRawReq struct {
TunnelConnection json.RawMessage `json:"tunnel_connection"`
}
// UpsertTunnelConnection upserts tunnel connection
func (c *HTTPClient) UpsertTunnelConnection(conn types.TunnelConnection) error {
data, err := services.MarshalTunnelConnection(conn)
if err != nil {
return trace.Wrap(err)
}
args := &upsertTunnelConnectionRawReq{
TunnelConnection: data,
}
_, err = c.PostJSON(context.TODO(), c.Endpoint("tunnelconnections"), args)
return trace.Wrap(err)
}
// GetTunnelConnections returns tunnel connections for a given cluster
func (c *HTTPClient) GetTunnelConnections(clusterName string, opts ...services.MarshalOption) ([]types.TunnelConnection, error) {
if clusterName == "" {
return nil, trace.BadParameter("missing cluster name parameter")
}
out, err := c.Get(context.TODO(), c.Endpoint("tunnelconnections", clusterName), url.Values{})
if err != nil {
return nil, trace.Wrap(err)
}
var items []json.RawMessage
if err := json.Unmarshal(out.Bytes(), &items); err != nil {
return nil, trace.Wrap(err)
}
conns := make([]types.TunnelConnection, len(items))
for i, raw := range items {
conn, err := services.UnmarshalTunnelConnection(raw)
if err != nil {
return nil, trace.Wrap(err)
}
conns[i] = conn
}
return conns, nil
}
// GetAllTunnelConnections returns all tunnel connections
func (c *HTTPClient) GetAllTunnelConnections(opts ...services.MarshalOption) ([]types.TunnelConnection, error) {
out, err := c.Get(context.TODO(), c.Endpoint("tunnelconnections"), url.Values{})
if err != nil {
return nil, trace.Wrap(err)
}
var items []json.RawMessage
if err := json.Unmarshal(out.Bytes(), &items); err != nil {
return nil, trace.Wrap(err)
}
conns := make([]types.TunnelConnection, len(items))
for i, raw := range items {
conn, err := services.UnmarshalTunnelConnection(raw)
if err != nil {
return nil, trace.Wrap(err)
}
conns[i] = conn
}
return conns, nil
}
// DeleteTunnelConnection deletes tunnel connection by name
func (c *HTTPClient) DeleteTunnelConnection(clusterName string, connName string) error {
if clusterName == "" {
return trace.BadParameter("missing parameter cluster name")
}
if connName == "" {
return trace.BadParameter("missing parameter connection name")
}
_, err := c.Delete(context.TODO(), c.Endpoint("tunnelconnections", clusterName, connName))
return trace.Wrap(err)
}
type upsertServerRawReq struct {
Server json.RawMessage `json:"server"`
TTL time.Duration `json:"ttl"`
}
// ExtendWebSession creates a new web session for a user based on another
// valid web session
func (c *HTTPClient) ExtendWebSession(ctx context.Context, req WebSessionReq) (types.WebSession, error) {
out, err := c.PostJSON(ctx, c.Endpoint("users", req.User, "web", "sessions"), req)
if err != nil {
return nil, trace.Wrap(err)
}
return services.UnmarshalWebSession(out.Bytes())
}
// CreateWebSession creates a new web session for a user
func (c *HTTPClient) CreateWebSession(ctx context.Context, user string) (types.WebSession, error) {
out, err := c.PostJSON(
ctx,
c.Endpoint("users", url.PathEscape(user), "web", "sessions"),
WebSessionReq{User: user},
)
if err != nil {
return nil, trace.Wrap(err)
}
return services.UnmarshalWebSession(out.Bytes())
}
// AuthenticateWebUser authenticates web user, creates and returns web session
// in case if authentication is successful
func (c *HTTPClient) AuthenticateWebUser(ctx context.Context, req AuthenticateUserRequest) (types.WebSession, error) {
out, err := c.PostJSON(
ctx,
c.Endpoint("users", url.PathEscape(req.Username), "web", "authenticate"),
req,
)
if err != nil {
return nil, trace.Wrap(err)
}
return services.UnmarshalWebSession(out.Bytes())
}
// AuthenticateSSHUser authenticates SSH console user, creates and returns a pair of signed TLS and SSH
// short lived certificates as a result
func (c *HTTPClient) AuthenticateSSHUser(ctx context.Context, req AuthenticateSSHRequest) (*CLILoginResponse, error) {
out, err := c.PostJSON(
ctx,
c.Endpoint("users", url.PathEscape(req.Username), "ssh", "authenticate"),
req,
)
if err != nil {
return nil, trace.Wrap(err)
}
var re CLILoginResponse
if err := json.Unmarshal(out.Bytes(), &re); err != nil {
return nil, trace.Wrap(err)
}
return &re, nil
}
// ValidateOIDCAuthCallbackReq is the request made by the proxy to validate
// and activate a login via OIDC.
type ValidateOIDCAuthCallbackReq struct {
Query url.Values `json:"query"`
}
// OIDCAuthRawResponse is returned when auth server validated callback parameters
// returned from OIDC provider
type OIDCAuthRawResponse struct {
// Username is authenticated teleport username
Username string `json:"username"`
// Identity contains validated OIDC identity
Identity types.ExternalIdentity `json:"identity"`
// Web session will be generated by auth server if requested in OIDCAuthRequest
Session json.RawMessage `json:"session,omitempty"`
// Cert will be generated by certificate authority
Cert []byte `json:"cert,omitempty"`
// TLSCert is PEM encoded TLS certificate
TLSCert []byte `json:"tls_cert,omitempty"`
// Req is original oidc auth request
Req OIDCAuthRequest `json:"req"`
// HostSigners is a list of signing host public keys
// trusted by proxy, used in console login
HostSigners []json.RawMessage `json:"host_signers"`
// MFAToken is an SSO MFA token.
MFAToken string `json:"mfa_token"`
// ClientOptions contains some options that the cluster wants the client to
// use.
ClientOptions ClientOptions `json:"client_options"`
}
// ValidateOIDCAuthCallback validates OIDC auth callback returned from redirect
func (c *HTTPClient) ValidateOIDCAuthCallback(ctx context.Context, q url.Values) (*OIDCAuthResponse, error) {
out, err := c.PostJSON(ctx, c.Endpoint("oidc", "requests", "validate"), ValidateOIDCAuthCallbackReq{
Query: q,
})
if err != nil {
return nil, trace.Wrap(err)
}
var rawResponse OIDCAuthRawResponse
if err := json.Unmarshal(out.Bytes(), &rawResponse); err != nil {
return nil, trace.Wrap(err)
}
response := OIDCAuthResponse{
Username: rawResponse.Username,
Identity: rawResponse.Identity,
Cert: rawResponse.Cert,
Req: rawResponse.Req,
TLSCert: rawResponse.TLSCert,
MFAToken: rawResponse.MFAToken,
ClientOptions: rawResponse.ClientOptions,
}
if len(rawResponse.Session) != 0 {
session, err := services.UnmarshalWebSession(rawResponse.Session)
if err != nil {
return nil, trace.Wrap(err)
}
response.Session = session
}
response.HostSigners = make([]types.CertAuthority, len(rawResponse.HostSigners))
for i, raw := range rawResponse.HostSigners {
ca, err := services.UnmarshalCertAuthority(raw)
if err != nil {
return nil, trace.Wrap(err)
}
response.HostSigners[i] = ca
}
return &response, nil
}
// ValidateSAMLResponseReq is the request made by the proxy to validate
// and activate a login via SAML.
type ValidateSAMLResponseReq struct {
// Response is SAML statements coming from the identity provider.
Response string `json:"response"`
// ConnectorID is ID of a SAML connector that should be used for this request.
ConnectorID string `json:"connector_id,omitempty"`
// ClientIP is IP of the logging in client, used in identity provider initiated login case,
// when we don't have original client's request with their IP stored.
ClientIP string `json:"client_ip,omitempty"`
}
// SAMLAuthRawResponse is returned when auth server validated callback parameters
// returned from SAML provider
type SAMLAuthRawResponse struct {
// Username is authenticated teleport username
Username string `json:"username"`
// Identity contains validated OIDC identity
Identity types.ExternalIdentity `json:"identity"`
// Web session will be generated by auth server if requested in OIDCAuthRequest
Session json.RawMessage `json:"session,omitempty"`
// Cert will be generated by certificate authority
Cert []byte `json:"cert,omitempty"`
// Req is original oidc auth request
Req SAMLAuthRequest `json:"req"`
// HostSigners is a list of signing host public keys
// trusted by proxy, used in console login
HostSigners []json.RawMessage `json:"host_signers"`
// TLSCert is TLS certificate authority certificate
TLSCert []byte `json:"tls_cert,omitempty"`
// MFAToken is an SSO MFA token.
MFAToken string `json:"mfa_token"`
// ClientOptions contains some options that the cluster wants the client to
// use.
ClientOptions ClientOptions `json:"client_options"`
}
// ValidateSAMLResponse validates response returned by SAML identity provider
func (c *HTTPClient) ValidateSAMLResponse(ctx context.Context, samlResponse, connectorID, clientIP string) (*SAMLAuthResponse, error) {
out, err := c.PostJSON(ctx, c.Endpoint("saml", "requests", "validate"), ValidateSAMLResponseReq{
Response: samlResponse,
ConnectorID: connectorID,
ClientIP: clientIP,
})
if err != nil {
return nil, trace.Wrap(err)
}
var rawResponse SAMLAuthRawResponse
if err := json.Unmarshal(out.Bytes(), &rawResponse); err != nil {
return nil, trace.Wrap(err)
}
response := SAMLAuthResponse{
Username: rawResponse.Username,
Identity: rawResponse.Identity,
Cert: rawResponse.Cert,
Req: rawResponse.Req,
TLSCert: rawResponse.TLSCert,
MFAToken: rawResponse.MFAToken,
ClientOptions: rawResponse.ClientOptions,
}
if len(rawResponse.Session) != 0 {
session, err := services.UnmarshalWebSession(rawResponse.Session)
if err != nil {
return nil, trace.Wrap(err)
}
response.Session = session
}
response.HostSigners = make([]types.CertAuthority, len(rawResponse.HostSigners))
for i, raw := range rawResponse.HostSigners {
ca, err := services.UnmarshalCertAuthority(raw)
if err != nil {
return nil, trace.Wrap(err)
}
response.HostSigners[i] = ca
}
return &response, nil
}
// validateGithubAuthCallbackReq is a request to validate Github OAuth2 callback
type validateGithubAuthCallbackReq struct {
// Query is the callback query string
Query url.Values `json:"query"`
}
// githubAuthRawResponse is returned when auth server validated callback
// parameters returned from Github during OAuth2 flow
type githubAuthRawResponse struct {
// Username is authenticated teleport username
Username string `json:"username"`
// Identity contains validated OIDC identity
Identity types.ExternalIdentity `json:"identity"`
// Web session will be generated by auth server if requested in OIDCAuthRequest
Session json.RawMessage `json:"session,omitempty"`
// Cert will be generated by certificate authority
Cert []byte `json:"cert,omitempty"`
// TLSCert is PEM encoded TLS certificate
TLSCert []byte `json:"tls_cert,omitempty"`
// Req is original oidc auth request
Req GithubAuthRequest `json:"req"`
// HostSigners is a list of signing host public keys
// trusted by proxy, used in console login
HostSigners []json.RawMessage `json:"host_signers"`
// ClientOptions contains some options that the cluster wants the client to
// use.
ClientOptions ClientOptions `json:"client_options"`
}
// ValidateGithubAuthCallback validates Github auth callback returned from redirect
func (c *HTTPClient) ValidateGithubAuthCallback(ctx context.Context, q url.Values) (*GithubAuthResponse, error) {
out, err := c.PostJSON(ctx, c.Endpoint("github", "requests", "validate"),
validateGithubAuthCallbackReq{Query: q})
if err != nil {
return nil, trace.Wrap(err)
}
var rawResponse githubAuthRawResponse
if err := json.Unmarshal(out.Bytes(), &rawResponse); err != nil {
return nil, trace.Wrap(err)
}
response := GithubAuthResponse{
Username: rawResponse.Username,
Identity: rawResponse.Identity,
Cert: rawResponse.Cert,
Req: rawResponse.Req,
TLSCert: rawResponse.TLSCert,
ClientOptions: rawResponse.ClientOptions,
}
if len(rawResponse.Session) != 0 {
session, err := services.UnmarshalWebSession(
rawResponse.Session)
if err != nil {
return nil, trace.Wrap(err)
}
response.Session = session
}
response.HostSigners = make([]types.CertAuthority, len(rawResponse.HostSigners))
for i, raw := range rawResponse.HostSigners {
ca, err := services.UnmarshalCertAuthority(raw)
if err != nil {
return nil, trace.Wrap(err)
}
response.HostSigners[i] = ca
}
return &response, nil
}