Skip to content

Commit

Permalink
feat: include leaf cluster allowed logins for AWS console applications (
Browse files Browse the repository at this point in the history
  • Loading branch information
gabrielcorado authored Jul 25, 2024
1 parent 1b36768 commit 2db4bc0
Show file tree
Hide file tree
Showing 7 changed files with 119 additions and 17 deletions.
2 changes: 1 addition & 1 deletion api/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
6 changes: 6 additions & 0 deletions api/client/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"},
},
},
},
}
Expand All @@ -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)
}
}
}
7 changes: 7 additions & 0 deletions lib/auth/auth_with_roles.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
}
Expand Down
44 changes: 38 additions & 6 deletions lib/auth/auth_with_roles_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)

Expand All @@ -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
})))
Expand Down Expand Up @@ -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
})))
Expand Down Expand Up @@ -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
Expand All @@ -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)

Expand All @@ -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
})))
Expand Down
19 changes: 10 additions & 9 deletions lib/services/access_checker.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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.
Expand Down
17 changes: 16 additions & 1 deletion lib/web/apiserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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)
}
Expand Down
41 changes: 41 additions & 0 deletions lib/web/apiserver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down

0 comments on commit 2db4bc0

Please sign in to comment.