Skip to content

Commit

Permalink
Exposes Identity Center accounts as Apps in Unified Resource Cache
Browse files Browse the repository at this point in the history
For the purposes of the UI, Identity Center accounts and account
assignments are treated like special Apps. This patch exposes
Account Assignments to the UI via the Unified Resource Cache.

Includes
- Generating an App resource from an Identity Center Account resource
- Automatic label generation for Accounts
- General plumbing from backend through to cache
  • Loading branch information
tcsc committed Nov 21, 2024
1 parent 9317c0e commit 6ba62c5
Show file tree
Hide file tree
Showing 13 changed files with 521 additions and 13 deletions.
17 changes: 17 additions & 0 deletions api/types/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ type Application interface {
GetRequiredAppNames() []string
// GetCORS returns the CORS configuration for the app.
GetCORS() *CORSPolicy
// GetIdentityCenter fetches identity center info for the app, if any.
GetIdentityCenter() *AppIdentityCenter
}

// NewAppV3 creates a new app resource.
Expand Down Expand Up @@ -405,6 +407,12 @@ func (a *AppV3) CheckAndSetDefaults() error {
return nil
}

// GetIdentityCenter returns the Identity Center information for the app, if any.
// May be nil.
func (a *AppV3) GetIdentityCenter() *AppIdentityCenter {
return a.Spec.IdentityCenter
}

// IsEqual determines if two application resources are equivalent to one another.
func (a *AppV3) IsEqual(i Application) bool {
if other, ok := i.(*AppV3); ok {
Expand Down Expand Up @@ -458,3 +466,12 @@ func (a Apps) Less(i, j int) bool { return a[i].GetName() < a[j].GetName() }

// Swap swaps two apps.
func (a Apps) Swap(i, j int) { a[i], a[j] = a[j], a[i] }

// GetPermissionSets fetches the list of permission sets from the Identity Center
// app information. Handles nil identity center values.
func (a *AppIdentityCenter) GetPermissionSets() []*IdentityCenterPermissionSet {
if a == nil {
return nil
}
return a.PermissionSets
}
8 changes: 8 additions & 0 deletions api/types/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,10 @@ const (
// KindApp is a web app resource.
KindApp = "app"

// AppSubKindIdentityCenterAccount indicates that an App actually represents
// an Identity Center account
AppSubKindIdentityCenterAccount = KindIdentityCenterAccount

// KindAppOrSAMLIdPServiceProvider represent an App Server resource or a SAML IdP Service Provider (SAML Application) resource.
// This is not a real resource stored in the backend, it is a pseudo resource used only to provide a common interface to
// the ListResources RPC in order to be able to list both AppServers and SAMLIdPServiceProviders in the same request.
Expand Down Expand Up @@ -832,6 +836,10 @@ const (
// ReqAnnotationTeamsLabel is the request annotation key at which teams are stored for access plugins.
ReqAnnotationTeamsLabel = "/teams"

// IdentityCenterAccountLabel annotates an Identity Center resource with the
// AWS account it applies to
IdentityCenterAccountLabel = TeleportNamespace + "/aws-ic-account"

// CloudAWS identifies that a resource was discovered in AWS.
CloudAWS = "AWS"
// CloudAzure identifies that a resource was discovered in Azure.
Expand Down
2 changes: 1 addition & 1 deletion api/types/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -509,7 +509,7 @@ func MatchKinds(resource ResourceWithLabels, kinds []string) bool {
}
resourceKind := resource.GetKind()
switch resourceKind {
case KindApp, KindSAMLIdPServiceProvider:
case KindApp, KindSAMLIdPServiceProvider, KindIdentityCenterAccount:
return slices.Contains(kinds, KindApp)
default:
return slices.Contains(kinds, resourceKind)
Expand Down
11 changes: 11 additions & 0 deletions api/types/role.go
Original file line number Diff line number Diff line change
Expand Up @@ -1913,6 +1913,13 @@ func (r *RoleV6) GetLabelMatchers(rct RoleConditionType, kind string) (LabelMatc
return LabelMatchers{cond.WindowsDesktopLabels, cond.WindowsDesktopLabelsExpression}, nil
case KindUserGroup:
return LabelMatchers{cond.GroupLabels, cond.GroupLabelsExpression}, nil
case KindIdentityCenterAccount:
var matchers LabelMatchers
accounts := utils.Transform(cond.AccountAssignments, IdentityCenterAccountAssignment.GetAccount)
if len(accounts) > 0 {
matchers.Labels = Labels{IdentityCenterAccountLabel: utils.Deduplicate(accounts)}
}
return matchers, nil
}
return LabelMatchers{}, trace.BadParameter("can't get label matchers for resource kind %q", kind)
}
Expand Down Expand Up @@ -2241,3 +2248,7 @@ func (h *CreateDatabaseUserMode) UnmarshalJSON(data []byte) error {
func (m CreateDatabaseUserMode) IsEnabled() bool {
return m != CreateDatabaseUserMode_DB_USER_MODE_UNSPECIFIED && m != CreateDatabaseUserMode_DB_USER_MODE_OFF
}

func (a IdentityCenterAccountAssignment) GetAccount() string {
return a.Account
}
58 changes: 58 additions & 0 deletions api/types/role_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -811,3 +811,61 @@ func TestRoleFilterMatch(t *testing.T) {
})
}
}

func TestIdentityCenterAccountLabels(t *testing.T) {
role, err := NewRole(t.Name(), RoleSpecV6{
Allow: RoleConditions{
AccountAssignments: []IdentityCenterAccountAssignment{
{
Account: "11111111",
PermissionSet: "some-permission-set",
},
{
Account: "11111111",
PermissionSet: "some-other-permission-set",
},
{
Account: "22222222",
PermissionSet: "*",
},
},
},
Deny: RoleConditions{
AccountAssignments: []IdentityCenterAccountAssignment{
{
Account: "33333333",
PermissionSet: "some-other-permission-set",
},
},
},
})
require.NoError(t, err)

testCases := []struct {
name string
condition RoleConditionType
expectedAccounts []string
}{
{
name: "allow",
condition: Allow,
expectedAccounts: []string{"11111111", "22222222"},
},
{
name: "deny",
condition: Deny,
expectedAccounts: []string{"33333333"},
},
}

for _, test := range testCases {
t.Run(test.name, func(y *testing.T) {
labelMatchers, err := role.GetLabelMatchers(test.condition, KindIdentityCenterAccount)
require.NoError(t, err)
require.Empty(t, labelMatchers.Expression)
require.Len(t, labelMatchers.Labels, 1)
require.Contains(t, labelMatchers.Labels, IdentityCenterAccountLabel)
require.ElementsMatch(t, test.expectedAccounts, labelMatchers.Labels[IdentityCenterAccountLabel])
})
}
}
14 changes: 14 additions & 0 deletions api/utils/slices.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,3 +129,17 @@ func CountBy[S ~[]E, E any](elements S, mapper func(E) string) map[string]int {
}
return out
}

// Transform transforms a slice of values into a new slice, with values being converted by the
// supplied mapping function
func Transform[S ~[]T, T any, U any](src S, mapper func(T) U) []U {
// preserve nil in case it matters
if src == nil {
return nil
}
dst := make([]U, len(src))
for i, t := range src {
dst[i] = mapper(t)
}
return dst
}
22 changes: 22 additions & 0 deletions api/utils/slices_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -223,3 +223,25 @@ func TestCountBy(t *testing.T) {
})
}
}

