Skip to content
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

Implement ListInvitations and ResolveInvitation APIs for UserService #3666

Merged
merged 1 commit into from
Jun 19, 2024

Conversation

rdimitrov
Copy link
Member

Summary

The following PR implements ListInvitations and ResolveInvitation APIs for UserService.

Fixes #(related issue)

Change Type

Mark the type of change your PR introduces:

  • Bug fix (resolves an issue without affecting existing features)
  • Feature (adds new functionality without breaking changes)
  • Breaking change (may impact existing functionalities or require documentation updates)
  • Documentation (updates or additions to documentation)
  • Refactoring or test improvements (no bug fixes or new functionality)

Testing

Outline how the changes were tested, including steps to reproduce and any relevant configurations.
Attach screenshots if helpful.

Review Checklist:

  • Reviewed my own code for quality and clarity.
  • Added comments to complex or tricky code sections.
  • Updated any affected documentation.
  • Included tests that validate the fix or feature.
  • Checked that related changes are merged.

@rdimitrov rdimitrov requested a review from a team as a code owner June 19, 2024 15:38
}

// Check if the invitation matches the user email
if token.Email() != ret.Email {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implemented this per - https://github.com/stacklok/minder/blob/07f93e0c1dcb59f74b6f15beb24cf684161287bb/database/query/invitations.sql#L15

which ensures if the email of the logged-in user corresponds to the one of the invite.

@evankanderson - I know we discussed this to be more loose but then I saw that you made that comment so I implemented it that way. I'll ping you to see which is the desired behavior we want to keep.

For the reviewers, don't take this comment as blocking as we can remove this check easily.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, we want to remove this for Resolve.

The comment was for ListInvitations -- a user might have an invitation that doesn't match up with their "known" email address. We still want to honor that code, even though it doesn't show up in ListInvitations. Feel free to clarify the comment in the schema to help make that more clear.

Signed-off-by: Radoslav Dimitrov <radoslav@stacklok.com>
@coveralls
Copy link

Coverage Status

coverage: 53.214% (-0.3%) from 53.476%
when pulling 1e42c31 on invite
into 07f93e0 on main.

@coveralls
Copy link

Coverage Status

coverage: 53.214% (-0.3%) from 53.476%
when pulling 1e42c31 on invite
into 07f93e0 on main.

Copy link
Contributor

@dmjb dmjb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't look like we have any unit test coverage for these, and there's enough new logic here that I feel like it would benefit from testing.

I wonder if splitting the logic between the handler, and an InviteService struct/interface would simplify writing tests too.

Copy link
Contributor

@dmjb dmjb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving to unblock FE. Let's get some tests for this written sooner rather than later :)

@rdimitrov rdimitrov merged commit 13e69af into main Jun 19, 2024
21 checks passed
@rdimitrov rdimitrov deleted the invite branch June 19, 2024 16:14
Copy link
Member

@evankanderson evankanderson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall LGTM, but a few questions / comments for a followup (sorry, I'm half-out today for Juneteenth, and then half-out Friday taking the kids on a trip).

Comment on lines +274 to +282
// Extracts the user email from the token
tokenString, err := gauth.AuthFromMD(ctx, "bearer")
if err != nil {
return nil, status.Errorf(codes.InvalidArgument, "no auth token: %v", err)
}
token, err := s.jwt.ParseAndValidate(tokenString)
if err != nil {
return nil, status.Errorf(codes.Unauthenticated, "failed to parse bearer token: %v", err)
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be nice to have "give me a parsed JWT" as a method, rather than needing this in the business logic.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a GetUserEmailFromContext method to internal/auth/jwtauth.go? We already have GetUserSubjectFromContext and GetUserClaimFromContext, so this feels like it should belong there.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(We've already parsed the JWT in TokenValidationInterceptor, so we should leverage that.)

// Get the list of invitations for the user
invites, err := s.store.GetInvitationsByEmail(ctx, token.Email())
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sql.ErrNoRows is only returned for :one result types, not :many, so this code should never be hit.

UserID: sponsorUser.IdentitySubject,
HumanName: sponsorUser.IdentitySubject,
}
if flags.Bool(ctx, s.featureFlags, flags.IDPResolver) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure whether we need to leave this behind this flag or not... I was thinking of removing (retiring) this flag, since we went a different approach.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Which would allow you to simplify the code by simply calling s.idClient.Resolve, rather than constructing a default Identity object.)

Email: invite.Email,
Project: invite.Project.String(),
CreatedAt: timestamppb.New(invite.CreatedAt),
ExpiresAt: timestamppb.New(invite.UpdatedAt.Add(7 * 24 * time.Hour)),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you want to extract the ExpiresAt math, since it's used in two fields?

CreatedAt: timestamppb.New(invite.CreatedAt),
ExpiresAt: timestamppb.New(invite.UpdatedAt.Add(7 * 24 * time.Hour)),
Expired: time.Now().After(invite.UpdatedAt.Add(7 * 24 * time.Hour)),
Sponsor: identity.UserID,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use identity.String() here, I think. It doesn't do anything right now, but if we decided to add other identity sources like GitHub Actions, that would allow us to refer to them uniquely.

ret, err := s.store.GetInvitationByCode(ctx, req.Code)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, util.UserVisibleError(codes.NotFound, "invitation not found")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should probably say "not found or already used"

}

// Check if the invitation matches the user email
if token.Email() != ret.Email {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, we want to remove this for Resolve.

The comment was for ListInvitations -- a user might have an invitation that doesn't match up with their "known" email address. We still want to honor that code, even though it doesn't show up in ListInvitations. Feel free to clarify the comment in the schema to help make that more clear.

// Delete the invitation since its resolved
ret, err = s.store.DeleteInvitation(ctx, req.Code)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to delete invitation: %s", err)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens in this case -- the user has been added to the OpenFGA database; if they try calling ResolveInvitation again, will they create a second tuple? Will OpenFGA error with "already exists"?

We may want to return a util.UserVisibleError(codes.Internal, "permission granted, but unable to revoke invitation") after logging the error internally here.

We may also want to check what the result of attempting to grant the same permission twice is in terms of OpenFGA errors, and account for that specially.

return nil, util.UserVisibleError(codes.PermissionDenied, "invitation expired")
}

isAccepted := false
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need isAccepted, or can we just use req.Accept below?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants