Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update AWS roles ARNs displayed on tsh app login for AWS console apps #44983

Merged
merged 25 commits into from
Sep 19, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
3b797d6
feat(tsh): list aws console logins from server
gabrielcorado Aug 1, 2024
f912806
chore(services): remove unified resources change
gabrielcorado Aug 2, 2024
c2da0ec
test(tsh): solve TestAzure flakiness by waiting using app servers are…
gabrielcorado Aug 6, 2024
ead8f87
fix(tsh): apps with logins were fallingback into using aws arns
gabrielcorado Aug 6, 2024
c5f1529
refactor(client): use GetEnrichedResources
gabrielcorado Aug 6, 2024
3968afc
chore(client): rename function
gabrielcorado Aug 6, 2024
64c358e
Merge branch 'master' into gabrielcorado/tsh-app-login-leaf-logins
gabrielcorado Aug 8, 2024
304fca0
refactor(tsh): directly resource lisiting for apps and reuse cluster …
gabrielcorado Aug 9, 2024
1d00da7
chore(client): reset client changes
gabrielcorado Aug 9, 2024
223494f
refactor(tsh): reuse cluster client for fetching allowed logins
gabrielcorado Aug 9, 2024
c6de8ac
chore(tsh): remove unused function param
gabrielcorado Aug 9, 2024
345c14d
refactor(tsh): update getApp retry with login
gabrielcorado Aug 13, 2024
da52380
refactor(tsh): use a single function to grab profile and cluste client
gabrielcorado Aug 15, 2024
af717c8
Merge branch 'master' into gabrielcorado/tsh-app-login-leaf-logins
gabrielcorado Aug 15, 2024
7fea5e9
refactor(tsh): perform retry with login at caller site
gabrielcorado Aug 16, 2024
429f546
fix(tsh): close auth client
gabrielcorado Aug 16, 2024
049fba7
Merge branch 'master' into gabrielcorado/tsh-app-login-leaf-logins
gabrielcorado Aug 16, 2024
a5ce89b
Merge branch 'master' into gabrielcorado/tsh-app-login-leaf-logins
gabrielcorado Aug 29, 2024
a722f88
test(tsh): fix test failing due to login misconfiguration
gabrielcorado Aug 30, 2024
8843221
Merge branch 'master' into gabrielcorado/tsh-app-login-leaf-logins
gabrielcorado Sep 5, 2024
efd8542
Merge branch 'master' into gabrielcorado/tsh-app-login-leaf-logins
gabrielcorado Sep 9, 2024
2740ab8
test(tsh): fix lint errors
gabrielcorado Sep 9, 2024
c5ad14c
Merge branch 'gabrielcorado/tsh-app-login-leaf-logins' of github.com:…
gabrielcorado Sep 9, 2024
954e8cf
test(tsh): remove unused imports
gabrielcorado Sep 10, 2024
8136cfa
Merge branch 'master' into gabrielcorado/tsh-app-login-leaf-logins
gabrielcorado Sep 19, 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
76 changes: 76 additions & 0 deletions lib/client/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -2550,6 +2550,82 @@ func (tc *TeleportClient) ListApps(ctx context.Context, customFilter *proto.List
return types.DeduplicateApps(apps), nil
}

// GetAppWithLogins returns an application with available logins.
func (tc *TeleportClient) GetAppWithLogins(ctx context.Context, predicateExpression string) (types.Application, []string, error) {
ctx, span := tc.Tracer.Start(
ctx,
"teleportClient/GetAppWithLogins",
oteltrace.WithSpanKind(oteltrace.SpanKindClient),
oteltrace.WithAttributes(attribute.String("predicate", predicateExpression)),
)
defer span.End()

clt, err := tc.ConnectToCluster(ctx)
if err != nil {
return nil, nil, trace.Wrap(err)
}
defer clt.Close()
gabrielcorado marked this conversation as resolved.
Show resolved Hide resolved

apps, err := tc.getAppsWithLogins(ctx, clt.AuthClient, predicateExpression)
if err != nil {
return nil, nil, trace.Wrap(err)
}

if len(apps) == 0 {
return nil, nil, trace.NotFound("application not found")
}

return apps[0].app, apps[0].logins, nil
}

