Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
f148a97
Allow creating app gateways in tshd
gzdunek Jan 8, 2024
ec0e702
Add UI for document gateway app
gzdunek Jan 8, 2024
cac1df3
Show apps in connections
gzdunek Jan 8, 2024
5f11764
Capture app protocol
gzdunek Jan 8, 2024
9309d99
Start app proxy when clicking on 'Connect' in app
gzdunek Jan 8, 2024
6d8a902
Remove `removeAppGateway`
gzdunek Jan 8, 2024
5b1151a
`appUri` -> `targetUri`
gzdunek Jan 8, 2024
2a0d899
Add TCP and HTTP constants
gzdunek Jan 8, 2024
2906658
Add CLI command for HTTP apps
gzdunek Jan 9, 2024
6f37ce9
Add `makeAppGateway`
gzdunek Jan 9, 2024
dcc2978
Specify `handleChangePort` dependencies correctly
gzdunek Jan 9, 2024
0277365
Remove `doc.gateway_app` and `connection.app`, instead differentiate …
gzdunek Jan 9, 2024
6a2e4eb
Correctly report protocol usage
gzdunek Jan 9, 2024
c77829b
Mention that AWS apps are supported in tsh
gzdunek Jan 10, 2024
7b75f23
Rename constants and add godoc
gzdunek Jan 10, 2024
b8bdfe5
Add a TODO comment about dialogs for connecting to unsupported apps
gzdunek Jan 10, 2024
97eab28
`onRun` -> `onButtonClick`, `runButtonText` -> `buttonText`
gzdunek Jan 10, 2024
4f73619
Show a notification after copying to clipboard
gzdunek Jan 10, 2024
7905dfe
Make the message about unsupported gateways more precise
gzdunek Jan 10, 2024
133b12f
Revert mistakenly removed `document.targetUri` from `getResourceUri`
gzdunek Jan 10, 2024
10514ee
Remove 'Local app proxy' header
gzdunek Jan 10, 2024
aa4cb24
Merge branch 'master' into gzdunek/app-gateways
gzdunek Jan 10, 2024
79718bf
Post-merge fixes
gzdunek Jan 10, 2024
5977ef1
Use proper component for the 'offline gateway' state
gzdunek Jan 10, 2024
718b688
Support all gateway types in relogin UI
ravicious Jan 11, 2024
4ef4504
Fix JSdoc comment
gzdunek Jan 11, 2024
e17c6a5
Add a TODO comment about the docs link
gzdunek Jan 11, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions api/types/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -1174,3 +1174,10 @@ const (
// installer script when agentless mode is enabled for a matcher
DefaultInstallerScriptNameAgentless = "default-agentless-installer"
)

const (
// ApplicationProtocolHTTP is the HTTP (Web) apps protocol
ApplicationProtocolHTTP = "HTTP"
// ApplicationProtocolTCP is the TCP apps protocol.
ApplicationProtocolTCP = "TCP"
)
4 changes: 2 additions & 2 deletions lib/teleterm/api/uri/parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,8 @@ func validateProfileName(r ResourceURI) error {
}

