-
Notifications
You must be signed in to change notification settings - Fork 43
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
Refactor UpdateRole and add display names to invite-related responses #3689
Conversation
Signed-off-by: Radoslav Dimitrov <radoslav@stacklok.com>
Signed-off-by: Radoslav Dimitrov <radoslav@stacklok.com>
Signed-off-by: Radoslav Dimitrov <radoslav@stacklok.com>
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.
A bunch of questions inline
prj := targetPrj.String() | ||
// Get all invitations for this email, project and role | ||
inviteToRemove, err := s.store.GetInvitationByEmailAndProjectAndRole(ctx, db.GetInvitationByEmailAndProjectAndRoleParams{ | ||
// Get all invitations for this email and project |
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.
A bit of a side-note, but I wonder if extracting all those non-exported methods like removeInvite
, removeRole
etc into interfaces (RoleService and InviteService maybe?) might make testing the area of code easier and would encapsulate the code better to only rely on what they need and not the whole big Server structure. Not for this PR though.
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.
👍 on this suggestion (and to do it in a separate 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.
I think we could have a single big user-management service, at least for now.
} | ||
|
||
// Get the list of invitations for the user | ||
invites, err := s.store.GetInvitationsByEmail(ctx, token.Email()) | ||
invites, err := s.store.GetInvitationsByEmail(ctx, tokenEmail) |
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 works only for invites for the address the user has in their token right?
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.
Is there a way to list the invites by the identity instead and look up the identity by e-mail?
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 expect that the same user identity can have multiple emails in their token (they get that from Keycloak right?) The idea was to check that for listing invitations, but still allow the user to accept/decline invitations by their code (without cross-checking the email)
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.
OK, maybe I'm missing what is the list invitations for. Is it supposed to be used by the user after they had logged in? Or by the UI?
I'm concerned about 2 things with respect to the invitations - since we would get the e-mail from keycloak we 1) can't rely on having the e-mail available I think or 2) it could be unrelated to the e-mail where invites are supposed to be sent (e.g. my e-mail address is available but it's my personal e-mail)
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.
OK, but looking at the invite table, I don't think we have any other data to cross-reference the invite with. So this is more to be careful where/how we use that endpoint - it might not give us a complete list I think.
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.
Yes, this is supposed to be called from a logged in user to check their pending invitations (both cli and ui). My assumption is we can rely on having the mail in the token since I don't think we have a use case where this is not true, i.e logging through GitHub does set the primary mail there. If the invite is sent to your company email and for whatever reason you want to accept it from your minder account using your personal mail you can still do that through the accept/decline flow but you are correct that it will not be shown via ListInvitations (so far this is the intended behaviour).
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.
Also invitations work solely through using the email(for the time being you cannot send an invite to a user using their minder account)
Signed-off-by: Radoslav Dimitrov <radoslav@stacklok.com>
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.
Only two more small comments, but non-blocking.
identity, err := s.idClient.Resolve(ctx, sponsorUser.IdentitySubject) | ||
if err != nil { | ||
zerolog.Ctx(ctx).Error().Err(err).Msg("error resolving identity") | ||
return nil, util.UserVisibleError(codes.NotFound, "could not find identity %q", sponsorUser.IdentitySubject) |
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.
Does this need to be a fatal error? (Ditto for getting the sponsorUser above) - could we just return some raw ID? Or do you think it's better to just return all well-formed as the UI expects it or fail?
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.
Addressed in #3710
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 fine with addressing some/all of my feedback in another PR. I am slightly concerned that our final user management will end up feeling complex to users because of the number of different states and transitions we support -- it would be nice if AssignRole
could basically do something like the following:
if user in project.roles:
# This is not "upsert", because the user already exists.
project.update_role(user, role)
else:
project.upsert_invite(user, role)
t.Render() | ||
return nil | ||
} | ||
// Otherwise, print the role assignments if it was about updating a role |
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 feels a little weird -- it feels like we should either show the output of role grant list
, or only include details for the current invitation / grant.
In particular, if we're updating a grant, we will get back the code as a one-time thing, and we should specifically print something like:
Created an invite for $EMAIL to $ROLE on $PROJECT. They can access it via $URL, or with $CODE.
Where $URL is something that includes the code and will redirect them to the Stacklok UI if needed.
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.
(Some part of this could be a TODO for later...)
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.
Partially addressed it in #3710
updateCmd.Flags().StringP("sub", "s", "", "subject to update role access for") | ||
updateCmd.Flags().StringP("email", "e", "", "email to send invitation to") |
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 a big fan of having separate sub
and email
parameters -- can we make this just one parameter (e.g. sub
), and then have the backend determine whether this should be an invite or just a role grant?
That avoids issues where (for example), the user is already a project member, but then the admin gets an invite code when it should have been a direct grant.
// Validate the subject and email - decide if it's about updating an invitation or a role assignment | ||
if sub == "" && email != "" { |
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.
IMO, this feels like it should be "if specified user is not in current OpenFGA roles, then look for invitations..."
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.
But I'm okay leaving it for now.
func (s *Server) updateInvite( | ||
ctx context.Context, | ||
targetProject uuid.UUID, | ||
authzRole authz.Role, | ||
email string, | ||
) (*minder.UpdateRoleResponse, 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.
I wish this could be an upsert
... I'm assuming that doesn't make sense given our RPCs, though, right?
return nil, status.Errorf(codes.Internal, "error getting invitations: %v", err) | ||
} | ||
|
||
// Exit early if there are no or multiple existing invitations for this email and project |
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's the recovery method if there are multiple existing invites? Is this something a user can resolve on their own?
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.
Yes. if this happens the user can list all invitations and remove the ones that are conflicting. We do have protections against it when creating invites but still a valid point 👍
// At this point, there should be exactly 1 invitation. We should either update its expiration or | ||
// discard it and create a new one |
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 confused about this logic, and when we extend an invite vs re-create it. I think this code does:
- User A invites foo@email.com to be a Viewer
- 8 days pass (expires) and then User A invites foo@email.com as a Viewer again (Update)
- 2 days pass and User A updates the invite for foo@email.com to Editor (Replace)
- 5 days pass and User B updates the invite for foo@email.com to Editor (Update)
- 12 days pass (expires) and User C renews the invite for foo@email.com to Editor (Update)
Is that correct? Why do we preserve the original invite for users B and C, but reset the creation date when A updates the role?
Do we need both a creation and and an update for the invitation?
Summary
The following PR includes a few updates on the user management implementation.
The changes that are introduced are:
minder role grant list
command to also print current invitations along with current role assignmentsflags.IDPResolver
Change Type
Mark the type of change your PR introduces:
Testing
Locally.
Review Checklist: