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
- Adds a RoleMatcher for matching Identity Center resources against the Account
  Assignments in Role Condition Statements
- Adds a special actionChecker that checks actions against Identity Center
  resource kinds, falling back to the generic `KindIdentityCenter` unless
  explicitly denied.
- Disables label matching for Identity Center Account resources
- General plumbing from backend through to cache and UI
  • Loading branch information
tcsc committed Dec 4, 2024
1 parent e734b04 commit 82f58ee
Show file tree
Hide file tree
Showing 18 changed files with 868 additions and 54 deletions.
2 changes: 2 additions & 0 deletions api/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -3807,6 +3807,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
74 changes: 73 additions & 1 deletion api/types/resource_153.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,12 @@ import (
"encoding/json"
"time"

"google.golang.org/protobuf/protoadapt"
"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"
apiutils "github.com/gravitational/teleport/api/utils"
)

// ResourceMetadata is the smallest interface that defines a Teleport resource.
Expand Down Expand Up @@ -116,7 +119,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 +134,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 +218,69 @@ func (r *resource153ToLegacyAdapter) SetRevision(rev string) {
func (r *resource153ToLegacyAdapter) SetSubKind(subKind string) {
panic("interface Resource153 does not implement SetSubKind")
}

func (r *resource153ToLegacyAdapter) Origin() string {
m := r.inner.GetMetadata()
if m == nil {
return ""
}
return m.Labels[OriginLabel]
}

func (r *resource153ToLegacyAdapter) SetOrigin(origin string) {
m := r.inner.GetMetadata()
if m == nil {
return
}
m.Labels[OriginLabel] = origin
}

func (r *resource153ToLegacyAdapter) GetLabel(key string) (value string, ok bool) {
m := r.inner.GetMetadata()
if m == nil {
return "", false
}
value, ok = m.Labels[key]
return
}

func (r *resource153ToLegacyAdapter) GetAllLabels() map[string]string {
m := r.inner.GetMetadata()
if m == nil {
return nil
}
return m.Labels
}

func (r *resource153ToLegacyAdapter) GetStaticLabels() map[string]string {
return r.GetAllLabels()
}

func (r *resource153ToLegacyAdapter) SetStaticLabels(labels map[string]string) {
m := r.inner.GetMetadata()
if m == nil {
return
}
m.Labels = labels
}

func (r *resource153ToLegacyAdapter) MatchSearch(searchValues []string) bool {
fieldVals := append(utils.MapToStrings(r.GetAllLabels()), r.GetName())
return MatchSearch(fieldVals, searchValues, nil)
}

func (r *resource153ToLegacyAdapter) CloneResource() ResourceWithLabels {
switch clonable := r.inner.(type) {
case interface{ CloneResource() Resource153 }:
return &resource153ToLegacyAdapter{
inner: clonable.CloneResource(),
}

case protoadapt.MessageV1:
return &resource153ToLegacyAdapter{
inner: apiutils.CloneProtoMsg(clonable).(Resource153),
}
}

panic("interface Resource153 does not implement CloneResource for the wrapped type")
}
3 changes: 2 additions & 1 deletion api/types/resource_153_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,8 @@ func TestResource153ToLegacy(t *testing.T) {

// Unwrap gives the underlying resource back.
t.Run("unwrap", func(t *testing.T) {
unwrapped := legacyResource.(interface{ Unwrap() types.Resource153 }).Unwrap()
unwrapper := types.Resource153Unwrapper(legacyResource)
unwrapped := unwrapper.Unwrap()
if diff := cmp.Diff(bot, unwrapped, protocmp.Transform()); diff != "" {
t.Errorf("Unwrap mismatch (-want +got)\n%s", diff)
}
Expand Down
17 changes: 17 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,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
}
47 changes: 42 additions & 5 deletions lib/auth/auth_with_roles.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,26 @@ func (a *ServerWithRoles) authConnectorAction(resource string, verb string) erro
return nil
}

// identityCenterAction wraps [ServerWithRoles.identityCenterActionNamespace],
// supplying the default namespace for convenience.
func (a *ServerWithRoles) identityCenterAction(resource string, verbs ...string) error {
return a.identityCenterActionNamespace(apidefaults.Namespace, resource, verbs...)
}

// identityCenterActionNamespace is a special checker that grants access to
// Identity Center resources. In order to simplify the writing of role condition
// statements, the various Identity Center resources are bundled up under an
// umbrella `KindIdentityCenter` resource kind. This means that if access to the
// target resource is not explicitly denied, then the user has a second chance
// to get access via the generic resource kind.
func (a *ServerWithRoles) identityCenterActionNamespace(namespace string, resource string, verbs ...string) error {
err := a.actionNamespace(namespace, resource, verbs...)
if err == nil || services.IsAccessExplicitlyDenied(err) {
return trace.Wrap(err)
}
return trace.Wrap(a.actionNamespace(namespace, types.KindIdentityCenter, verbs...))
}

// actionForListWithCondition extracts a restrictive filter condition to be
// added to a list query after a simple resource check fails.
func (a *ServerWithRoles) actionForListWithCondition(resource, identifier string) (*types.WhereExpr, error) {
Expand Down Expand Up @@ -1329,7 +1349,12 @@ func (a *ServerWithRoles) ListUnifiedResources(ctx context.Context, req *proto.L
actionVerbs = []string{types.VerbList}
}

resourceAccess.kindAccessMap[kind] = a.action(kind, actionVerbs...)
actionChecker := a.action
if kind == types.KindIdentityCenterAccount {
actionChecker = a.identityCenterAction
}

resourceAccess.kindAccessMap[kind] = actionChecker(kind, actionVerbs...)
}

// Before doing any listing, verify that the user is allowed to list
Expand Down Expand Up @@ -1666,13 +1691,19 @@ 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)
}

if err := a.actionNamespace(req.Namespace, req.ResourceType, actionVerbs...); err != nil {
actionChecker := a.actionNamespace
if req.ResourceType == types.KindIdentityCenterAccount {
actionChecker = a.identityCenterActionNamespace
}

if err := actionChecker(req.Namespace, req.ResourceType, actionVerbs...); err != nil {
return nil, trace.Wrap(err)
}

Expand Down Expand Up @@ -1799,9 +1830,14 @@ func (r resourceChecker) CanAccess(resource types.Resource) error {
}
case types.SAMLIdPServiceProvider:
return r.CheckAccess(rr, state)

case types.Resource153Unwrapper:
if checkable, ok := rr.(services.AccessCheckable); ok {
return r.CheckAccess(checkable, 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 @@ -1816,7 +1852,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
Loading

0 comments on commit 82f58ee

Please sign in to comment.