func validateGatewayTargetResource(r ResourceURI) error {
if r.GetDbName() == "" && r.GetKubeName() == "" {
return trace.BadParameter("malformed gateway target URI %q, expecting a database or kube resource", r)
if r.GetDbName() == "" && r.GetKubeName() == "" && r.GetAppName() == "" {
return trace.BadParameter("malformed gateway target URI %q, expecting a database, kube or app resource", r)
}
return nil
}
Expand Down
97 changes: 97 additions & 0 deletions lib/teleterm/clusters/cluster_apps.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,16 @@
package clusters

import (
"context"
"crypto/tls"
"fmt"

"github.com/gravitational/trace"

"github.com/gravitational/teleport/api/client/proto"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/client"
libclient "github.com/gravitational/teleport/lib/client"
"github.com/gravitational/teleport/lib/teleterm/api/uri"
)

Expand All @@ -36,3 +45,91 @@ type SAMLIdPServiceProvider struct {

Provider types.SAMLIdPServiceProvider
}

func (c *Cluster) getApp(ctx context.Context, appName string) (types.Application, error) {
var app types.Application
err := AddMetadataToRetryableError(ctx, func() error {
apps, err := c.clusterClient.ListApps(ctx, &proto.ListResourcesRequest{
Namespace: c.clusterClient.Namespace,
ResourceType: types.KindAppServer,
PredicateExpression: fmt.Sprintf(`name == "%s"`, appName),
})
if err != nil {
return trace.Wrap(err)
}

if len(apps) == 0 {
return trace.NotFound("app %q not found", appName)
}

app = apps[0]
return nil
})

return app, trace.Wrap(err)
}

// reissueAppCert issue new certificates for the app and saves them to disk.
func (c *Cluster) reissueAppCert(ctx context.Context, app types.Application) (tls.Certificate, error) {
Comment thread
gzdunek marked this conversation as resolved.
Outdated
if app.IsAWSConsole() || app.IsGCP() || app.IsAzureCloud() {
return tls.Certificate{}, trace.BadParameter("cloud applications are not supported")
}
// Refresh the certs to account for clusterClient.SiteName pointing at a leaf cluster.
err := c.clusterClient.ReissueUserCerts(ctx, client.CertCacheKeep, client.ReissueParams{
RouteToCluster: c.clusterClient.SiteName,
AccessRequests: c.status.ActiveRequests.AccessRequests,
})
if err != nil {
return tls.Certificate{}, trace.Wrap(err)
}

proxyClient, err := c.clusterClient.ConnectToProxy(ctx)
if err != nil {
return tls.Certificate{}, trace.Wrap(err)
}
defer proxyClient.Close()

request := types.CreateAppSessionRequest{
Username: c.status.Username,
PublicAddr: app.GetPublicAddr(),
ClusterName: c.clusterClient.SiteName,
AWSRoleARN: "",
AzureIdentity: "",
GCPServiceAccount: "",
}

ws, err := proxyClient.CreateAppSession(ctx, request)
if err != nil {
return tls.Certificate{}, trace.Wrap(err)
}

err = proxyClient.ReissueUserCerts(ctx, client.CertCacheKeep, client.ReissueParams{
RouteToCluster: c.clusterClient.SiteName,
RouteToApp: proto.RouteToApp{
Name: app.GetName(),
SessionID: ws.GetName(),
PublicAddr: app.GetPublicAddr(),
ClusterName: c.clusterClient.SiteName,
AWSRoleARN: "",
AzureIdentity: "",
GCPServiceAccount: "",
},
AccessRequests: c.status.ActiveRequests.AccessRequests,
})
if err != nil {
return tls.Certificate{}, trace.Wrap(err)
}

key, err := c.clusterClient.LocalAgent().GetKey(c.clusterClient.SiteName, libclient.WithAppCerts{})
if err != nil {
return tls.Certificate{}, trace.Wrap(err)
}

cert, ok := key.AppTLSCerts[app.GetName()]
if !ok {
return tls.Certificate{}, trace.NotFound("the user is not logged in into the application %v", app.GetName())
}

tlsCert, err := key.TLSCertificate(cert)
return tlsCert, trace.Wrap(err)
Comment on lines 101 to 134
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI I took this from

func loadAppCertificate(tc *libclient.TeleportClient, appName string) (certificate tls.Certificate, needLogin bool, err error) {

}
51 changes: 51 additions & 0 deletions lib/teleterm/clusters/cluster_gateways.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ func (c *Cluster) CreateGateway(ctx context.Context, params CreateGatewayParams)
gateway, err := c.createKubeGateway(ctx, params)
return gateway, trace.Wrap(err)

case params.TargetURI.IsApp():
gateway, err := c.createAppGateway(ctx, params)
return gateway, trace.Wrap(err)

default:
return nil, trace.NotImplemented("gateway not supported for %v", params.TargetURI)
}
Expand Down Expand Up @@ -148,6 +152,43 @@ func (c *Cluster) createKubeGateway(ctx context.Context, params CreateGatewayPar
return gw, trace.Wrap(err)
}

func (c *Cluster) createAppGateway(ctx context.Context, params CreateGatewayParams) (gateway.Gateway, error) {
appName := params.TargetURI.GetAppName()

app, err := c.getApp(ctx, appName)
if err != nil {
return nil, trace.Wrap(err)
}

var cert tls.Certificate

if err := AddMetadataToRetryableError(ctx, func() error {
cert, err = c.reissueAppCert(ctx, app)
return trace.Wrap(err)
}); err != nil {
return nil, trace.Wrap(err)
}

gw, err := gateway.New(gateway.Config{
LocalPort: params.LocalPort,
TargetURI: params.TargetURI,
TargetName: appName,
Cert: cert,
Protocol: app.GetProtocol(),
Insecure: c.clusterClient.InsecureSkipVerify,
WebProxyAddr: c.clusterClient.WebProxyAddr,
Log: c.Log,
TCPPortAllocator: params.TCPPortAllocator,
OnExpiredCert: params.OnExpiredCert,
Clock: c.clock,
TLSRoutingConnUpgradeRequired: c.clusterClient.TLSRoutingConnUpgradeRequired,
RootClusterCACertPoolFunc: c.clusterClient.RootClusterCACertPool,
ClusterName: c.Name,
Username: c.status.Username,
})
return gw, trace.Wrap(err)
}

// ReissueGatewayCerts reissues certificate for the provided gateway.
//
// At the moment, kube gateways reload their certs in memory while db gateways use the old approach
Expand Down Expand Up @@ -177,6 +218,16 @@ func (c *Cluster) ReissueGatewayCerts(ctx context.Context, g gateway.Gateway) (t
case g.TargetURI().IsKube():
cert, err := c.reissueKubeCert(ctx, g.TargetName())
return cert, trace.Wrap(err)
case g.TargetURI().IsApp():
appName := g.TargetURI().GetAppName()

app, err := c.getApp(ctx, appName)
if err != nil {
return tls.Certificate{}, trace.Wrap(err)
}

cert, err := c.reissueAppCert(ctx, app)
return cert, trace.Wrap(err)
default:
return tls.Certificate{}, trace.NotImplemented("ReissueGatewayCerts does not support this gateway kind %v", g.TargetURI().String())
}
Expand Down
43 changes: 43 additions & 0 deletions lib/teleterm/cmd/app.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* Teleport
* Copyright (C) 2024 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 cmd

import (
"os/exec"

"github.com/gravitational/trace"

"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/teleterm/gateway"
)

// NewAppCLICommand creates CLI commands for app gateways.
func NewAppCLICommand(g gateway.Gateway) (*exec.Cmd, error) {
app, err := gateway.AsApp(g)
if err != nil {
return nil, trace.Wrap(err)
}

if g.Protocol() == types.ApplicationProtocolTCP {
return exec.Command(""), nil
}

cmd := exec.Command("curl", app.LocalProxyURL())
return cmd, nil
}
81 changes: 81 additions & 0 deletions lib/teleterm/cmd/app_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/*
* Teleport
* Copyright (C) 2024 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 cmd

import (
"fmt"
"strings"
"testing"

"github.com/stretchr/testify/require"

"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/teleterm/gateway"
)

type fakeAppGateway struct {
gateway.App
protocol string
generatedUrl string
}

func (m fakeAppGateway) Protocol() string { return m.protocol }
func (m fakeAppGateway) LocalProxyURL() string { return m.generatedUrl }

func TestNewAppCLICommand(t *testing.T) {
testCases := []struct {
name string
protocol string
generatedUrl string
output string
}{
{
name: "TCP app",
protocol: types.ApplicationProtocolTCP,
generatedUrl: "tcp://localhost:8888",
output: "",
},
{
name: "HTTP app",
protocol: types.ApplicationProtocolHTTP,
generatedUrl: "http://localhost:8888",
output: "curl http://localhost:8888",
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {

mockGateway := fakeAppGateway{
protocol: tc.protocol,
generatedUrl: tc.generatedUrl,
}

command, err := NewAppCLICommand(mockGateway)

require.NoError(t, err)
cmdString := strings.TrimSpace(
fmt.Sprintf("%s %s",
strings.Join(command.Env, " "),
strings.Join(command.Args, " ")))

require.Equal(t, cmdString, tc.output)
})
}
}
4 changes: 4 additions & 0 deletions lib/teleterm/daemon/daemon.go
Original file line number Diff line number Diff line change
Expand Up @@ -447,6 +447,10 @@ func (s *Service) GetGatewayCLICommand(gateway gateway.Gateway) (*exec.Cmd, erro
cmd, err := cmd.NewKubeCLICommand(gateway)
return cmd, trace.Wrap(err)

case targetURI.IsApp():
cmd, err := cmd.NewAppCLICommand(gateway)
return cmd, trace.Wrap(err)

default:
return nil, trace.NotImplemented("gateway not supported for %v", targetURI)
}
Expand Down
Loading