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
    - General plumbing from backend through to cache and UI
  • Loading branch information
tcsc committed Dec 5, 2024
1 parent 3fcbf16 commit aa1132c
Show file tree
Hide file tree
Showing 16 changed files with 523 additions and 25 deletions.
2 changes: 1 addition & 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, and user groups.
switch resourceID.Kind {
case types.KindNode, types.KindApp, types.KindUserGroup:
case types.KindNode, types.KindApp, types.KindUserGroup, types.KindIdentityCenterAccount:
resourceIDs = append(resourceIDs, resourceID)
}
}
Expand Down
2 changes: 2 additions & 0 deletions api/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -3808,6 +3808,8 @@ func (c *Client) ListResources(ctx context.Context, req proto.ListResourcesReque
resources[i] = respResource.GetAppServerOrSAMLIdPServiceProvider()
case types.KindSAMLIdPServiceProvider:
resources[i] = respResource.GetSAMLIdPServiceProvider()
case types.KindIdentityCenterAccount:
resources[i] = respResource.GetAppServer()
default:
return nil, trace.NotImplemented("resource type %s does not support pagination", req.ResourceType)
}
Expand Down
28 changes: 28 additions & 0 deletions api/types/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ type Application interface {
GetTCPPorts() []*PortRange
// SetTCPPorts sets port ranges to which connections can be forwarded to.
SetTCPPorts([]*PortRange)
// GetIdentityCenter fetches identity center info for the app, if any.
GetIdentityCenter() *AppIdentityCenter
}

// NewAppV3 creates a new app resource.
Expand Down Expand Up @@ -456,6 +458,23 @@ func (a *AppV3) checkTCPPorts() 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
}

// GetDisplayName fetches a human-readable display name for the App.
func (a *AppV3) GetDisplayName() string {
// Only Identity Center apps have a display name at this point. Returning
// the empty string signals to the caller they should fall back to whatever
// they have been using in the past.
if a.Spec.IdentityCenter == nil {
return ""
}
return a.GetName()
}

// 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 @@ -509,3 +528,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
}
9 changes: 6 additions & 3 deletions 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 Expand Up @@ -686,8 +686,11 @@ func FriendlyName(resource ResourceWithLabels) string {
return resource.GetMetadata().Description
}

if hn, ok := resource.(interface{ GetHostname() string }); ok {
return hn.GetHostname()
switch rr := resource.(type) {
case interface{ GetHostname() string }:
return rr.GetHostname()
case interface{ GetDisplayName() string }:
return rr.GetDisplayName()
}

return ""
Expand Down
114 changes: 113 additions & 1 deletion api/types/resource_153.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"google.golang.org/protobuf/types/known/timestamppb"

headerv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/header/v1"
"github.com/gravitational/teleport/api/utils"
)

// ResourceMetadata is the smallest interface that defines a Teleport resource.
Expand Down Expand Up @@ -116,7 +117,8 @@ func (r *legacyToResource153Adapter) GetVersion() string {
}

// Resource153ToLegacy transforms an RFD 153 style resource into a legacy
// [Resource] type.
// [Resource] type. Implements [ResourceWithLabels] and CloneResource (where the)
// wrapped resource supports cloning).
//
// Note that CheckAndSetDefaults is a noop for the returned resource and
// SetSubKind is not implemented and panics on use.
Expand All @@ -130,6 +132,8 @@ type Resource153Unwrapper interface {
Unwrap() Resource153
}