func TestTransform(t *testing.T) {
t.Run("nil", func(t *testing.T) {
dst := Transform(([]int)(nil), strconv.Itoa)
require.Nil(t, dst)
})

t.Run("empty", func(t *testing.T) {
dst := Transform([]int{}, strconv.Itoa)
require.NotNil(t, dst)
require.Empty(t, dst)
})

t.Run("populated", func(t *testing.T) {
src := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 0}
dst := Transform(src, strconv.Itoa)
require.Equal(t,
[]string{"1", "2", "3", "4", "5", "6", "7", "8", "9", "0"},
dst,
)
})
}
17 changes: 13 additions & 4 deletions lib/auth/auth_with_roles.go
Original file line number Diff line number Diff line change
Expand Up @@ -1281,7 +1281,7 @@ func (c *resourceAccess) checkAccess(resource types.ResourceWithLabels, filter s
return false, nil
}

// check access normally if base checker doesnt exist
// check access normally if base checker doesn't exist
if c.baseAuthChecker == nil {
if err := c.accessChecker.CanAccess(resource); err != nil {
if trace.IsAccessDenied(err) {
Expand Down Expand Up @@ -1352,7 +1352,12 @@ func (a *ServerWithRoles) ListUnifiedResources(ctx context.Context, req *proto.L
actionVerbs = []string{types.VerbList}
}

resourceAccess.kindAccessMap[kind] = a.action(apidefaults.Namespace, kind, actionVerbs...)
checkKind := kind
if kind == types.KindIdentityCenterAccount {
checkKind = types.KindIdentityCenter
}

resourceAccess.kindAccessMap[kind] = a.action(apidefaults.Namespace, checkKind, actionVerbs...)
}

// Before doing any listing, verify that the user is allowed to list
Expand Down Expand Up @@ -1822,9 +1827,12 @@ func (r resourceChecker) CanAccess(resource types.Resource) error {
}
case types.SAMLIdPServiceProvider:
return r.CheckAccess(rr, state)

case services.UnifiedResource153Adapter[services.IdentityCenterAccount]:
return r.CheckAccess(rr, state)
}

return trace.BadParameter("could not check access to resource type %T", r)
return trace.BadParameter("could not check access to resource type %T", resource)
}

// newResourceAccessChecker creates a resourceAccessChecker for the provided resource type
Expand All @@ -1839,7 +1847,8 @@ func (a *ServerWithRoles) newResourceAccessChecker(resource string) (resourceAcc
types.KindKubeServer,
types.KindUserGroup,
types.KindUnifiedResource,
types.KindSAMLIdPServiceProvider:
types.KindSAMLIdPServiceProvider,
types.KindIdentityCenterAccount:
return &resourceChecker{AccessChecker: a.context.Checker}, nil
default:
return nil, trace.BadParameter("could not check access to resource type %s", resource)
Expand Down
2 changes: 2 additions & 0 deletions lib/services/matchers.go
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,8 @@ func MatchResourceByFilters(resource types.ResourceWithLabels, filter MatchResou
default:
return false, trace.BadParameter("expected types.SAMLIdPServiceProvider or types.AppServer, got %T", resource)
}
case types.KindIdentityCenterAccount:
specResource = resource
default:
// We check if the resource kind is a Kubernetes resource kind to reduce the amount of
// of cases we need to handle. If the resource type didn't match any arm before
Expand Down
Loading

0 comments on commit 6ba62c5

Please sign in to comment.