-
Notifications
You must be signed in to change notification settings - Fork 1.8k
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
Exposes Identity Center accounts as Apps in Unified Resource Cache #49301
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this would benefit from a review from someone on the scale team as well who is familiar with unified resources cache. @rosstimothy Who would be best to take a look?
@tcsc Also, I think we're getting overly excited about generics and iterators and should dial back. It is my strong opinion that they hurt readability a lot and we should be using them very sparingly and only if there's a really good reason. I think it took me 3x more time to review this than it should have because of trying to comprehend what all these templated types do and why are they needed and I strongly suspect we do not need any of them.
lib/services/unified_resource.go
Outdated
@@ -470,6 +480,7 @@ func (c *UnifiedResourceCache) getResourcesAndUpdateCurrent(ctx context.Context) | |||
putResources[types.KubeServer](c, newKubes) | |||
putResources[types.SAMLIdPServiceProvider](c, newSAMLApps) | |||
putResources[types.WindowsDesktop](c, newDesktops) | |||
putResources[UnifiedResource153Adapter[IdentityCenterAccount]](c, newICAccounts) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How is this RFD 153 style resource going to work with the ListUnifiedResources RPC which requires a legacy gogo proto style resource? I thought when we discussed this in the past we decided that these should just be applications?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Accounts gets converted to an App on read
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why can't they be inserted into the unified resource cache as an app? Wouldn't that reduce/eliminate 90% of the complexity introduced in this PR?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mainly so they are governed by the RBAC conditions for Identity Center resources, rather than app resources.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What can't RBAC be performed against the identity center resource if it detects the appropriate subkind?
72ece0d
to
9d1bdad
Compare
eff54e2
to
34ea806
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd suggest splitting this up into more focused PRs. There is a lot of what feels like glued on functionality that isn't directly related to unified resource caching being included here.
// AssignmentName is the name of an account assignment representing this | ||
// permission set being assigned to the enclosing account. | ||
string assignment_name = 4; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Move this out into it's own PR and leave this PR solely dedicated to unified resource caching
api/types/resource_153.go
Outdated
if cloner, ok := r.inner.(interface{ CloneResource() Resource153 }); ok { | ||
return &Resource153ToLegacyAdapter{inner: cloner.CloneResource().(Resource153)} | ||
} | ||
panic("interface Resource153 does not implement CloneResource for the wrapped type") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we really panic here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure what the correct alternative is for resources that don't implement CloneResource, short of reflect.DeepCopy
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
An alternative approach would be to validate this during the LegacyToResource153() call when the adapter is created. This would allow you to return an error if the object does not fulfill the required interfaces for cloning.
We can create a new private adapter like toLegacyResourceWithLabels to avoid aligning already exiting code what uses LegacyToResource153
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is why I had a separate implementation in the first place.
api/types/resource_153.go
Outdated
func (r *Resource153ToLegacyAdapter) MatchSearch(searchValues []string) bool { | ||
fieldVals := append(utils.MapToStrings(r.GetAllLabels()), r.GetName()) | ||
return MatchSearch(fieldVals, searchValues, nil) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This might work now, but I can imagine that it will be too limiting for a resource that wants to customize this behavior in the future.
lib/services/unified_resource.go
Outdated
URI: apiconstants.AWSConsoleURL, | ||
PublicAddr: apiconstants.AWSConsoleURL, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The value of apiconstants.AWSConsoleURL
is https://console.aws.amazon.com, which sends user to the root AWS console that lets them pick account with permission set. Since we want to launch users directly to AWS console with specific account+permission set, we need URL value that contains Identity Center instance ID and account ID.
In the POC, I used StartUrl
field to contain such value https://github.com/gravitational/teleport.e/blob/sshah/idc-dev/lib/aws/identitycenter/accounts.go#L156. But in our current version, we have the StartUrl field defined but we missed to enrich the field with instance ID and account ID on initial account import.
tldr; We need to update the newIdentityCenterAccount func to enrich StartUrl field and the use the value here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated the references here. Will update the account constructor in a later PR.
}, | ||
IdentityCenter: &types.AppIdentityCenter{ | ||
AccountID: acct.Spec.Id, | ||
PermissionSets: pss, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Given your another PR #49580 and that we use account assignment resource list to let user select desired permission set for access request, why do we need to include PermissionSets
field and its value?
e4d174c
to
1ca7aec
Compare
8ff81be
to
82f58ee
Compare
Label matching removed and replaces with custom role matchers. |
lib/auth/auth_with_roles.go
Outdated
actionChecker := a.action | ||
if kind == types.KindIdentityCenterAccount { | ||
actionChecker = a.identityCenterAction | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you lave a comment explaining the custom behavior for the types.KindIdentityCenterAccount
kind ?
URI: acct.Spec.StartUrl, | ||
PublicAddr: acct.Spec.StartUrl, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In current state the StartUrl is never set in our backend code:
e git:(tcsc/idc-ui) rg 'StartUrl'
web/teleport/src/services/upgradeWindow/upgradeWindow.ts
16: .get(cfg.getWindowUpgradeStartUrl(clusterId))
31: .post(cfg.getWindowUpgradeStartUrl(clusterId), req)
web/teleport/src/config.ts
182: getWindowUpgradeStartUrl(clusterId: string) {
lib/aws/identitycenter/equal/equal_test.go
145: StartUrl: "https://example.com/start",
164: StartUrl: "https://example.com/start",
lib/aws/identitycenter/equal/account.go
21: s1.GetStartUrl() == s2.GetStartUrl() &&
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is coming in a change discussed with @flyinghermit
// AssignmentName is the assignment resource that will provision an Account | ||
// Assignment for this Permission Set on the enclosing account | ||
AssignmentName string `json:"accountAssignment,omitempty"` | ||
RequiresRequest bool `json:"requiresRequest,omitempty"` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right now the RequiresRequest
field is never set:
Line 188 in 4bdc809
AssignmentName: srcPS.AssignmentName, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will be used when we filter the resources based on access.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you add a go docs ?
lib/web/ui/app.go
Outdated
@@ -66,6 +69,9 @@ type App struct { | |||
// Integration is the integration name that must be used to access this Application. | |||
// Only applicable to AWS App Access. | |||
Integration string `json:"integration,omitempty"` | |||
// PermissionSets holds the permission sets that this app grants access to. | |||
// Only valid for Identity Center Account apps | |||
PermissionSets []IdentityCenterPermissionSet `json:"permission_sets,omitempty"` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The json:camel_cases
seems to be inconsistent with current json style for that uses PascalCase
lib/services/identitycenter.go
Outdated
return true, nil | ||
} | ||
|
||
psMatches, err := m.matchExpression(*(m.permissionSetARN), asmt.PermissionSet) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The naming conventions are quite vague - the asmt.PermissionSet is passed to matchExpression as accountExpression:
m.matchExpression(*(m.permissionSetARN), asmt.PermissionSet)
However, the naming of the second parameter in matchExpression suggests that it should represent an accountExpression:
func (m *IdentityCenterMatcher) matchExpression(target, accountExpression string) (bool, error) {
This inconsistency in parameter naming can be confusing and might lead to misunderstandings about the purpose of the method nad match logic.
lib/services/identitycenter.go
Outdated
|
||
// NewIdentityCenterAccountAssignmentMatcher creates a new [IdentityCenterMatcher] | ||
// configured to match the supplied [IdentityCenterAccountAssignment]. | ||
func NewIdentityCenterAccountAssignmentMatcher(account IdentityCenterAccountAssignment) *IdentityCenterMatcher { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The AWS Account assignment in the business domain is defined as an accountID and permissionSetARN pair. How is it possible that an Account Assignment is missing the permissionSetARN element?
This creates confusion for the reader and demonstrates tight coupling in the current implementation. The matcher logic tries to couple two distinct objects.
I recommend splitting the matcher into separate matchers for each object. This would also help avoid logic like m.permissionSetARN == nil, which is unclear without a broader understanding of the context.
For example, it is not immediately obvious why the permissionSetARN can be empty when matching IdentityCenterAccountAssignments in current code
for _, asmt := range role.GetIdentityCenterAccountAssignments(condition) {
accountMatches, err := m.matchExpression(m.accountID, asmt.Account)
if !accountMatches {
continue
}
// Why permissionSetARN is nil during GetIdentityCenterAccountAssignment match check ?
if m.permissionSetARN == nil {
return true, nil
}
psMatches, err := m.matchExpression(*(m.permissionSetARN), asmt.PermissionSet)
}
By decoupling the logic into separate matchers, the design would become clearer and easier to maintain and reason about business domain. It would also reduce the need for special-case-if logic.
lib/auth/auth_with_roles_test.go
Outdated
}) | ||
|
||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
}) | |
} | |
}) | |
} |
api/types/resource_153.go
Outdated
if cloner, ok := r.inner.(interface{ CloneResource() Resource153 }); ok { | ||
return &Resource153ToLegacyAdapter{inner: cloner.CloneResource().(Resource153)} | ||
} | ||
panic("interface Resource153 does not implement CloneResource for the wrapped type") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
An alternative approach would be to validate this during the LegacyToResource153() call when the adapter is created. This would allow you to return an error if the object does not fulfill the required interfaces for cloning.
We can create a new private adapter like toLegacyResourceWithLabels to avoid aligning already exiting code what uses LegacyToResource153
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have left some comment/suggestion
Also and wonder about:
#49301 (comment)
it feels like it’s attempting to introduce multiple changes at once—such as RBAC > checks, backend updates, and adding new resources.
To make the review process more efficient and focused it might be helpful to break > these changes into separate smaller PRs his way each part can be reviewed and > discussed independently.
For example - the PR description mentions: Exposes Identity Center accounts as Apps > in Unified Resource Cache but it’s not immediately clear how or why RBAC alignment > backend changes ** adding new resources** are tied to this goal.
Splitting the changes could make the purpose of each adjustment clearer and help reviewers better understand the overall intent.
I think that this is worth to consider as it could significantly improve the review process and the overall quality of the PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This makes much more sense than the previous iteration, lgtm once remaining feedback has been addressed.
|
||
// 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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Technically Okta apps also have a different display name because their actual name is an internal Okta ID. What do we use for them?
lib/auth/auth_with_roles.go
Outdated
// 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 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: We literally never use a namespace other than "default", do we really need a separate identityCenterActionNamespace
method?
I would consider just keeping identityCenterAction
and hardcoding apidefaults.Namespace
to keep things a little simpler.
lib/services/identitycenter.go
Outdated
// NewIdentityCenterAccountAssignmentMatcher creates a new [IdentityCenterMatcher] | ||
// configured to match the supplied [IdentityCenterAccountAssignment]. | ||
func NewIdentityCenterAccountAssignmentMatcher(account IdentityCenterAccountAssignment) *IdentityCenterMatcher { | ||
psARN := account.GetSpec().GetPermissionSet().GetArn() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can any of these be nil?
lib/services/role.go
Outdated
if requiresLabelMatching { | ||
matchLabels, labelsMessage, err := checkRoleLabelsMatch(types.Deny, role, traits, r, isDebugEnabled) | ||
if err != nil { | ||
return trace.Wrap(err) | ||
} | ||
if matchLabels { | ||
debugf("Access to %v %q denied, deny rule in role %q matched; match(namespace=%v, %s)", | ||
r.GetKind(), r.GetName(), role.GetName(), namespaceMessage, labelsMessage) | ||
return trace.AccessDenied("access to %v denied. User does not have permissions. %v", | ||
r.GetKind(), additionalDeniedMessage) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This has been here before though and I think "allow" and "deny" code paths are slightly different so may be tricky to factor out. I think it's ok to keep as-is.
3cc204d
to
a3c8cdb
Compare
512945c
to
8ecf2cd
Compare
Moved RBAC in to another PR |
f25e964
to
1e8f8bc
Compare
7bed34c
to
ba6f65d
Compare
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
ba6f65d
to
aa1132c
Compare
…' into tcsc/idc-expose-accounts-as-apps
Backports #49301 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
…che (#49858) Backports #49301 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
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: