diff --git a/constants.go b/constants.go index 90d83cb46e304..fe1b0021cf782 100644 --- a/constants.go +++ b/constants.go @@ -934,3 +934,10 @@ const ( // debug service. DebugServiceSocketName = "debug.sock" ) + +const ( + // OktaAccessRoleContext is the context used to name Okta Access role created by Okta access list sync + OktaAccessRoleContext = "access-okta-acl-role" + // OktaReviewerRoleContext is the context used to name Okta Reviewer role created by Okta Access List sync + OktaReviewerRoleContext = "reviewer-okta-acl-role" +) diff --git a/tool/tsh/common/tsh.go b/tool/tsh/common/tsh.go index 379e947c56e17..a3f8013ecc055 100644 --- a/tool/tsh/common/tsh.go +++ b/tool/tsh/common/tsh.go @@ -4440,7 +4440,7 @@ func printStatus(debug bool, p *profileInfo, env map[string]string, isActive boo if cluster != "" { fmt.Printf(" Cluster: %v\n", cluster) } - fmt.Printf(" Roles: %v\n", strings.Join(p.Roles, ", ")) + fmt.Printf(" Roles: %v\n", rolesToString(debug, p.Roles)) if debug { var count int for k, v := range p.Traits { @@ -4498,6 +4498,35 @@ func printStatus(debug bool, p *profileInfo, env map[string]string, isActive boo fmt.Printf("\n") } +func isOktaRole(role string) bool { + return strings.Contains(role, teleport.OktaReviewerRoleContext) || strings.Contains(role, teleport.OktaAccessRoleContext) +} + +func rolesToString(debug bool, roles []string) string { + sort.Strings(roles) + var nonOktaRoles, oktaRoles []string + for _, role := range roles { + if isOktaRole(role) { + oktaRoles = append(oktaRoles, role) + } else { + nonOktaRoles = append(nonOktaRoles, role) + } + } + if len(oktaRoles) == 0 { + return strings.Join(nonOktaRoles, ", ") + } + + squashRolesThreshold := 9 + + if !debug && len(nonOktaRoles)+len(oktaRoles) > squashRolesThreshold { + oktaRolesText := fmt.Sprintf("and %v more Okta access list roles ...", len(oktaRoles)) + return strings.Join(append(nonOktaRoles, oktaRolesText), ", ") + } + // Keep okta roles at the end of list. + out := append(nonOktaRoles, oktaRoles...) + return strings.Join(out, ", ") +} + // printLoginInformation displays the provided profile information to the user. func printLoginInformation(cf *CLIConf, profile *client.ProfileStatus, profiles []*client.ProfileStatus, accessListsToReview []*accesslist.AccessList) error { env := getTshEnv() diff --git a/tool/tsh/common/tsh_test.go b/tool/tsh/common/tsh_test.go index 10136b74742e7..351ee9dd1cc6a 100644 --- a/tool/tsh/common/tsh_test.go +++ b/tool/tsh/common/tsh_test.go @@ -5756,3 +5756,35 @@ func TestProxyTemplatesMakeClient(t *testing.T) { }) } } + +func TestRolesToString(t *testing.T) { + tests := []struct { + name string + roles []string + expected string + debug bool + }{ + { + name: "empty", + roles: []string{}, + expected: "", + }, + { + name: "exceed threshold okta roles should be squashed", + roles: append([]string{"app-figma-reviewer-okta-acl-role", "app-figma-access-okta-acl-role"}, []string{"r1", "r2", "r3", "r4", "r5", "r6", "r7", "r8", "r9", "r10"}...), + expected: "r1, r10, r2, r3, r4, r5, r6, r7, r8, r9, and 2 more Okta access list roles ...", + }, + { + name: "debug flag", + roles: append([]string{"app-figma-reviewer-okta-acl-role", "app-figma-access-okta-acl-role"}, []string{"r1", "r2", "r3", "r4", "r5", "r6", "r7", "r8", "r9", "r10"}...), + debug: true, + expected: "r1, r10, r2, r3, r4, r5, r6, r7, r8, r9, app-figma-access-okta-acl-role, app-figma-reviewer-okta-acl-role", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + require.Equal(t, tc.expected, rolesToString(tc.debug, tc.roles)) + }) + } +}