From ef2349b26b4b71e637018057480e309dfa315a4e Mon Sep 17 00:00:00 2001 From: Trent Clarke Date: Mon, 16 Dec 2024 13:06:46 +1100 Subject: [PATCH] [v17] Adds Roles for IC resource access requests Backports #49565 --------- Co-authored-by: Roman Tkachenko --- api/types/role.go | 13 ++++ constants.go | 5 ++ lib/auth/init.go | 1 + lib/auth/init_test.go | 1 + lib/services/presets.go | 132 ++++++++++++++++++++++++++++++++--- lib/services/presets_test.go | 96 ++++++++++++++++++++++--- 6 files changed, 229 insertions(+), 19 deletions(-) diff --git a/api/types/role.go b/api/types/role.go index 4b3e41baf6baa..f77a897209327 100644 --- a/api/types/role.go +++ b/api/types/role.go @@ -290,6 +290,9 @@ type Role interface { // GetIdentityCenterAccountAssignments fetches the allow or deny Account // Assignments for the role GetIdentityCenterAccountAssignments(RoleConditionType) []IdentityCenterAccountAssignment + // GetIdentityCenterAccountAssignments sets the allow or deny Account + // Assignments for the role + SetIdentityCenterAccountAssignments(RoleConditionType, []IdentityCenterAccountAssignment) } // NewRole constructs new standard V7 role. @@ -2068,6 +2071,16 @@ func (r *RoleV6) GetIdentityCenterAccountAssignments(rct RoleConditionType) []Id return r.Spec.Deny.AccountAssignments } +// SetIdentityCenterAccountAssignments sets the allow or deny Identity Center +// Account Assignments for the role +func (r *RoleV6) SetIdentityCenterAccountAssignments(rct RoleConditionType, assignments []IdentityCenterAccountAssignment) { + cond := &r.Spec.Deny + if rct == Allow { + cond = &r.Spec.Allow + } + cond.AccountAssignments = assignments +} + // LabelMatcherKinds is the complete list of resource kinds that support label // matchers. var LabelMatcherKinds = []string{ diff --git a/constants.go b/constants.go index 803a58d44fbbd..70bb2837520d8 100644 --- a/constants.go +++ b/constants.go @@ -698,6 +698,11 @@ const ( // access to Okta resources. This will be used by the Okta requester role to // search for Okta resources. SystemOktaAccessRoleName = "okta-access" + + // SystemIdentityCenterAccessRoleName specifies the name of a system role + // that grants a user access to AWS Identity Center resources via + // Access Requests. + SystemIdentityCenterAccessRoleName = "aws-ic-access" ) var PresetRoles = []string{PresetEditorRoleName, PresetAccessRoleName, PresetAuditorRoleName} diff --git a/lib/auth/init.go b/lib/auth/init.go index aaacaca8dfad4..5f1413e1ff268 100644 --- a/lib/auth/init.go +++ b/lib/auth/init.go @@ -1031,6 +1031,7 @@ func GetPresetRoles() []types.Role { services.NewSystemOktaAccessRole(), services.NewSystemOktaRequesterRole(), services.NewPresetTerraformProviderRole(), + services.NewSystemIdentityCenterAccessRole(), } // Certain `New$FooRole()` functions will return a nil role if the diff --git a/lib/auth/init_test.go b/lib/auth/init_test.go index 59fab636b43a0..fd5ce9925c1d4 100644 --- a/lib/auth/init_test.go +++ b/lib/auth/init_test.go @@ -1115,6 +1115,7 @@ func TestPresets(t *testing.T) { enterpriseSystemRoleNames := []string{ teleport.SystemAutomaticAccessApprovalRoleName, teleport.SystemOktaAccessRoleName, + teleport.SystemIdentityCenterAccessRoleName, } enterpriseUsers := []types.User{ diff --git a/lib/services/presets.go b/lib/services/presets.go index 887545d164cf6..2b271a754c6ae 100644 --- a/lib/services/presets.go +++ b/lib/services/presets.go @@ -28,8 +28,10 @@ import ( "github.com/gravitational/teleport/api/constants" apidefaults "github.com/gravitational/teleport/api/defaults" "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/api/types/common" apiutils "github.com/gravitational/teleport/api/utils" "github.com/gravitational/teleport/lib/modules" + "github.com/gravitational/teleport/lib/utils" ) // NewSystemAutomaticAccessApproverRole creates a new Role that is allowed to @@ -577,6 +579,32 @@ func NewSystemOktaRequesterRole() types.Role { return role } +// NewSystemIdentityCenterAccessRole creates a role that allows access to AWS +// IdentityCenter resources via Access Requests +func NewSystemIdentityCenterAccessRole() types.Role { + if modules.GetModules().BuildType() != modules.BuildEnterprise { + return nil + } + return &types.RoleV6{ + Kind: types.KindRole, + Version: types.V7, + Metadata: types.Metadata{ + Name: teleport.SystemIdentityCenterAccessRoleName, + Namespace: apidefaults.Namespace, + Description: "Access AWS IAM Identity Center resources", + Labels: map[string]string{ + types.TeleportInternalResourceType: types.SystemResource, + types.OriginLabel: common.OriginAWSIdentityCenter, + }, + }, + Spec: types.RoleSpecV6{ + Allow: types.RoleConditions{ + AccountAssignments: defaultAllowAccountAssignments(true)[teleport.SystemIdentityCenterAccessRoleName], + }, + }, + } +} + // NewPresetTerraformProviderRole returns a new pre-defined role for the Teleport Terraform provider. // This role can edit any Terraform-supported resource. func NewPresetTerraformProviderRole() types.Role { @@ -716,6 +744,7 @@ func defaultAllowAccessRequestConditions(enterprise bool) map[string]*types.Acce SearchAsRoles: []string{ teleport.PresetAccessRoleName, teleport.PresetGroupAccessRoleName, + teleport.SystemIdentityCenterAccessRoleName, }, }, teleport.SystemOktaRequesterRoleName: { @@ -739,10 +768,12 @@ func defaultAllowAccessReviewConditions(enterprise bool) map[string]*types.Acces PreviewAsRoles: []string{ teleport.PresetAccessRoleName, teleport.PresetGroupAccessRoleName, + teleport.SystemIdentityCenterAccessRoleName, }, Roles: []string{ teleport.PresetAccessRoleName, teleport.PresetGroupAccessRoleName, + teleport.SystemIdentityCenterAccessRoleName, }, }, } @@ -751,6 +782,21 @@ func defaultAllowAccessReviewConditions(enterprise bool) map[string]*types.Acces return map[string]*types.AccessReviewConditions{} } +func defaultAllowAccountAssignments(enterprise bool) map[string][]types.IdentityCenterAccountAssignment { + if enterprise { + return map[string][]types.IdentityCenterAccountAssignment{ + teleport.SystemIdentityCenterAccessRoleName: { + { + Account: types.Wildcard, + PermissionSet: types.Wildcard, + }, + }, + } + } + + return map[string][]types.IdentityCenterAccountAssignment{} +} + // AddRoleDefaults adds default role attributes to a preset role. // Only attributes whose resources are not already defined (either allowing or denying) are added. func AddRoleDefaults(role types.Role) (types.Role, error) { @@ -852,18 +898,18 @@ func AddRoleDefaults(role types.Role) (types.Role, error) { } } - if role.GetAccessRequestConditions(types.Allow).IsEmpty() { - arc := defaultAllowAccessRequestConditions(enterprise)[role.GetName()] - if arc != nil { - role.SetAccessRequestConditions(types.Allow, *arc) - changed = true - } + if roleUpdated := applyAccessRequestConditionDefaults(role, enterprise); roleUpdated { + changed = true } - if role.GetAccessReviewConditions(types.Allow).IsEmpty() { - arc := defaultAllowAccessReviewConditions(enterprise)[role.GetName()] - if arc != nil { - role.SetAccessReviewConditions(types.Allow, *arc) + if roleUpdated := applyAccessReviewConditionDefaults(role, enterprise); roleUpdated { + changed = true + } + + if len(role.GetIdentityCenterAccountAssignments(types.Allow)) == 0 { + assignments := defaultAllowAccountAssignments(enterprise)[role.GetName()] + if assignments != nil { + role.SetIdentityCenterAccountAssignments(types.Allow, assignments) changed = true } } @@ -875,6 +921,72 @@ func AddRoleDefaults(role types.Role) (types.Role, error) { return role, nil } +func mergeStrings(dst, src []string) (merged []string, changed bool) { + items := utils.NewSet[string](dst...) + items.Add(src...) + if len(items) == len(dst) { + return dst, false + } + dst = items.Elements() + slices.Sort(dst) + return dst, true +} + +func applyAccessRequestConditionDefaults(role types.Role, enterprise bool) bool { + defaults := defaultAllowAccessRequestConditions(enterprise)[role.GetName()] + if defaults == nil { + return false + } + + target := role.GetAccessRequestConditions(types.Allow) + changed := false + if target.IsEmpty() { + target = *defaults + changed = true + } else { + var rolesUpdated bool + + target.Roles, rolesUpdated = mergeStrings(target.Roles, defaults.Roles) + changed = changed || rolesUpdated + + target.SearchAsRoles, rolesUpdated = mergeStrings(target.SearchAsRoles, defaults.SearchAsRoles) + changed = changed || rolesUpdated + } + + if changed { + role.SetAccessRequestConditions(types.Allow, target) + } + + return changed +} + +func applyAccessReviewConditionDefaults(role types.Role, enterprise bool) bool { + defaults := defaultAllowAccessReviewConditions(enterprise)[role.GetName()] + if defaults == nil { + return false + } + + target := role.GetAccessReviewConditions(types.Allow) + changed := false + if target.IsEmpty() { + target = *defaults + changed = true + } else { + var rolesUpdated bool + + target.Roles, rolesUpdated = mergeStrings(target.Roles, defaults.Roles) + changed = changed || rolesUpdated + + target.PreviewAsRoles, rolesUpdated = mergeStrings(target.PreviewAsRoles, defaults.PreviewAsRoles) + changed = changed || rolesUpdated + } + + if changed { + role.SetAccessReviewConditions(types.Allow, target) + } + return changed +} + func labelMatchersUnset(role types.Role, kind string) (bool, error) { for _, cond := range []types.RoleConditionType{types.Allow, types.Deny} { labelMatchers, err := role.GetLabelMatchers(cond, kind) diff --git a/lib/services/presets_test.go b/lib/services/presets_test.go index af4dbf3b06791..3e4f12c5d4084 100644 --- a/lib/services/presets_test.go +++ b/lib/services/presets_test.go @@ -349,13 +349,41 @@ func TestAddRoleDefaults(t *testing.T) { Spec: types.RoleSpecV6{ Allow: types.RoleConditions{ ReviewRequests: &types.AccessReviewConditions{ - Roles: []string{"some-role"}, + Roles: []string{"some-role"}, + PreviewAsRoles: []string{"preview-role"}, + }, + }, + }, + }, + enterprise: true, + expectedErr: require.NoError, + reviewNotEmpty: true, + expected: &types.RoleV6{ + Metadata: types.Metadata{ + Name: teleport.PresetReviewerRoleName, + Labels: map[string]string{ + types.TeleportInternalResourceType: types.PresetResource, + }, + }, + Spec: types.RoleSpecV6{ + Allow: types.RoleConditions{ + ReviewRequests: &types.AccessReviewConditions{ + Roles: []string{ + teleport.PresetAccessRoleName, + teleport.SystemIdentityCenterAccessRoleName, + teleport.PresetGroupAccessRoleName, + "some-role", + }, + PreviewAsRoles: []string{ + teleport.PresetAccessRoleName, + teleport.SystemIdentityCenterAccessRoleName, + teleport.PresetGroupAccessRoleName, + "preview-role", + }, }, }, }, }, - enterprise: true, - expectedErr: noChange, }, { name: "requester (not enterprise)", @@ -411,6 +439,25 @@ func TestAddRoleDefaults(t *testing.T) { { name: "requester (enterprise, existing requests)", role: &types.RoleV6{ + Metadata: types.Metadata{ + Name: teleport.PresetRequesterRoleName, + Labels: map[string]string{ + types.TeleportInternalResourceType: types.PresetResource, + }, + }, + Spec: types.RoleSpecV6{ + Allow: types.RoleConditions{ + Request: &types.AccessRequestConditions{ + Roles: []string{"some-role"}, + SearchAsRoles: []string{"search-as-role"}, + }, + }, + }, + }, + enterprise: true, + expectedErr: require.NoError, + accessRequestsNotEmpty: true, + expected: &types.RoleV6{ Metadata: types.Metadata{ Name: teleport.PresetRequesterRoleName, Labels: map[string]string{ @@ -421,12 +468,16 @@ func TestAddRoleDefaults(t *testing.T) { Allow: types.RoleConditions{ Request: &types.AccessRequestConditions{ Roles: []string{"some-role"}, + SearchAsRoles: []string{ + teleport.PresetAccessRoleName, + teleport.SystemIdentityCenterAccessRoleName, + teleport.PresetGroupAccessRoleName, + "search-as-role", + }, }, }, }, }, - enterprise: true, - expectedErr: noChange, }, { name: "okta resources (not enterprise)", @@ -555,8 +606,28 @@ func TestAddRoleDefaults(t *testing.T) { }, }, }, - enterprise: true, - expectedErr: noChange, + enterprise: true, + expectedErr: require.NoError, + accessRequestsNotEmpty: true, + expected: &types.RoleV6{ + Metadata: types.Metadata{ + Name: teleport.SystemOktaRequesterRoleName, + Labels: map[string]string{ + types.TeleportInternalResourceType: types.SystemResource, + types.OriginLabel: types.OriginOkta, + }, + }, + Spec: types.RoleSpecV6{ + Allow: types.RoleConditions{ + Request: &types.AccessRequestConditions{ + Roles: []string{"some-role"}, + SearchAsRoles: []string{ + teleport.SystemOktaAccessRoleName, + }, + }, + }, + }, + }, }, } @@ -574,8 +645,15 @@ func TestAddRoleDefaults(t *testing.T) { require.Empty(t, cmp.Diff(role, test.expected)) if test.expected != nil { - require.Equal(t, test.reviewNotEmpty, !role.GetAccessReviewConditions(types.Allow).IsEmpty()) - require.Equal(t, test.accessRequestsNotEmpty, !role.GetAccessRequestConditions(types.Allow).IsEmpty()) + require.Equal(t, test.reviewNotEmpty, + !role.GetAccessReviewConditions(types.Allow).IsEmpty(), + "Expected populated Access Review Conditions (%t)", + test.reviewNotEmpty) + + require.Equal(t, test.accessRequestsNotEmpty, + !role.GetAccessRequestConditions(types.Allow).IsEmpty(), + "Expected populated Access Request Conditions (%t)", + test.accessRequestsNotEmpty) } }) }