Skip to content

Commit

Permalink
Implement ListInvitations and ResolveInvitation APIs for UserService (#…
Browse files Browse the repository at this point in the history
…3666)

Signed-off-by: Radoslav Dimitrov <[email protected]>
  • Loading branch information
rdimitrov authored Jun 19, 2024
1 parent 0acb937 commit 13e69af
Showing 1 changed file with 132 additions and 0 deletions.
132 changes: 132 additions & 0 deletions internal/controlplane/handlers_user.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"net/http"
"path"
"strconv"
"time"

gauth "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/auth"
"github.com/rs/zerolog"
Expand All @@ -29,7 +30,9 @@ import (
"google.golang.org/protobuf/types/known/timestamppb"

"github.com/stacklok/minder/internal/auth"
"github.com/stacklok/minder/internal/authz"
"github.com/stacklok/minder/internal/db"
"github.com/stacklok/minder/internal/flags"
"github.com/stacklok/minder/internal/logger"
"github.com/stacklok/minder/internal/projects"
"github.com/stacklok/minder/internal/util"
Expand Down Expand Up @@ -259,3 +262,132 @@ func (s *Server) GetUser(ctx context.Context, _ *pb.GetUserRequest) (*pb.GetUser

return &resp, nil
}

// ListInvitations is a service for listing invitations.
func (s *Server) ListInvitations(ctx context.Context, _ *pb.ListInvitationsRequest) (*pb.ListInvitationsResponse, error) {
// Check if the UserManagement feature is enabled
if !flags.Bool(ctx, s.featureFlags, flags.UserManagement) {
return nil, status.Error(codes.Unimplemented, "feature not enabled")
}
invitations := make([]*pb.Invitation, 0)

// 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)
}

// 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) {
return &pb.ListInvitationsResponse{}, nil
}
return nil, status.Errorf(codes.Internal, "failed to get invitations: %s", err)
}

// Build the response list of invitations
for _, invite := range invites {
// Get the sponsor's user information (current user)
sponsorUser, err := s.store.GetUserByID(ctx, invite.Sponsor)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to get user: %s", err)
}
// Resolve the sponsor's identity and display name
identity := &auth.Identity{
Provider: nil,
UserID: sponsorUser.IdentitySubject,
HumanName: sponsorUser.IdentitySubject,
}
if flags.Bool(ctx, s.featureFlags, flags.IDPResolver) {
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)
}
}
invitations = append(invitations, &pb.Invitation{
Code: invite.Code,
Role: invite.Role,
Email: invite.Email,
Project: invite.Project.String(),
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,
SponsorDisplay: identity.Human(),
})
}

return &pb.ListInvitationsResponse{
Invitations: invitations,
}, nil
}

// ResolveInvitation is a service for resolving an invitation.
func (s *Server) ResolveInvitation(ctx context.Context, req *pb.ResolveInvitationRequest) (*pb.ResolveInvitationResponse, error) {
// Check if the UserManagement feature is enabled
if !flags.Bool(ctx, s.featureFlags, flags.UserManagement) {
return nil, status.Error(codes.Unimplemented, "feature not enabled")
}

// 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)
}

// Check if the invitation code is valid
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")
}
return nil, status.Errorf(codes.Internal, "failed to get invitation: %s", err)
}

// Check if the invitation matches the user email
if token.Email() != ret.Email {
return nil, util.UserVisibleError(codes.PermissionDenied, "invitation does not match user")
}

// Check if the invitation is expired
if time.Now().After(ret.UpdatedAt.Add(7 * 24 * time.Hour)) {
return nil, util.UserVisibleError(codes.PermissionDenied, "invitation expired")
}

isAccepted := false
// Accept invitation
if req.Accept {
// Parse the role
authzRole, err := authz.ParseRole(ret.Role)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to parse invitation role: %s", err)
}
// Add the user to the project
if err := s.authzClient.Write(ctx, token.Subject(), authzRole, ret.Project); err != nil {
return nil, status.Errorf(codes.Internal, "error writing role assignment: %v", err)
}
isAccepted = true
}

// 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)
}
return &pb.ResolveInvitationResponse{
Role: ret.Role,
Project: ret.Project.String(),
Email: ret.Email,
IsAccepted: isAccepted,
}, nil
}

0 comments on commit 13e69af

Please sign in to comment.