// appWithLogins represents an application with its available logins.
type appWithLogins struct {
app types.Application
logins []string
}

// getAppsWithLogins returns a list of apps with their available logins.
func (tc *TeleportClient) getAppsWithLogins(ctx context.Context, clt client.GetResourcesClient, predicateExpression string) ([]appWithLogins, error) {
ctx, span := tc.Tracer.Start(
ctx,
"teleportClient/getAppsWithLogins",
oteltrace.WithSpanKind(oteltrace.SpanKindClient),
)
defer span.End()

req := proto.ListResourcesRequest{
ResourceType: types.KindAppServer,
SortBy: types.SortBy{Field: types.ResourceMetadataName},
PredicateExpression: predicateExpression,
IncludeLogins: true,
}

var apps []appWithLogins
for {
res, err := client.GetEnrichedResourcePage(ctx, clt, &req)
if err != nil {
return nil, trace.Wrap(err)
}

for _, r := range res.Resources {
appServer, ok := r.ResourceWithLabels.(types.AppServer)
if !ok {
log.Warnf("expected types.AppServer but received unexpected type %T", r.ResourceWithLabels)
continue
}

apps = append(apps, appWithLogins{app: appServer.GetApp(), logins: r.Logins})
gabrielcorado marked this conversation as resolved.
Show resolved Hide resolved
}

req.StartKey = res.NextKey
if req.StartKey == "" {
break
}
}

return apps, nil
}