// resource153ToLegacyAdapter wraps a new-style resource in a type implementing
// the legacy resource interfaces
type resource153ToLegacyAdapter struct {
inner Resource153
}
Expand Down Expand Up @@ -212,3 +216,111 @@ func (r *resource153ToLegacyAdapter) SetRevision(rev string) {
func (r *resource153ToLegacyAdapter) SetSubKind(subKind string) {
panic("interface Resource153 does not implement SetSubKind")
}

// ClonableResource153 adds a restriction on [Resource153] such that implementors
// must have a CloneResource() method.
type ClonableResource153 interface {
Resource153
CloneResource() ClonableResource153
}

// UnifiedResource represents the combined set of interfaces that a resource
// must implement to be used with the Teleport Unified Resource Cache
type UnifiedResource interface {
ResourceWithLabels
CloneResource() ResourceWithLabels
}

// Resource153ToUnifiedResource wraps an RFD153-style resource in a type that
// implements the legacy [ResourceWithLabels] interface and is suitable for use
// with the Teleport Unified Resources Cache.
//
// The same caveats that apply to [Resource153ToLegacy] apply.
func Resource153ToUnifiedResource(r ClonableResource153) UnifiedResource {
return &resource153ToUnifiedResourceAdapter{
resource153ToLegacyAdapter: resource153ToLegacyAdapter{
inner: r,
},
}
}

// resource153ToUnifiedResourceAdapter wraps a [resource153ToLegacyAdapter] to
// provide an implementation of [UnifiedResource]
type resource153ToUnifiedResourceAdapter struct {
resource153ToLegacyAdapter
}

// Origin implements ResourceWithLabels for the adapter.
func (r *resource153ToUnifiedResourceAdapter) Origin() string {
m := r.inner.GetMetadata()
if m == nil {
return ""
}
return m.Labels[OriginLabel]
}

// SetOrigin implements ResourceWithLabels for the adapter.
func (r *resource153ToUnifiedResourceAdapter) SetOrigin(origin string) {
m := r.inner.GetMetadata()
if m == nil {
return
}
m.Labels[OriginLabel] = origin
}

// GetLabel implements ResourceWithLabels for the adapter.
func (r *resource153ToUnifiedResourceAdapter) GetLabel(key string) (value string, ok bool) {
m := r.inner.GetMetadata()
if m == nil {
return "", false
}
value, ok = m.Labels[key]
return
}

// GetAllLabels implements ResourceWithLabels for the adapter.
func (r *resource153ToUnifiedResourceAdapter) GetAllLabels() map[string]string {
m := r.inner.GetMetadata()
if m == nil {
return nil
}
return m.Labels
}

// GetStaticLabels implements ResourceWithLabels for the adapter.
func (r *resource153ToUnifiedResourceAdapter) GetStaticLabels() map[string]string {
return r.GetAllLabels()
}

// SetStaticLabels implements ResourceWithLabels for the adapter.
func (r *resource153ToUnifiedResourceAdapter) SetStaticLabels(labels map[string]string) {
m := r.inner.GetMetadata()
if m == nil {
return
}
m.Labels = labels
}

// MatchSearch implements ResourceWithLabels for the adapter. If the underlying
// type exposes a MatchSearch method, this method will defer to that, otherwise
// it will match against the resource label values and name.
func (r *resource153ToUnifiedResourceAdapter) MatchSearch(searchValues []string) bool {
if matcher, ok := r.inner.(interface{ MatchSearch([]string) bool }); ok {
return matcher.MatchSearch(searchValues)
}
fieldVals := append(utils.MapToStrings(r.GetAllLabels()), r.GetName())
return MatchSearch(fieldVals, searchValues, nil)
}

// CloneResource clones the underlying resource and wraps it in
func (r *resource153ToUnifiedResourceAdapter) CloneResource() ResourceWithLabels {
// We assume that this type assertion will work because we force `inner`
// to implement ClonableResource153 in [Resource153ToUnifiedResource], which
// is the only externally-visible constructor function
clone := r.inner.(ClonableResource153).CloneResource()
return &resource153ToUnifiedResourceAdapter{
resource153ToLegacyAdapter: resource153ToLegacyAdapter{
inner: clone,
},
}
}
4 changes: 2 additions & 2 deletions api/types/resource_153_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,10 @@ func TestResource153ToLegacy(t *testing.T) {
}

legacyResource := types.Resource153ToLegacy(bot)

// Unwrap gives the underlying resource back.
t.Run("unwrap", func(t *testing.T) {
unwrapped := legacyResource.(interface{ Unwrap() types.Resource153 }).Unwrap()
unwrapper := legacyResource.(types.Resource153Unwrapper)
unwrapped := unwrapper.Unwrap()
if diff := cmp.Diff(bot, unwrapped, protocmp.Transform()); diff != "" {
t.Errorf("Unwrap mismatch (-want +got)\n%s", diff)
}
Expand Down
18 changes: 18 additions & 0 deletions api/types/role.go
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,10 @@ type Role interface {
GetGitHubPermissions(RoleConditionType) []GitHubPermission
// SetGitHubPermissions sets the allow or deny GitHub-related permissions.
SetGitHubPermissions(RoleConditionType, []GitHubPermission)

// GetIdentityCenterAccountAssignments fetches the allow or deny Account
// Assignments for the role
GetIdentityCenterAccountAssignments(RoleConditionType) []IdentityCenterAccountAssignment
}

// NewRole constructs new standard V7 role.
Expand Down Expand Up @@ -2061,6 +2065,15 @@ func (r *RoleV6) makeGitServerLabelMatchers(cond *RoleConditions) LabelMatchers
}
}

// GetIdentityCenterAccountAssignments fetches the allow or deny Identity Center
// Account Assignments for the role
func (r *RoleV6) GetIdentityCenterAccountAssignments(rct RoleConditionType) []IdentityCenterAccountAssignment {
if rct == Allow {
return r.Spec.Allow.AccountAssignments
}
return r.Spec.Deny.AccountAssignments
}

// LabelMatcherKinds is the complete list of resource kinds that support label
// matchers.
var LabelMatcherKinds = []string{
Expand Down Expand Up @@ -2286,3 +2299,8 @@ 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
}

// GetAccount fetches the Account ID from a Role Condition Account Assignment
func (a IdentityCenterAccountAssignment) GetAccount() string {
return a.Account
}
3 changes: 2 additions & 1 deletion lib/auth/auth_with_roles.go
Original file line number Diff line number Diff line change
Expand Up @@ -1666,7 +1666,8 @@ func (a *ServerWithRoles) ListResources(ctx context.Context, req proto.ListResou
types.KindWindowsDesktop,
types.KindWindowsDesktopService,
types.KindUserGroup,
types.KindSAMLIdPServiceProvider:
types.KindSAMLIdPServiceProvider,
types.KindIdentityCenterAccount:

default:
return nil, trace.NotImplemented("resource type %s does not support pagination", req.ResourceType)
Expand Down
57 changes: 56 additions & 1 deletion lib/auth/auth_with_roles_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,15 @@ import (
"github.com/gravitational/teleport/api/client/proto"
"github.com/gravitational/teleport/api/constants"
apidefaults "github.com/gravitational/teleport/api/defaults"
headerv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/header/v1"
identitycenterv1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/identitycenter/v1"
mfav1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/mfa/v1"
trustpb "github.com/gravitational/teleport/api/gen/proto/go/teleport/trust/v1"
userpreferencesv1 "github.com/gravitational/teleport/api/gen/proto/go/userpreferences/v1"
"github.com/gravitational/teleport/api/metadata"
"github.com/gravitational/teleport/api/mfa"
"github.com/gravitational/teleport/api/types"
apicommon "github.com/gravitational/teleport/api/types/common"
apievents "github.com/gravitational/teleport/api/types/events"
"github.com/gravitational/teleport/api/types/installers"
wanpb "github.com/gravitational/teleport/api/types/webauthn"
Expand Down Expand Up @@ -5698,7 +5701,8 @@ func TestListUnifiedResources_MixedAccess(t *testing.T) {
Limit: 20,
SortBy: types.SortBy{IsDesc: true, Field: types.ResourceMetadataName},
})
require.True(t, trace.IsAccessDenied(err))

require.True(t, trace.IsAccessDenied(err), "Expected Access Denied, got %v", err)
require.Nil(t, resp)

// Validate that an error is returned when a subset of kinds are requested.
Expand Down Expand Up @@ -5773,6 +5777,57 @@ func TestListUnifiedResources_WithPredicate(t *testing.T) {
require.Error(t, err)
}

func TestUnifiedResources_IdentityCenter(t *testing.T) {
ctx := context.Background()
srv := newTestTLSServer(t, withCacheEnabled(true))

require.Eventually(t, func() bool {
return srv.Auth().UnifiedResourceCache.IsInitialized()
}, 5*time.Second, 200*time.Millisecond, "unified resource watcher never initialized")

_, err := srv.Auth().CreateIdentityCenterAccount(ctx, services.IdentityCenterAccount{
Account: &identitycenterv1.Account{
Kind: types.KindIdentityCenterAccount,
Version: types.V1,
Metadata: &headerv1.Metadata{
Name: "test_acct",
Labels: map[string]string{
types.OriginLabel: apicommon.OriginAWSIdentityCenter,
},
},
Spec: &identitycenterv1.AccountSpec{
Id: "11111111",
Arn: "some:arn",
Name: "Test Account",
},
},
})
require.NoError(t, err)

t.Run("access denied", func(t *testing.T) {
// Asserts that, with no RBAC or matchers in place, acces to IC Accounts
// is denied by default

userNoAccess, _, err := CreateUserAndRole(srv.Auth(), "test", nil, nil)
require.NoError(t, err)

identity := TestUser(userNoAccess.GetName())
clt, err := srv.NewClient(identity)
require.NoError(t, err)
defer clt.Close()

_, err = clt.ListResources(ctx, proto.ListResourcesRequest{
ResourceType: types.KindIdentityCenterAccount,
Labels: map[string]string{
types.OriginLabel: apicommon.OriginAWSIdentityCenter,
},
})
require.True(t, trace.IsAccessDenied(err))
})

// TODO(tcsc): Add other tests one RBAC implemented
}

func BenchmarkListUnifiedResourcesFilter(b *testing.B) {
const nodeCount = 150_000
const roleCount = 32
Expand Down
Loading

0 comments on commit aa1132c

Please sign in to comment.