Skip to content

Commit

Permalink
Adds FriendlyName support for IC resources
Browse files Browse the repository at this point in the history
The IC Account and AccountAssignment resources do not have very user-friendly
ids, making it hard to know what access is actually being granted when reviewing
an access request.

This patch adds support for
 - querying Identity Center accounts via `auth.ListResources`
 - generating a friendly resource name when reviewing an access request
   with IC resources.
  • Loading branch information
tcsc committed Dec 18, 2024
1 parent 1957489 commit fb656bd
Show file tree
Hide file tree
Showing 10 changed files with 231 additions and 1 deletion.
22 changes: 21 additions & 1 deletion api/accessrequest/access_request.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ func GetResourceDetails(ctx context.Context, clusterName string, lister client.L
// We're interested in hostname or friendly name details. These apply to
// nodes, app servers, user groups and Identity Center resources.
switch resourceID.Kind {
case types.KindNode, types.KindApp, types.KindUserGroup, types.KindIdentityCenterAccount:
case types.KindNode, types.KindApp, types.KindUserGroup, types.KindIdentityCenterAccount, types.KindIdentityCenterAccountAssignment:
resourceIDs = append(resourceIDs, resourceID)
}
}
Expand Down Expand Up @@ -89,6 +89,26 @@ func GetResourceDetails(ctx context.Context, clusterName string, lister client.L
Kind: resource.GetKind(),
Name: resource.GetName(),
}

// We pretend that AWS accounts are Apps for display, so we have to rewrite
// the `id` of the App resource returned by GetResourcesByResourceIDs()
// to that of the corresponding `IdentityCenterAccount` that the caller
// was asking for.
if resource.GetKind() == types.KindApp && resource.GetSubKind() == types.KindIdentityCenterAccount {
appResource, ok := resource.(*types.AppV3)
if !ok {
return nil, trace.BadParameter("invalid type for kind App: %T", resource)
}

icInfo := appResource.GetIdentityCenter()
if icInfo == nil {
return nil, trace.BadParameter("malformed Identity Center App: identity center info is missing")
}

id.Kind = types.KindIdentityCenterAccount
id.Name = icInfo.AccountID
}

result[types.ResourceIDToString(id)] = types.ResourceDetails{
FriendlyName: friendlyName,
}
Expand Down
14 changes: 14 additions & 0 deletions api/types/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
"github.com/gravitational/trace"

"github.com/gravitational/teleport/api/defaults"
identitycenterv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/identitycenter/v1"
"github.com/gravitational/teleport/api/types/common"
"github.com/gravitational/teleport/api/types/compare"
"github.com/gravitational/teleport/api/utils"
Expand Down Expand Up @@ -705,6 +706,19 @@ func FriendlyName(resource ResourceWithLabels) string {
}

switch rr := resource.(type) {
case Resource153Unwrapper:
// RFD-153 style resources are generally data-only and do not have any
// methods beyond the minimal [Resource153] interface. Because we can't
// rely on them being able to implement an interface in order to generate
// a friendly name, any 153-style resources that *want* a friendly name
// will have to manually generate it.
switch urr := rr.Unwrap().(type) {
case *identitycenterv1.Account:
return urr.GetSpec().GetName()

case *identitycenterv1.AccountAssignment:
return urr.GetSpec().GetDisplay()
}
case interface{ GetHostname() string }:
return rr.GetHostname()
case interface{ GetDisplayName() string }:
Expand Down
46 changes: 46 additions & 0 deletions lib/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ import (
usagereporter "github.com/gravitational/teleport/lib/usagereporter/teleport"
"github.com/gravitational/teleport/lib/utils"
"github.com/gravitational/teleport/lib/utils/interval"
"github.com/gravitational/teleport/lib/utils/pagination"
vc "github.com/gravitational/teleport/lib/versioncontrol"
"github.com/gravitational/teleport/lib/versioncontrol/github"
uw "github.com/gravitational/teleport/lib/versioncontrol/upgradewindow"
Expand Down Expand Up @@ -6372,9 +6373,54 @@ func (a *Server) ListResources(ctx context.Context, req proto.ListResourcesReque
NextKey: wResp.NextKey,
}, nil
}
if req.ResourceType == types.KindIdentityCenterAccount {
return a.listIdentityCenterAccounts(ctx, req)
}
return a.Cache.ListResources(ctx, req)
}

func (a *Server) listIdentityCenterAccounts(ctx context.Context, req proto.ListResourcesRequest) (*types.ListResourcesResponse, error) {
filter := services.MatchResourceFilter{
ResourceKind: types.KindIdentityCenterAccount,
Labels: req.Labels,
SearchKeywords: req.SearchKeywords,
}

if req.PredicateExpression != "" {
expression, err := services.NewResourceExpression(req.PredicateExpression)
if err != nil {
return nil, trace.Wrap(err)
}
filter.PredicateExpression = expression
}

startKey := pagination.NewRequestToken(req.StartKey)

accounts, nextPage, err := a.Cache.ListIdentityCenterAccountsWithFilter(ctx, int(req.Limit), &startKey,
func(acct services.IdentityCenterAccount) bool {
match, err := services.MatchResourceByFilters(
types.Resource153ToResourceWithLabels(acct), filter, nil)
if err != nil {
a.logger.ErrorContext(ctx, "failed running matcher", "error", err)
return false
}
return match
})
if err != nil {
return nil, trace.Wrap(err)
}

resources := make([]types.ResourceWithLabels, len(accounts))
for i, acct := range accounts {
resources[i] = types.Resource153ToResourceWithLabels(acct)
}

return &types.ListResourcesResponse{
Resources: resources,
NextKey: string(nextPage),
}, nil
}