// DeleteAppSession removes the specified application access session.
func (tc *TeleportClient) DeleteAppSession(ctx context.Context, sessionID string) error {
ctx, span := tc.Tracer.Start(
Expand Down
117 changes: 109 additions & 8 deletions lib/client/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
"io"
"math"
"os"
"strconv"
"testing"
"time"

Expand Down Expand Up @@ -1258,16 +1259,35 @@ func TestIsErrorResolvableWithRelogin(t *testing.T) {
type fakeResourceClient struct {
apiclient.GetResourcesClient

nodes []*types.ServerV2
// resources represents the pages returned by GetResources.
resources [][]*proto.PaginatedResource
resourcesErr error
}

func (f fakeResourceClient) GetResources(ctx context.Context, req *proto.ListResourcesRequest) (*proto.ListResourcesResponse, error) {
out := make([]*proto.PaginatedResource, 0, len(f.nodes))
for _, n := range f.nodes {
out = append(out, &proto.PaginatedResource{Resource: &proto.PaginatedResource_Node{Node: n}})
if f.resourcesErr != nil {
return nil, f.resourcesErr
}

return &proto.ListResourcesResponse{Resources: out}, nil
if len(f.resources) == 0 {
return &proto.ListResourcesResponse{}, nil
}

pageIdx, err := strconv.Atoi(req.StartKey)
if req.StartKey != "" && (err != nil || pageIdx >= len(f.resources)) {
return &proto.ListResourcesResponse{}, nil
}

currPage := f.resources[pageIdx]
var nextKey string
if pageIdx+1 < len(f.resources) {
nextKey = strconv.Itoa(pageIdx + 1)
}

return &proto.ListResourcesResponse{
Resources: currPage,
NextKey: nextKey,
}, nil
}

func TestGetTargetNodes(t *testing.T) {
Expand Down Expand Up @@ -1299,19 +1319,25 @@ func TestGetTargetNodes(t *testing.T) {
name: "labels",
labels: map[string]string{"foo": "bar"},
expected: []targetNode{{hostname: "labels", addr: "abcd:0"}},
clt: fakeResourceClient{nodes: []*types.ServerV2{{Metadata: types.Metadata{Name: "abcd"}, Spec: types.ServerSpecV2{Hostname: "labels"}}}},
clt: fakeResourceClient{resources: [][]*proto.PaginatedResource{{
paginatedNode(&types.ServerV2{Metadata: types.Metadata{Name: "abcd"}, Spec: types.ServerSpecV2{Hostname: "labels"}}),
}}},
},
{
name: "search",
search: []string{"foo", "bar"},
expected: []targetNode{{hostname: "search", addr: "abcd:0"}},
clt: fakeResourceClient{nodes: []*types.ServerV2{{Metadata: types.Metadata{Name: "abcd"}, Spec: types.ServerSpecV2{Hostname: "search"}}}},
clt: fakeResourceClient{resources: [][]*proto.PaginatedResource{{
paginatedNode(&types.ServerV2{Metadata: types.Metadata{Name: "abcd"}, Spec: types.ServerSpecV2{Hostname: "search"}}),
}}},
},
{
name: "predicate",
predicate: `resource.spec.hostname == "test"`,
expected: []targetNode{{hostname: "predicate", addr: "abcd:0"}},
clt: fakeResourceClient{nodes: []*types.ServerV2{{Metadata: types.Metadata{Name: "abcd"}, Spec: types.ServerSpecV2{Hostname: "predicate"}}}},
clt: fakeResourceClient{resources: [][]*proto.PaginatedResource{{
paginatedNode(&types.ServerV2{Metadata: types.Metadata{Name: "abcd"}, Spec: types.ServerSpecV2{Hostname: "predicate"}}),
}}},
},
}

Expand All @@ -1334,3 +1360,78 @@ func TestGetTargetNodes(t *testing.T) {
})
}
}

func TestGetAppsWithLogin(t *testing.T) {
greedy52 marked this conversation as resolved.
Show resolved Hide resolved
ctx := context.Background()

for name, tc := range map[string]struct {
clt fakeResourceClient
expectedApps []appWithLogins
expectedErr string
}{
"single app": {
clt: fakeResourceClient{resources: [][]*proto.PaginatedResource{
{paginatedAppServer("sample", []string{"llama", "bird"})},
}},
expectedApps: []appWithLogins{
{app: appWithNameURI("sample"), logins: []string{"llama", "bird"}},
},
},
"multiple pages": {
clt: fakeResourceClient{resources: [][]*proto.PaginatedResource{
{paginatedAppServer("first", []string{"llama", "bird"}),
paginatedAppServer("second", []string{"bob", "alice"})},
{paginatedAppServer("third", []string{}),
paginatedAppServer("forth", []string{"bob"})},
}},
expectedApps: []appWithLogins{
{app: appWithNameURI("first"), logins: []string{"llama", "bird"}},
{app: appWithNameURI("second"), logins: []string{"bob", "alice"}},
{app: appWithNameURI("third"), logins: []string{}},
{app: appWithNameURI("forth"), logins: []string{"bob"}},
},
},
"no apps": {
clt: fakeResourceClient{resources: [][]*proto.PaginatedResource{}},
},
"list error": {
clt: fakeResourceClient{resourcesErr: errors.New("failure")},
expectedErr: "failure",
},
} {
t.Run(name, func(t *testing.T) {
clt := TeleportClient{Config: Config{Tracer: tracing.NoopTracer("")}}
// Given we're mocking the list result, the expression won't be
// considered.
res, err := clt.getAppsWithLogins(ctx, tc.clt, "")
if tc.expectedErr != "" {
require.ErrorContains(t, err, tc.expectedErr)
return
}

require.NoError(t, err)
require.EqualValues(t, tc.expectedApps, res)
})
}
}

func paginatedNode(node *types.ServerV2) *proto.PaginatedResource {
return &proto.PaginatedResource{Resource: &proto.PaginatedResource_Node{Node: node}}
}

func appWithNameURI(name string) *types.AppV3 {
return &types.AppV3{Metadata: types.Metadata{Name: name}, Spec: types.AppSpecV3{URI: name}}
}

func paginatedAppServer(name string, logins []string) *proto.PaginatedResource {
return &proto.PaginatedResource{
Logins: logins,
Resource: &proto.PaginatedResource_AppServer{
AppServer: &types.AppServerV3{
Spec: types.AppServerSpecV3{
App: appWithNameURI(name),
},
},
},
}
}
32 changes: 32 additions & 0 deletions tool/teleport/testenv/test_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,10 @@ func waitForServices(t *testing.T, auth *service.TeleportProcess, cfg *servicecf
if cfg.Auth.Enabled && cfg.Databases.Enabled {
waitForDatabases(t, auth, cfg.Databases.Databases)
}

if cfg.Auth.Enabled && cfg.Apps.Enabled {
waitForApps(t, auth, cfg.Apps.Apps)
}
}

func waitForEvents(t *testing.T, svc service.Supervisor, events ...string) {
Expand Down Expand Up @@ -291,6 +295,34 @@ func waitForDatabases(t *testing.T, auth *service.TeleportProcess, dbs []service
}
}

func waitForApps(t *testing.T, auth *service.TeleportProcess, apps []servicecfg.App) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
for {
select {
case <-time.After(500 * time.Millisecond):
all, err := auth.GetAuthServer().GetApplicationServers(ctx, apidefaults.Namespace)
require.NoError(t, err)

var registered int
for _, app := range apps {
for _, a := range all {
if a.GetName() == app.Name {
registered++
break
}
}
}

if registered == len(apps) {
return
}
case <-ctx.Done():
t.Fatal("Apps not registered after 10s")
}
}
}

