From 2db4bc0dd763f94df01db9cb3eda8e57d9883372 Mon Sep 17 00:00:00 2001 From: Gabriel Corado Date: Thu, 25 Jul 2024 11:53:41 -0300 Subject: [PATCH] feat: include leaf cluster allowed logins for AWS console applications (#44611) --- api/client/client.go | 2 +- api/client/client_test.go | 6 +++++ lib/auth/auth_with_roles.go | 7 +++++ lib/auth/auth_with_roles_test.go | 44 +++++++++++++++++++++++++++----- lib/services/access_checker.go | 19 +++++++------- lib/web/apiserver.go | 17 +++++++++++- lib/web/apiserver_test.go | 41 +++++++++++++++++++++++++++++ 7 files changed, 119 insertions(+), 17 deletions(-) diff --git a/api/client/client.go b/api/client/client.go index c5efa29d2493c..0313deb561e4c 100644 --- a/api/client/client.go +++ b/api/client/client.go @@ -3670,7 +3670,7 @@ func convertEnrichedResource(resource *proto.PaginatedResource) (*types.Enriched } else if r := resource.GetUserGroup(); r != nil { return &types.EnrichedResource{ResourceWithLabels: r, RequiresRequest: resource.RequiresRequest}, nil } else if r := resource.GetAppServer(); r != nil { - return &types.EnrichedResource{ResourceWithLabels: r, RequiresRequest: resource.RequiresRequest}, nil + return &types.EnrichedResource{ResourceWithLabels: r, Logins: resource.Logins, RequiresRequest: resource.RequiresRequest}, nil } else if r := resource.GetSAMLIdPServiceProvider(); r != nil { return &types.EnrichedResource{ResourceWithLabels: r, RequiresRequest: resource.RequiresRequest}, nil } else { diff --git a/api/client/client_test.go b/api/client/client_test.go index 33d0c1d8f9946..788ad961a8d8f 100644 --- a/api/client/client_test.go +++ b/api/client/client_test.go @@ -732,6 +732,10 @@ func TestGetUnifiedResourcesWithLogins(t *testing.T) { Resource: &proto.PaginatedResource_WindowsDesktop{WindowsDesktop: &types.WindowsDesktopV3{}}, Logins: []string{"llama"}, }, + { + Resource: &proto.PaginatedResource_AppServer{AppServer: &types.AppServerV3{}}, + Logins: []string{"llama"}, + }, }, }, } @@ -753,6 +757,8 @@ func TestGetUnifiedResourcesWithLogins(t *testing.T) { assert.Equal(t, enriched.Logins, clt.resp.Resources[0].Logins) case *types.WindowsDesktopV3: assert.Equal(t, enriched.Logins, clt.resp.Resources[1].Logins) + case *types.AppServerV3: + assert.Equal(t, enriched.Logins, clt.resp.Resources[2].Logins) } } } diff --git a/lib/auth/auth_with_roles.go b/lib/auth/auth_with_roles.go index 1f2b5d06c2cb5..83bcd6099d310 100644 --- a/lib/auth/auth_with_roles.go +++ b/lib/auth/auth_with_roles.go @@ -1483,6 +1483,13 @@ func (a *ServerWithRoles) ListUnifiedResources(ctx context.Context, req *proto.L continue } r.Logins = logins + } else if d := r.GetAppServer(); d != nil { + logins, err := checker.GetAllowedLoginsForResource(d.GetApp()) + if err != nil { + log.WithError(err).WithField("resource", d.GetApp().GetName()).Warn("Unable to determine logins for app") + continue + } + r.Logins = logins } } } diff --git a/lib/auth/auth_with_roles_test.go b/lib/auth/auth_with_roles_test.go index 9cde844c32762..12f01e5dcc77a 100644 --- a/lib/auth/auth_with_roles_test.go +++ b/lib/auth/auth_with_roles_test.go @@ -3764,6 +3764,21 @@ func TestListResources_WithLogins(t *testing.T) { require.NoError(t, err) require.NoError(t, srv.Auth().UpsertWindowsDesktop(ctx, desktop)) + + awsApp, err := types.NewAppServerV3(types.Metadata{Name: name}, types.AppServerSpecV3{ + HostID: "_", + Hostname: "_", + App: &types.AppV3{ + Metadata: types.Metadata{Name: fmt.Sprintf("name-%d", i)}, + Spec: types.AppSpecV3{ + URI: "https://console.aws.amazon.com/ec2/v2/home", + }, + }, + }) + require.NoError(t, err) + + _, err = srv.Auth().UpsertApplicationServer(ctx, awsApp) + require.NoError(t, err) } // create user and client @@ -3772,6 +3787,7 @@ func TestListResources_WithLogins(t *testing.T) { require.NoError(t, err) role.SetWindowsDesktopLabels(types.Allow, types.Labels{types.Wildcard: []string{types.Wildcard}}) role.SetWindowsLogins(types.Allow, logins) + role.SetAWSRoleARNs(types.Allow, logins) _, err = srv.Auth().UpdateRole(ctx, role) require.NoError(t, err) @@ -3797,10 +3813,10 @@ func TestListResources_WithLogins(t *testing.T) { start = resp.NextKey } - // Check that only server and desktop resources contain the expected logins + // Check that only server, desktop, and app server resources contain the expected logins for _, resource := range results { switch resource.ResourceWithLabels.(type) { - case types.Server, types.WindowsDesktop: + case types.Server, types.WindowsDesktop, types.AppServer: require.Empty(t, cmp.Diff(resource.Logins, logins, cmpopts.SortSlices(func(a, b string) bool { return strings.Compare(a, b) < 0 }))) @@ -3829,10 +3845,10 @@ func TestListResources_WithLogins(t *testing.T) { start = resp.NextKey } - // Check that only server and desktop resources contain the expected logins + // Check that only server, desktop, and app server resources contain the expected logins for _, resource := range results { switch resource.ResourceWithLabels.(type) { - case types.Server, types.WindowsDesktop: + case types.Server, types.WindowsDesktop, types.AppServer: require.Empty(t, cmp.Diff(resource.Logins, logins, cmpopts.SortSlices(func(a, b string) bool { return strings.Compare(a, b) < 0 }))) @@ -4753,6 +4769,21 @@ func TestListUnifiedResources_WithLogins(t *testing.T) { require.NoError(t, err) require.NoError(t, srv.Auth().UpsertWindowsDesktop(ctx, desktop)) + + awsApp, err := types.NewAppServerV3(types.Metadata{Name: name}, types.AppServerSpecV3{ + HostID: "_", + Hostname: "_", + App: &types.AppV3{ + Metadata: types.Metadata{Name: fmt.Sprintf("name-%d", i)}, + Spec: types.AppSpecV3{ + URI: "https://console.aws.amazon.com/ec2/v2/home", + }, + }, + }) + require.NoError(t, err) + + _, err = srv.Auth().UpsertApplicationServer(ctx, awsApp) + require.NoError(t, err) } // create user and client @@ -4761,6 +4792,7 @@ func TestListUnifiedResources_WithLogins(t *testing.T) { require.NoError(t, err) role.SetWindowsDesktopLabels(types.Allow, types.Labels{types.Wildcard: []string{types.Wildcard}}) role.SetWindowsLogins(types.Allow, logins) + role.SetAWSRoleARNs(types.Allow, logins) _, err = srv.Auth().UpdateRole(ctx, role) require.NoError(t, err) @@ -4782,9 +4814,9 @@ func TestListUnifiedResources_WithLogins(t *testing.T) { start = resp.NextKey } - // Check that only server and desktop resources contain the expected logins + // Check that only server, desktop, and app server resources contain the expected logins for _, resource := range results { - if resource.GetNode() != nil || resource.GetWindowsDesktop() != nil { + if resource.GetNode() != nil || resource.GetWindowsDesktop() != nil || resource.GetAppServer() != nil { require.Empty(t, cmp.Diff(resource.Logins, logins, cmpopts.SortSlices(func(a, b string) bool { return strings.Compare(a, b) < 0 }))) diff --git a/lib/services/access_checker.go b/lib/services/access_checker.go index 1ed3e38880e72..b4ccd0cfc0c48 100644 --- a/lib/services/access_checker.go +++ b/lib/services/access_checker.go @@ -259,8 +259,8 @@ type AccessChecker interface { // Supports the following resource types: // // - types.Server with GetKind() == types.KindNode - // // - types.KindWindowsDesktop + // - types.KindApp with IsAWSConsole() == true GetAllowedLoginsForResource(resource AccessCheckable) ([]string, error) // CheckSPIFFESVID checks if the role set has access to generating the @@ -767,8 +767,8 @@ func (a *accessChecker) EnumerateEntities(resource AccessCheckable, listFn roleE // Supports the following resource types: // // - types.Server with GetKind() == types.KindNode -// // - types.KindWindowsDesktop +// - types.KindApp with IsAWSConsole() == true func (a *accessChecker) GetAllowedLoginsForResource(resource AccessCheckable) ([]string, error) { // Create a map indexed by all logins in the RoleSet, // mapped to false if any role has it in its deny section, @@ -1228,14 +1228,15 @@ func AccessInfoFromLocalIdentity(identity tlsca.Identity, access UserGetter) (*A // local roles based on the given roleMap. func AccessInfoFromRemoteIdentity(identity tlsca.Identity, roleMap types.RoleMap) (*AccessInfo, error) { // Set internal traits for the remote user. This allows Teleport to work by - // passing exact logins, Kubernetes users/groups and database users/names - // to the remote cluster. + // passing exact logins, Kubernetes users/groups, database users/names, and + // AWS Role ARNs to the remote cluster. traits := map[string][]string{ - constants.TraitLogins: identity.Principals, - constants.TraitKubeGroups: identity.KubernetesGroups, - constants.TraitKubeUsers: identity.KubernetesUsers, - constants.TraitDBNames: identity.DatabaseNames, - constants.TraitDBUsers: identity.DatabaseUsers, + constants.TraitLogins: identity.Principals, + constants.TraitKubeGroups: identity.KubernetesGroups, + constants.TraitKubeUsers: identity.KubernetesUsers, + constants.TraitDBNames: identity.DatabaseNames, + constants.TraitDBUsers: identity.DatabaseUsers, + constants.TraitAWSRoleARNs: identity.AWSRoleARNs, } // Prior to Teleport 6.2 no user traits were passed to remote clusters // except for the internal ones specified above. diff --git a/lib/web/apiserver.go b/lib/web/apiserver.go index 8716904b2ad93..0d23f7c272e4b 100644 --- a/lib/web/apiserver.go +++ b/lib/web/apiserver.go @@ -2746,6 +2746,21 @@ func calculateDesktopLogins(loginGetter loginGetter, r types.ResourceWithLabels, return logins, trace.Wrap(err) } +// calculateAppLogins determines the app logins allowed for the provided +// resource. +// +// TODO(gabrielcorado): DELETE IN V18.0.0 +// This is here for backward compatibility in case the auth server +// does not support enriched resources yet. +func calculateAppLogins(loginGetter loginGetter, r types.AppServer, allowedLogins []string) ([]string, error) { + if len(allowedLogins) > 0 { + return allowedLogins, nil + } + + logins, err := loginGetter.GetAllowedLoginsForResource(r.GetApp()) + return logins, trace.Wrap(err) +} + // getUserGroupLookup is a generator to retrieve UserGroupLookup on first call and return it again in subsequent calls. // If we encounter an error, we log it once and return an empty UserGroupLookup for the current and subsequent calls. // The returned function is not thread safe. @@ -2829,7 +2844,7 @@ func (h *Handler) clusterUnifiedResourcesGet(w http.ResponseWriter, request *htt db := ui.MakeDatabase(r.GetDatabase(), dbUsers, dbNames, enriched.RequiresRequest) unifiedResources = append(unifiedResources, db) case types.AppServer: - allowedAWSRoles, err := accessChecker.GetAllowedLoginsForResource(r.GetApp()) + allowedAWSRoles, err := calculateAppLogins(accessChecker, r, enriched.Logins) if err != nil { return nil, trace.Wrap(err) } diff --git a/lib/web/apiserver_test.go b/lib/web/apiserver_test.go index 18e2949323739..0448541253976 100644 --- a/lib/web/apiserver_test.go +++ b/lib/web/apiserver_test.go @@ -10348,6 +10348,47 @@ func TestCalculateDesktopLogins(t *testing.T) { } } +func TestCalculateAppLogins(t *testing.T) { + cases := []struct { + name string + allowedLogins []string + expectedLogins []string + loginGetter loginGetterFunc + }{ + { + name: "allowed logins", + allowedLogins: []string{"llama", "fish", "dog"}, + expectedLogins: []string{"llama", "fish", "dog"}, + loginGetter: func(_ services.AccessCheckable) ([]string, error) { + return nil, nil + }, + }, + { + name: "no allowed logins", + loginGetter: func(_ services.AccessCheckable) ([]string, error) { + return nil, nil + }, + }, + { + name: "no allowed logins with fallback", + expectedLogins: []string{"apple", "banana"}, + loginGetter: func(_ services.AccessCheckable) ([]string, error) { + return []string{"apple", "banana"}, nil + }, + }, + } + + for _, test := range cases { + t.Run(test.name, func(t *testing.T) { + logins, err := calculateAppLogins(test.loginGetter, &types.AppServerV3{}, test.allowedLogins) + require.NoError(t, err) + require.Empty(t, cmp.Diff(logins, test.expectedLogins, cmpopts.SortSlices(func(a, b string) bool { + return strings.Compare(a, b) < 0 + }))) + }) + } +} + type loginGetterFunc func(resource services.AccessCheckable) ([]string, error) func (f loginGetterFunc) GetAllowedLoginsForResource(resource services.AccessCheckable) ([]string, error) {