// CreateKubernetesCluster creates a new kubernetes cluster resource.
func (a *Server) CreateKubernetesCluster(ctx context.Context, kubeCluster types.KubeCluster) error {
if err := enforceLicense(types.KindKubernetesCluster); err != nil {
Expand Down
4 changes: 4 additions & 0 deletions lib/auth/authclient/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -1244,6 +1244,10 @@ type Cache interface {
// GetProvisioningState gets a specific provisioning state
GetProvisioningState(context.Context, services.DownstreamID, services.ProvisioningStateID) (*provisioningv1.PrincipalState, error)

// ListIdentityCenterAccountsWithFilter returns a paginated list of all
// Identity Center Accounts in the cache that match the supplied predicate
ListIdentityCenterAccountsWithFilter(context.Context, int, *pagination.PageRequestToken, func(services.IdentityCenterAccount) bool) ([]services.IdentityCenterAccount, pagination.NextPageToken, error)

// GetAccountAssignment fetches specific IdentityCenter Account Assignment
GetAccountAssignment(context.Context, services.IdentityCenterAccountAssignmentID) (services.IdentityCenterAccountAssignment, error)

Expand Down
20 changes: 20 additions & 0 deletions lib/cache/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -3620,6 +3620,26 @@ func (c *Cache) GetProvisioningState(ctx context.Context, downstream services.Do
return rg.reader.GetProvisioningState(ctx, downstream, id)
}

// ListIdentityCenterAccountsWithFilter returns a paginated list of all
// Identity Center Accounts in the cache that match the supplied predicate
func (c *Cache) ListIdentityCenterAccountsWithFilter(
ctx context.Context,
pageSize int,
pageToken *pagination.PageRequestToken,
filter func(services.IdentityCenterAccount) bool,
) ([]services.IdentityCenterAccount, pagination.NextPageToken, error) {
ctx, span := c.Tracer.Start(ctx, "cache/ListIdentityCenterAccountsWithFilter")
defer span.End()

rg, err := readCollectionCache(c, c.collections.identityCenterAccounts)
if err != nil {
return nil, "", trace.Wrap(err)
}
defer rg.Release()

return rg.reader.ListIdentityCenterAccountsWithFilter(ctx, pageSize, pageToken, filter)
}

func (c *Cache) GetAccountAssignment(ctx context.Context, id services.IdentityCenterAccountAssignmentID) (services.IdentityCenterAccountAssignment, error) {
ctx, span := c.Tracer.Start(ctx, "cache/GetAccountAssignment")
defer span.End()
Expand Down
1 change: 1 addition & 0 deletions lib/cache/identitycenter.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import (
type identityCenterAccountGetter interface {
GetIdentityCenterAccount(context.Context, services.IdentityCenterAccountID) (services.IdentityCenterAccount, error)
ListIdentityCenterAccounts(context.Context, int, *pagination.PageRequestToken) ([]services.IdentityCenterAccount, pagination.NextPageToken, error)
ListIdentityCenterAccountsWithFilter(context.Context, int, *pagination.PageRequestToken, func(services.IdentityCenterAccount) bool) ([]services.IdentityCenterAccount, pagination.NextPageToken, error)
}

type identityCenterAccountExecutor struct{}
Expand Down
8 changes: 8 additions & 0 deletions lib/services/identitycenter.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,14 @@ type IdentityCenterAccountGetter interface {
type IdentityCenterAccounts interface {
IdentityCenterAccountGetter

// ListIdentityCenterAccountsWithFilter lists Identity Center Accounts in the backend store
ListIdentityCenterAccountsWithFilter(
context.Context,
int,
*pagination.PageRequestToken,
func(IdentityCenterAccount) bool,
) ([]IdentityCenterAccount, pagination.NextPageToken, error)

// CreateIdentityCenterAccount creates a new Identity Center Account record
CreateIdentityCenterAccount(context.Context, IdentityCenterAccount) (IdentityCenterAccount, error)

Expand Down
34 changes: 34 additions & 0 deletions lib/services/local/identitycenter.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,40 @@ func (svc *IdentityCenterService) ListIdentityCenterAccounts(ctx context.Context
return result, pagination.NextPageToken(nextPage), nil
}

// ListIdentityCenterAccountsWithFilter returns a paginated list of all
// Identity Center Accounts in the service backend that match the supplied
// predicate
func (svc *IdentityCenterService) ListIdentityCenterAccountsWithFilter(
ctx context.Context,
pageSize int,
page *pagination.PageRequestToken,
matcher func(services.IdentityCenterAccount) bool,
) ([]services.IdentityCenterAccount, pagination.NextPageToken, error) {
if pageSize == 0 {
pageSize = identityCenterPageSize
}

pageToken, err := page.Consume()
if err != nil {
return nil, "", trace.Wrap(err, "listing identity center assignment records")
}

wrappedMatcher := func(a *identitycenterv1.Account) bool {
return matcher(services.IdentityCenterAccount{Account: a})
}

accounts, nextPage, err := svc.accounts.ListResourcesWithFilter(ctx, pageSize, pageToken, wrappedMatcher)
if err != nil {
return nil, "", trace.Wrap(err)
}

result := make([]services.IdentityCenterAccount, len(accounts))
for i, acct := range accounts {
result[i] = services.IdentityCenterAccount{Account: acct}
}
return result, pagination.NextPageToken(nextPage), nil
}

// CreateIdentityCenterAccount creates a new Identity Center Account record
func (svc *IdentityCenterService) CreateIdentityCenterAccount(ctx context.Context, acct services.IdentityCenterAccount) (services.IdentityCenterAccount, error) {
created, err := svc.accounts.CreateResource(ctx, acct.Account)
Expand Down
78 changes: 78 additions & 0 deletions lib/services/local/identitycenter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ package local
import (
"context"
"fmt"
"maps"
"slices"
"testing"

"github.com/gravitational/trace"
Expand All @@ -32,6 +34,7 @@ import (
"github.com/gravitational/teleport/lib/backend"
"github.com/gravitational/teleport/lib/backend/lite"
"github.com/gravitational/teleport/lib/services"
"github.com/gravitational/teleport/lib/utils/pagination"
)

func newTestBackend(t *testing.T, ctx context.Context, clock clockwork.Clock) backend.Backend {
Expand Down Expand Up @@ -270,6 +273,81 @@ func TestIdentityCenterResourceCRUD(t *testing.T) {
}
}

func TestIdentityCenterAccountListing(t *testing.T) {
// GIVEN a test cluster
ctx := newTestContext(t)
clock := clockwork.NewFakeClock()
backend := newTestBackend(t, ctx, clock)

// GIVEN an Identity Center Service
uut, err := NewIdentityCenterService(IdentityCenterServiceConfig{Backend: backend})
require.NoError(t, err)

// GIVEN a collection of Identity Center accounts
accounts := make(map[services.IdentityCenterAccountID]services.IdentityCenterAccount)
for _, id := range []string{"alpha", "bravo", "charlie", "delta", "echo", "foxtrot", "golf"} {
accounts[services.IdentityCenterAccountID(id)] =
makeTestIdentityCenterAccount(t, ctx, uut, id)
}

testCases := []struct {
name string
pageSize int
filter func(services.IdentityCenterAccount) bool
expected []services.IdentityCenterAccountID
}{
{
name: "full",
pageSize: 0,
filter: func(services.IdentityCenterAccount) bool { return true },
expected: slices.Collect(maps.Keys(accounts)),
},
{
name: "paged",
pageSize: 2,
filter: func(services.IdentityCenterAccount) bool { return true },
expected: slices.Collect(maps.Keys(accounts)),
},
{
name: "filtered",
pageSize: 3,
expected: []services.IdentityCenterAccountID{
"alpha", "charlie", "echo", "golf",
},
filter: func(acct services.IdentityCenterAccount) bool {
name := acct.Metadata.Name
return name == "alpha" || name == "charlie" ||
name == "echo" || name == "golf"
},
},
}

for _, test := range testCases {
t.Run(test.name, func(t *testing.T) {
var pageToken pagination.PageRequestToken
output := make(map[services.IdentityCenterAccountID]services.IdentityCenterAccount)
for {
page, nextPage, err := uut.ListIdentityCenterAccountsWithFilter(ctx, test.pageSize, &pageToken, test.filter)
require.NoError(t, err)

if test.pageSize != 0 {
require.LessOrEqual(t, len(page), test.pageSize)
}

for _, account := range page {
output[services.IdentityCenterAccountID(account.Metadata.Name)] = account
}

if nextPage == pagination.EndOfList {
break
}
pageToken.Update(nextPage)
}
require.Len(t, output, len(test.expected))
})
}
}

func makeTestIdentityCenterAccount(t *testing.T, ctx context.Context, svc services.IdentityCenter, id string) services.IdentityCenterAccount {
t.Helper()
created, err := svc.CreateIdentityCenterAccount(ctx, services.IdentityCenterAccount{
Expand Down
5 changes: 5 additions & 0 deletions lib/utils/pagination/pagination.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,11 @@ type PageRequestToken struct {
stale bool
}

// NewRequestToken wraps the supplied string in a PageRequestToken
func NewRequestToken(key string) PageRequestToken {
return PageRequestToken{token: key}
}

// Consume moves the token value out of the PageRequestToken and marks the token
// as stale. If the token is already stale, this method will fail with
// BadParameter.
Expand Down

0 comments on commit fb656bd

Please sign in to comment.