type TestServersOpts struct {
Bootstrap []types.Resource
ConfigFuncs []func(cfg *servicecfg.Config)
Expand Down
31 changes: 16 additions & 15 deletions tool/tsh/common/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -518,11 +518,15 @@ func getAppInfo(cf *CLIConf, tc *client.TeleportClient, matchRouteToApp func(tls
}

// If we didn't find an active profile for the app, get info from server.
app, err := getApp(cf.Context, tc, cf.AppName)
app, logins, err := getApp(cf.Context, tc, cf.AppName)
if err != nil {
return nil, trace.Wrap(err)
}

if len(logins) == 0 && app.IsAWSConsole() {
logins = getARNFromRoles(cf, tc, profile, app)
rosstimothy marked this conversation as resolved.
Show resolved Hide resolved
}

appInfo := &appInfo{
profile: profile,
RouteToApp: proto.RouteToApp{
Expand All @@ -537,7 +541,7 @@ func getAppInfo(cf *CLIConf, tc *client.TeleportClient, matchRouteToApp func(tls
// If this is a cloud app, set additional applicable fields from CLI flags or roles.
switch {
case app.IsAWSConsole():
awsRoleARN, err := getARNFromFlags(cf, profile, app)
awsRoleARN, err := getARNFromFlags(cf, app, logins)
if err != nil {
return nil, trace.Wrap(err)
}
Expand Down Expand Up @@ -592,7 +596,7 @@ func (a *appInfo) GetApp(ctx context.Context, tc *client.TeleportClient) (types.
return a.app.Copy(), nil
}
// holding mutex across the api call to avoid multiple redundant api calls.
app, err := getApp(ctx, tc, a.Name)
app, _, err := getApp(ctx, tc, a.Name)
if err != nil {
return nil, trace.Wrap(err)
}
Expand All @@ -601,23 +605,20 @@ func (a *appInfo) GetApp(ctx context.Context, tc *client.TeleportClient) (types.
}

// getApp returns the registered application with the specified name.
func getApp(ctx context.Context, tc *client.TeleportClient, name string) (app types.Application, err error) {
var apps []types.Application
func getApp(ctx context.Context, tc *client.TeleportClient, name string) (app types.Application, logins []string, err error) {
err = client.RetryWithRelogin(ctx, tc, func() error {
apps, err = tc.ListApps(ctx, &proto.ListResourcesRequest{
Namespace: tc.Namespace,
ResourceType: types.KindAppServer,
PredicateExpression: fmt.Sprintf(`name == "%s"`, name),
})
app, logins, err = tc.GetAppWithLogins(ctx, fmt.Sprintf(`name == "%s"`, name))
return trace.Wrap(err)
})
if err != nil {
return nil, trace.Wrap(err)
}
if len(apps) == 0 {
return nil, trace.NotFound("app %q not found, use `tsh apps ls` to see registered apps", name)
if trace.IsNotFound(err) {
return nil, nil, trace.NotFound("app %q not found, use `tsh apps ls` to see registered apps", name)
}

return nil, nil, trace.Wrap(err)
}
return apps[0], nil

return
}

// pickActiveApp returns the app the current profile is logged into.
Expand Down
Loading
Loading