Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds Roles for IC resource access requests #49565

Merged
merged 9 commits into from
Dec 16, 2024
13 changes: 13 additions & 0 deletions api/types/role.go
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,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.
Expand Down Expand Up @@ -2127,6 +2130,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{
Expand Down
5 changes: 5 additions & 0 deletions constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -704,6 +704,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}
Expand Down
1 change: 1 addition & 0 deletions lib/auth/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -1037,6 +1037,7 @@ func GetPresetRoles() []types.Role {
services.NewSystemOktaAccessRole(),
services.NewSystemOktaRequesterRole(),
services.NewPresetTerraformProviderRole(),
services.NewSystemIdentityCenterAccessRole(),
}

// Certain `New$FooRole()` functions will return a nil role if the
Expand Down
1 change: 1 addition & 0 deletions lib/auth/init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1115,6 +1115,7 @@ func TestPresets(t *testing.T) {
enterpriseSystemRoleNames := []string{
teleport.SystemAutomaticAccessApprovalRoleName,
teleport.SystemOktaAccessRoleName,
teleport.SystemIdentityCenterAccessRoleName,
}

enterpriseUsers := []types.User{
Expand Down
132 changes: 122 additions & 10 deletions lib/services/presets.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -579,6 +581,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 {
Expand Down Expand Up @@ -718,6 +746,7 @@ func defaultAllowAccessRequestConditions(enterprise bool) map[string]*types.Acce
SearchAsRoles: []string{
teleport.PresetAccessRoleName,
teleport.PresetGroupAccessRoleName,
teleport.SystemIdentityCenterAccessRoleName,
},
},
teleport.SystemOktaRequesterRoleName: {
Expand All @@ -741,10 +770,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,
},
},
}
Expand All @@ -753,6 +784,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) {
Expand Down Expand Up @@ -854,18 +900,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
}
}
Expand All @@ -877,6 +923,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
Comment on lines +951 to +955
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm isn't this the equivalent (and cleaner) or am I missing something?

Same below.

Suggested change
target.Roles, rolesUpdated = mergeStrings(target.Roles, defaults.Roles)
changed = changed || rolesUpdated
target.SearchAsRoles, rolesUpdated = mergeStrings(target.SearchAsRoles, defaults.SearchAsRoles)
changed = changed || rolesUpdated
target.Roles, changed = mergeStrings(target.Roles, defaults.Roles)
target.SearchAsRoles, changed = mergeStrings(target.SearchAsRoles, defaults.SearchAsRoles)

Copy link
Contributor Author

@tcsc tcsc Dec 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the first mergeStrings call changes the roles but the second one doesn't, then the changed flag will be incorrectly reset and the change won't be propagated.

}

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
Comment on lines +979 to +983
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
target.Roles, rolesUpdated = mergeStrings(target.Roles, defaults.Roles)
changed = changed || rolesUpdated
target.PreviewAsRoles, rolesUpdated = mergeStrings(target.PreviewAsRoles, defaults.PreviewAsRoles)
changed = changed || rolesUpdated
target.Roles, changed = mergeStrings(target.Roles, defaults.Roles)
target.PreviewAsRoles, changed = mergeStrings(target.PreviewAsRoles, defaults.PreviewAsRoles)

}

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)
Expand Down
96 changes: 87 additions & 9 deletions lib/services/presets_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)",
Expand Down Expand Up @@ -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{
Expand All @@ -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)",
Expand Down Expand Up @@ -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,
},
},
},
},
},
},
}

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