Skip to content

Commit

Permalink
[v10] Allow updating of trusted cluster role maps (#18168) (#23645)
Browse files Browse the repository at this point in the history
* Allow updating of trusted cluster role maps (#18168)

* Fix tests for updating role maps
  • Loading branch information
EdwardDowling committed Apr 3, 2023
1 parent 7bfbf3b commit 47793d3
Show file tree
Hide file tree
Showing 10 changed files with 766 additions and 26 deletions.
6 changes: 2 additions & 4 deletions api/types/trustedcluster.go
Expand Up @@ -250,11 +250,9 @@ func (c *TrustedClusterV2) CanChangeStateTo(t TrustedCluster) error {
if !utils.StringSlicesEqual(c.GetRoles(), t.GetRoles()) {
return immutableFieldErr("roles")
}
if !cmp.Equal(c.GetRoleMap(), t.GetRoleMap()) {
return immutableFieldErr("role_map")
}
roleMapUpdated := !cmp.Equal(c.GetRoleMap(), t.GetRoleMap())

if c.GetEnabled() == t.GetEnabled() {
if c.GetEnabled() == t.GetEnabled() && !roleMapUpdated {
if t.GetEnabled() {
return trace.AlreadyExists("leaf cluster is already enabled, this update would have no effect")
}
Expand Down
12 changes: 10 additions & 2 deletions integration/helpers.go
Expand Up @@ -674,13 +674,20 @@ func nullImpersonationCheck(context.Context, string, authztypes.SelfSubjectAcces
// Unlike Create() it allows for greater customization because it accepts
// a full Teleport config structure
func (i *TeleInstance) CreateEx(t *testing.T, trustedSecrets []*InstanceSecrets, tconf *service.Config) error {
ctx := context.TODO()
tconf, err := i.GenerateConfig(t, trustedSecrets, tconf)
if err != nil {
return trace.Wrap(err)
}

return i.CreateWithConf(t, tconf)
}

// CreateWithConf creates a new instance of Teleport using the supplied config
func (i *TeleInstance) CreateWithConf(_ *testing.T, tconf *service.Config) error {
i.Config = tconf
i.Process, err = service.NewTeleport(tconf, service.WithIMDSClient(&disabledIMDSClient{}))

var err error
i.Process, err = service.NewTeleport(tconf)
if err != nil {
return trace.Wrap(err)
}
Expand All @@ -694,6 +701,7 @@ func (i *TeleInstance) CreateEx(t *testing.T, trustedSecrets []*InstanceSecrets,
// create users and roles if they don't exist, or sign their keys if they're
// already present
auth := i.Process.GetAuthServer()
ctx := context.TODO()

for _, user := range i.Secrets.Users {
teleUser, err := types.NewUser(user.Username)
Expand Down
313 changes: 313 additions & 0 deletions integration/integration_test.go
Expand Up @@ -203,6 +203,8 @@ func TestIntegrations(t *testing.T) {
t.Run("SSHExitCode", suite.bind(testSSHExitCode))
t.Run("Shutdown", suite.bind(testShutdown))
t.Run("TrustedClusters", suite.bind(testTrustedClusters))
t.Run("TrustedDisabledClusters", suite.bind(testDisabledTrustedClusters))
t.Run("TrustedClustersRoleMapChanges", suite.bind(testTrustedClustersRoleMapChanges))
t.Run("TrustedClustersWithLabels", suite.bind(testTrustedClustersWithLabels))
t.Run("TrustedTunnelNode", suite.bind(testTrustedTunnelNode))
t.Run("TwoClustersProxy", suite.bind(testTwoClustersProxy))
Expand Down Expand Up @@ -2374,6 +2376,22 @@ func testTrustedClusters(t *testing.T, suite *integrationTestSuite) {
trustedClusters(t, suite, trustedClusterTest{multiplex: false})
}

// testDisabledTrustedClusters tests creation of disabled trusted cluster
func testDisabledTrustedClusters(t *testing.T, suite *integrationTestSuite) {
tr := utils.NewTracer(utils.ThisFunction()).Start()
defer tr.Stop()

trustedDisabledCluster(t, suite, trustedClusterTest{multiplex: false})
}

// testTrustedClustersRoleMapChanges tests the changing of role maps for trusted clusters
func testTrustedClustersRoleMapChanges(t *testing.T, suite *integrationTestSuite) {
tr := utils.NewTracer(utils.ThisFunction()).Start()
defer tr.Stop()

trustedClustersRoleMapChanges(t, suite, trustedClusterTest{multiplex: false})
}

// TestTrustedClustersWithLabels tests remote clusters scenarios
// using trusted clusters feature and access labels
func testTrustedClustersWithLabels(t *testing.T, suite *integrationTestSuite) {
Expand Down Expand Up @@ -2669,6 +2687,301 @@ func waitForClusters(tun reversetunnel.Server, expected int) func() bool {
}
}

func trustedDisabledCluster(t *testing.T, suite *integrationTestSuite, test trustedClusterTest) {
ctx := context.Background()
username := suite.me.Username

clusterMain := "cluster-main"
clusterAux := "cluster-aux"
mainCfg := InstanceConfig{
ClusterName: clusterMain,
HostID: HostID,
NodeName: Host,
Priv: suite.priv,
Pub: suite.pub,
Log: suite.log,
}

main := NewInstance(mainCfg)
aux := suite.newNamedTeleportInstance(t, clusterAux)

// main cluster has a local user and belongs to role "main-devs" and "main-admins"
mainDevs := "main-devs"
devsRole, err := types.NewRole(mainDevs, types.RoleSpecV5{
Allow: types.RoleConditions{
Logins: []string{username},
NodeLabels: types.Labels{types.Wildcard: []string{types.Wildcard}},
},
})
// If the test is using labels, the cluster will be labeled
// and user will be granted access if labels match.
// Otherwise, to preserve backwards-compatibility
// roles with no labels will grant access to clusters with no labels.
if test.useLabels {
devsRole.SetClusterLabels(types.Allow, types.Labels{"access": []string{"prod"}})
}
require.NoError(t, err)

mainAdmins := "main-admins"
adminsRole, err := types.NewRole(mainAdmins, types.RoleSpecV5{
Allow: types.RoleConditions{
Logins: []string{"superuser"},
NodeLabels: types.Labels{types.Wildcard: []string{types.Wildcard}},
},
})
require.NoError(t, err)

main.AddUserWithRole(username, devsRole, adminsRole)

// Ops users can only access remote clusters with label 'access': 'ops'
mainOps := "main-ops"
mainOpsRole, err := types.NewRole(mainOps, types.RoleSpecV5{
Allow: types.RoleConditions{
Logins: []string{username},
ClusterLabels: types.Labels{"access": []string{"ops"}},
NodeLabels: types.Labels{types.Wildcard: []string{types.Wildcard}},
},
})
require.NoError(t, err)
main.AddUserWithRole(mainOps, mainOpsRole, adminsRole)

// for role mapping test we turn on Web API on the main cluster
// as it's used
makeConfig := func(instance *TeleInstance, enableSSH bool) (*testing.T, *service.Config) {
tconf := suite.defaultServiceConfig()
tconf.Proxy.DisableWebService = false
tconf.Proxy.DisableWebInterface = true
tconf.SSH.Enabled = enableSSH
tconf, err := instance.GenerateConfig(t, nil, tconf)
require.NoError(t, err)

tconf.CachePolicy.Enabled = false

return t, tconf
}
lib.SetInsecureDevMode(true)
defer lib.SetInsecureDevMode(false)

require.NoError(t, main.CreateWithConf(makeConfig(main, false)))
require.NoError(t, aux.CreateWithConf(makeConfig(aux, true)))

// auxiliary cluster has only a role aux-devs
// connect aux cluster to main cluster
// using trusted clusters, so remote user will be allowed to assume
// role specified by mapping remote role "devs" to local role "local-devs"
auxDevs := "aux-devs"
auxRole, err := types.NewRole(auxDevs, types.RoleSpecV5{
Allow: types.RoleConditions{
Logins: []string{username},
NodeLabels: types.Labels{types.Wildcard: []string{types.Wildcard}},
},
})
require.NoError(t, err)
err = aux.Process.GetAuthServer().UpsertRole(ctx, auxRole)
require.NoError(t, err)

trustedClusterToken := "trusted-cluster-token"
tokenResource, err := types.NewProvisionToken(trustedClusterToken, []types.SystemRole{types.RoleTrustedCluster}, time.Time{})
require.NoError(t, err)
if test.useLabels {
meta := tokenResource.GetMetadata()
meta.Labels = map[string]string{"access": "prod"}
tokenResource.SetMetadata(meta)
}
err = main.Process.GetAuthServer().UpsertToken(ctx, tokenResource)
require.NoError(t, err)
// Note that the mapping omits admins role, this is to cover the scenario
// when root cluster and leaf clusters have different role sets
trustedCluster := main.AsTrustedCluster(trustedClusterToken, types.RoleMap{
{Remote: mainDevs, Local: []string{auxDevs}},
{Remote: mainOps, Local: []string{auxDevs}},
})

// modify trusted cluster resource name, so it would not
// match the cluster name to check that it does not matter
trustedCluster.SetName(main.Secrets.SiteName + "-cluster")
// disable cluster
trustedCluster.SetEnabled(false)

require.NoError(t, main.Start())
require.NoError(t, aux.Start())

err = trustedCluster.CheckAndSetDefaults()
require.NoError(t, err)

// try and upsert a trusted cluster while disabled
TryCreateTrustedCluster(t, aux.Process.GetAuthServer(), trustedCluster)

// try to enable disabled cluster
trustedCluster.SetEnabled(true)
TryCreateTrustedCluster(t, aux.Process.GetAuthServer(), trustedCluster)
WaitForTunnelConnections(t, main.Process.GetAuthServer(), clusterAux, 1)

CheckTrustedClustersCanConnect(ctx, t, TrustedClusterSetup{
Aux: aux,
Main: main,
Username: username,
ClusterAux: clusterAux,
UseJumpHost: test.useJumpHost,
})

// stop clusters and remaining nodes
require.NoError(t, main.StopAll())
require.NoError(t, aux.StopAll())
}

func trustedClustersRoleMapChanges(t *testing.T, suite *integrationTestSuite, test trustedClusterTest) {
ctx := context.Background()
username := suite.me.Username

clusterMain := "cluster-main"
clusterAux := "cluster-aux"
mainCfg := InstanceConfig{
ClusterName: clusterMain,
HostID: HostID,
NodeName: Host,
Priv: suite.priv,
Pub: suite.pub,
Log: suite.log,
}

main := NewInstance(mainCfg)
aux := suite.newNamedTeleportInstance(t, clusterAux)

// main cluster has a local user and belongs to role "main-devs" and "main-admins"
mainDevs := "main-devs"
devsRole, err := types.NewRole(mainDevs, types.RoleSpecV5{
Allow: types.RoleConditions{
Logins: []string{username},
NodeLabels: types.Labels{types.Wildcard: []string{types.Wildcard}},
},
})
// If the test is using labels, the cluster will be labeled
// and user will be granted access if labels match.
// Otherwise, to preserve backwards-compatibility
// roles with no labels will grant access to clusters with no labels.
if test.useLabels {
devsRole.SetClusterLabels(types.Allow, types.Labels{"access": []string{"prod"}})
}
require.NoError(t, err)

mainAdmins := "main-admins"
adminsRole, err := types.NewRole(mainAdmins, types.RoleSpecV5{
Allow: types.RoleConditions{
Logins: []string{"superuser"},
NodeLabels: types.Labels{types.Wildcard: []string{types.Wildcard}},
},
})
require.NoError(t, err)

main.AddUserWithRole(username, devsRole, adminsRole)

// Ops users can only access remote clusters with label 'access': 'ops'
mainOps := "main-ops"
mainOpsRole, err := types.NewRole(mainOps, types.RoleSpecV5{
Allow: types.RoleConditions{
Logins: []string{username},
ClusterLabels: types.Labels{"access": []string{"ops"}},
NodeLabels: types.Labels{types.Wildcard: []string{types.Wildcard}},
},
})
require.NoError(t, err)
main.AddUserWithRole(mainOps, mainOpsRole, adminsRole)

// for role mapping test we turn on Web API on the main cluster
// as it's used
makeConfig := func(instance *TeleInstance, enableSSH bool) (*testing.T, *service.Config) {
tconf := suite.defaultServiceConfig()
tconf.Proxy.DisableWebService = false
tconf.Proxy.DisableWebInterface = true
tconf.SSH.Enabled = enableSSH
tconf, err := instance.GenerateConfig(t, nil, tconf)
require.NoError(t, err)

tconf.CachePolicy.Enabled = false
return t, tconf
}
lib.SetInsecureDevMode(true)
defer lib.SetInsecureDevMode(false)

require.NoError(t, main.CreateWithConf(makeConfig(main, false)))
require.NoError(t, aux.CreateWithConf(makeConfig(aux, true)))

// auxiliary cluster has only a role aux-devs
// connect aux cluster to main cluster
// using trusted clusters, so remote user will be allowed to assume
// role specified by mapping remote role "devs" to local role "local-devs"
auxDevs := "aux-devs"
auxRole, err := types.NewRole(auxDevs, types.RoleSpecV5{
Allow: types.RoleConditions{
Logins: []string{username},
NodeLabels: types.Labels{types.Wildcard: []string{types.Wildcard}},
},
})
require.NoError(t, err)
err = aux.Process.GetAuthServer().UpsertRole(ctx, auxRole)
require.NoError(t, err)

trustedClusterToken := "trusted-cluster-token"
tokenResource, err := types.NewProvisionToken(trustedClusterToken, []types.SystemRole{types.RoleTrustedCluster}, time.Time{})
require.NoError(t, err)
if test.useLabels {
meta := tokenResource.GetMetadata()
meta.Labels = map[string]string{"access": "prod"}
tokenResource.SetMetadata(meta)
}
err = main.Process.GetAuthServer().UpsertToken(ctx, tokenResource)
require.NoError(t, err)
// Note that the mapping omits admins role, this is to cover the scenario
// when root cluster and leaf clusters have different role sets
trustedCluster := main.AsTrustedCluster(trustedClusterToken, types.RoleMap{
{Remote: mainOps, Local: []string{auxDevs}},
})

// modify trusted cluster resource name, so it would not
// match the cluster name to check that it does not matter
trustedCluster.SetName(main.Secrets.SiteName + "-cluster")

require.NoError(t, main.Start())
require.NoError(t, aux.Start())

err = trustedCluster.CheckAndSetDefaults()
require.NoError(t, err)

TryCreateTrustedCluster(t, aux.Process.GetAuthServer(), trustedCluster)
WaitForTunnelConnections(t, main.Process.GetAuthServer(), clusterAux, 1)

// change role mapping to ensure updating works
trustedCluster.SetRoleMap(types.RoleMap{
{Remote: mainDevs, Local: []string{auxDevs}},
})

TryCreateTrustedCluster(t, aux.Process.GetAuthServer(), trustedCluster)
WaitForTunnelConnections(t, main.Process.GetAuthServer(), clusterAux, 1)

CheckTrustedClustersCanConnect(ctx, t, TrustedClusterSetup{
Aux: aux,
Main: main,
Username: username,
ClusterAux: clusterAux,
UseJumpHost: test.useJumpHost,
})

// disable the enabled trusted cluster and ensure it no longer works
trustedCluster.SetEnabled(false)
TryCreateTrustedCluster(t, aux.Process.GetAuthServer(), trustedCluster)

// Wait for both cluster to no longer see each other via reverse tunnels.
require.Eventually(t, WaitForClusters(main.Tunnel, 0), 10*time.Second, 1*time.Second,
"Two clusters still see eachother after being disabled.")
require.Eventually(t, WaitForClusters(aux.Tunnel, 0), 10*time.Second, 1*time.Second,
"Two clusters still see eachother after being disabled.")

// stop clusters and remaining nodes
require.NoError(t, main.StopAll())
require.NoError(t, aux.StopAll())
}

func testTrustedTunnelNode(t *testing.T, suite *integrationTestSuite) {
ctx := context.Background()
username := suite.me.Username
Expand Down

0 comments on commit 47793d3

Please sign in to comment.