Skip to content

Commit 484a8ce

Browse files
authored
feat(invitations): Expand invitation context to external metadata (#2299)
Signed-off-by: Javier Rodriguez <[email protected]>
1 parent 403f4a1 commit 484a8ce

File tree

7 files changed

+102
-39
lines changed

7 files changed

+102
-39
lines changed

app/controlplane/pkg/biz/group.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -557,7 +557,7 @@ func (uc *GroupUseCase) handleNonExistingUser(ctx context.Context, orgID, groupI
557557

558558
// Create an organization invitation with group context
559559
invitationContext := &OrgInvitationContext{
560-
GroupIDToJoin: groupID,
560+
GroupIDToJoin: &groupID,
561561
GroupMaintainer: opts.Maintainer,
562562
}
563563

app/controlplane/pkg/biz/orginvitation.go

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ package biz
1717

1818
import (
1919
"context"
20+
"encoding/json"
2021
"fmt"
2122
"strings"
2223
"time"
@@ -58,13 +59,15 @@ type OrgInvitation struct {
5859
// OrgInvitationContext is used to pass additional context when accepting an invitation
5960
type OrgInvitationContext struct {
6061
// GroupIDToJoin is the ID of the group to join when accepting the invitation
61-
GroupIDToJoin uuid.UUID `json:"group_id_to_join,omitempty"`
62+
GroupIDToJoin *uuid.UUID `json:"group_id_to_join,omitempty"`
6263
// GroupMaintainer indicates if the user should be added as a maintainer of the group
6364
GroupMaintainer bool `json:"group_maintainer,omitempty"`
6465
// ProjectIDToJoin is the ID of the project to join when accepting the invitation
65-
ProjectIDToJoin uuid.UUID `json:"project_id_to_join,omitempty"`
66+
ProjectIDToJoin *uuid.UUID `json:"project_id_to_join,omitempty"`
6667
// ProjectRole is the role to assign to the user in the project
6768
ProjectRole authz.Role `json:"project_role,omitempty"`
69+
// ExternalMetadata can be used to store additional information
70+
ExternalMetadata json.RawMessage `json:"external_metadata,omitempty"`
6871
}
6972

7073
type OrgInvitationRepo interface {
@@ -338,20 +341,20 @@ func (uc *OrgInvitationUseCase) FindByID(ctx context.Context, invitationID strin
338341
// processGroupMembership adds a user to a group if the invitation context contains a group to join
339342
func (uc *OrgInvitationUseCase) processGroupMembership(ctx context.Context, invitation *OrgInvitation, orgUUID uuid.UUID, userUUID uuid.UUID) error {
340343
// Skip if there's no group to join in the invitation context
341-
if invitation.Context == nil || invitation.Context.GroupIDToJoin == uuid.Nil {
344+
if invitation.Context == nil || invitation.Context.GroupIDToJoin == nil || *invitation.Context.GroupIDToJoin == uuid.Nil {
342345
return nil
343346
}
344347

345348
groupID := invitation.Context.GroupIDToJoin
346349
uc.logger.Infow("msg", "Adding user to group", "invitation_id", invitation.ID.String(), "org_id", invitation.Org.ID, "user_id", userUUID, "group_id", groupID)
347350

348351
// Check if the group exists
349-
gr, err := uc.groupRepo.FindByOrgAndID(ctx, orgUUID, groupID)
352+
gr, err := uc.groupRepo.FindByOrgAndID(ctx, orgUUID, *groupID)
350353
if err != nil {
351354
return fmt.Errorf("error finding group %s: %w", groupID.String(), err)
352355
}
353356

354-
if _, err := uc.groupRepo.AddMemberToGroup(ctx, orgUUID, groupID, userUUID, invitation.Context.GroupMaintainer); err != nil {
357+
if _, err := uc.groupRepo.AddMemberToGroup(ctx, orgUUID, *groupID, userUUID, invitation.Context.GroupMaintainer); err != nil {
355358
if IsErrAlreadyExists(err) {
356359
// User is already a member of the group, nothing to do
357360
uc.logger.Infow("msg", "User already in group", "invitation_id", invitation.ID.String(), "org_id", invitation.Org.ID, "user_id", userUUID.String(), "group_id", groupID.String())
@@ -364,7 +367,7 @@ func (uc *OrgInvitationUseCase) processGroupMembership(ctx context.Context, invi
364367
// Dispatch event to the audit log for group membership addition
365368
uc.auditor.Dispatch(ctx, &events.GroupMemberAdded{
366369
GroupBase: &events.GroupBase{
367-
GroupID: &groupID,
370+
GroupID: groupID,
368371
GroupName: gr.Name,
369372
},
370373
UserID: &userUUID,
@@ -380,15 +383,15 @@ func (uc *OrgInvitationUseCase) processGroupMembership(ctx context.Context, invi
380383
// processProjectMembership adds a user to a project if the invitation context contains a project to join
381384
func (uc *OrgInvitationUseCase) processProjectMembership(ctx context.Context, invitation *OrgInvitation, orgUUID uuid.UUID, userUUID uuid.UUID) error {
382385
// Skip if there's no group to join in the invitation context
383-
if invitation.Context == nil || invitation.Context.ProjectIDToJoin == uuid.Nil {
386+
if invitation.Context == nil || invitation.Context.ProjectIDToJoin == nil || *invitation.Context.ProjectIDToJoin == uuid.Nil {
384387
return nil
385388
}
386389

387390
projectID := invitation.Context.ProjectIDToJoin
388391
uc.logger.Infow("msg", "Adding user to project", "invitation_id", invitation.ID.String(), "org_id", invitation.Org.ID, "user_id", userUUID, "project_id", projectID)
389392

390393
// Check if the project exists
391-
project, err := uc.projectRepo.FindProjectByOrgIDAndID(ctx, orgUUID, projectID)
394+
project, err := uc.projectRepo.FindProjectByOrgIDAndID(ctx, orgUUID, *projectID)
392395
if err != nil {
393396
return fmt.Errorf("error finding project %s: %w", projectID.String(), err)
394397
}
@@ -406,7 +409,7 @@ func (uc *OrgInvitationUseCase) processProjectMembership(ctx context.Context, in
406409
}
407410

408411
// Check if the user is already a member of the project
409-
existingMembership, err := uc.projectRepo.FindProjectMembershipByProjectAndID(ctx, orgUUID, projectID, userUUID, authz.MembershipTypeUser)
412+
existingMembership, err := uc.projectRepo.FindProjectMembershipByProjectAndID(ctx, orgUUID, *projectID, userUUID, authz.MembershipTypeUser)
410413
if err != nil && !IsNotFound(err) {
411414
return fmt.Errorf("error checking project membership for user %s: %w", userUUID, err)
412415
}
@@ -418,14 +421,14 @@ func (uc *OrgInvitationUseCase) processProjectMembership(ctx context.Context, in
418421
}
419422

420423
// Add the user to the project
421-
if _, err := uc.projectRepo.AddMemberToProject(ctx, orgUUID, projectID, userUUID, authz.MembershipTypeUser, role); err != nil {
424+
if _, err := uc.projectRepo.AddMemberToProject(ctx, orgUUID, *projectID, userUUID, authz.MembershipTypeUser, role); err != nil {
422425
return fmt.Errorf("error adding user %s to project %s: %w", userUUID, projectID.String(), err)
423426
}
424427

425428
// Dispatch event to the audit log for project membership addition
426429
uc.auditor.Dispatch(ctx, &events.ProjectMembershipAdded{
427430
ProjectBase: &events.ProjectBase{
428-
ProjectID: &projectID,
431+
ProjectID: projectID,
429432
ProjectName: project.Name,
430433
},
431434
UserID: &userUUID,

app/controlplane/pkg/biz/orginvitation_integration_test.go

Lines changed: 68 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -314,7 +314,7 @@ func (s *OrgInvitationIntegrationTestSuite) TestInvitationWithGroupContext() {
314314
s.T().Run("invitation with group context adds user to group when accepted", func(t *testing.T) {
315315
// Create invitation context with group information
316316
invitationContext := &biz.OrgInvitationContext{
317-
GroupIDToJoin: group.ID,
317+
GroupIDToJoin: &group.ID,
318318
GroupMaintainer: true,
319319
}
320320

@@ -332,7 +332,7 @@ func (s *OrgInvitationIntegrationTestSuite) TestInvitationWithGroupContext() {
332332

333333
// Verify context was saved properly
334334
assert.NotNil(t, invite.Context)
335-
assert.Equal(t, group.ID, invite.Context.GroupIDToJoin)
335+
assert.Equal(t, group.ID, *invite.Context.GroupIDToJoin)
336336
assert.Equal(t, true, invite.Context.GroupMaintainer)
337337

338338
// Accept the invitation
@@ -380,7 +380,7 @@ func (s *OrgInvitationIntegrationTestSuite) TestInvitationWithGroupContext() {
380380

381381
// Create invitation context with group information, but not as maintainer
382382
invitationContext := &biz.OrgInvitationContext{
383-
GroupIDToJoin: group.ID,
383+
GroupIDToJoin: &group.ID,
384384
GroupMaintainer: false,
385385
}
386386

@@ -447,7 +447,7 @@ func (s *OrgInvitationIntegrationTestSuite) TestInvitationWithProjectContext() {
447447
s.T().Run("invitation with project context adds user to project when accepted", func(t *testing.T) {
448448
// Create invitation context with project information
449449
invitationContext := &biz.OrgInvitationContext{
450-
ProjectIDToJoin: project.ID,
450+
ProjectIDToJoin: &project.ID,
451451
ProjectRole: authz.RoleProjectAdmin,
452452
}
453453

@@ -465,7 +465,7 @@ func (s *OrgInvitationIntegrationTestSuite) TestInvitationWithProjectContext() {
465465

466466
// Verify context was saved properly
467467
assert.NotNil(t, invite.Context)
468-
assert.Equal(t, project.ID, invite.Context.ProjectIDToJoin)
468+
assert.Equal(t, project.ID, *invite.Context.ProjectIDToJoin)
469469
assert.Equal(t, authz.RoleProjectAdmin, invite.Context.ProjectRole)
470470

471471
// Accept the invitation
@@ -511,7 +511,7 @@ func (s *OrgInvitationIntegrationTestSuite) TestInvitationWithProjectContext() {
511511

512512
// Create invitation context with project information, but with viewer role
513513
invitationContext := &biz.OrgInvitationContext{
514-
ProjectIDToJoin: project.ID,
514+
ProjectIDToJoin: &project.ID,
515515
ProjectRole: authz.RoleProjectViewer,
516516
}
517517

@@ -570,9 +570,9 @@ func (s *OrgInvitationIntegrationTestSuite) TestInvitationWithProjectContext() {
570570

571571
// Create invitation context with both group and project information
572572
invitationContext := &biz.OrgInvitationContext{
573-
GroupIDToJoin: group.ID,
573+
GroupIDToJoin: &group.ID,
574574
GroupMaintainer: true,
575-
ProjectIDToJoin: project.ID,
575+
ProjectIDToJoin: &project.ID,
576576
ProjectRole: authz.RoleProjectViewer,
577577
}
578578

@@ -590,9 +590,9 @@ func (s *OrgInvitationIntegrationTestSuite) TestInvitationWithProjectContext() {
590590

591591
// Verify context was saved properly
592592
assert.NotNil(t, invite.Context)
593-
assert.Equal(t, group.ID, invite.Context.GroupIDToJoin)
593+
assert.Equal(t, group.ID, *invite.Context.GroupIDToJoin)
594594
assert.True(t, invite.Context.GroupMaintainer)
595-
assert.Equal(t, project.ID, invite.Context.ProjectIDToJoin)
595+
assert.Equal(t, project.ID, *invite.Context.ProjectIDToJoin)
596596
assert.Equal(t, authz.RoleProjectViewer, invite.Context.ProjectRole)
597597

598598
// Accept the invitation
@@ -645,6 +645,64 @@ func (s *OrgInvitationIntegrationTestSuite) TestInvitationWithProjectContext() {
645645
assert.True(t, foundProjectMember, "The user should be a member of the project")
646646
assert.Equal(t, authz.RoleProjectViewer, projectRole, "The user should have the project contributor role")
647647
})
648+
649+
s.T().Run("invitation with nil UUID on project is rejected", func(t *testing.T) {
650+
// Create a new receiver that isn't a member of any org yet
651+
newReceiverEmail := "[email protected]"
652+
newReceiver, err := s.User.UpsertByEmail(ctx, newReceiverEmail, nil)
653+
require.NoError(t, err)
654+
require.NotNil(t, newReceiver)
655+
656+
// Create invitation context with nil project ID
657+
invitationContext := &biz.OrgInvitationContext{
658+
ProjectIDToJoin: &uuid.Nil,
659+
}
660+
661+
// Create invitation with combined context
662+
invite, err := s.Repos.OrgInvitationRepo.Create(
663+
ctx,
664+
uuid.MustParse(s.org1.ID),
665+
uuid.MustParse(s.user.ID),
666+
newReceiverEmail,
667+
authz.RoleViewer,
668+
invitationContext,
669+
)
670+
require.NoError(t, err)
671+
require.NotNil(t, invite)
672+
673+
// Accept the invitation and check that there is no error
674+
err = s.OrgInvitation.AcceptPendingInvitations(ctx, newReceiverEmail)
675+
require.NoError(t, err, "Accepting invitation with nil project ID should not fail just skip the project context")
676+
})
677+
678+
s.T().Run("invitation with nil UUID on group is rejected", func(t *testing.T) {
679+
// Create a new receiver that isn't a member of any org yet
680+
newReceiverEmail := "[email protected]"
681+
newReceiver, err := s.User.UpsertByEmail(ctx, newReceiverEmail, nil)
682+
require.NoError(t, err)
683+
require.NotNil(t, newReceiver)
684+
685+
// Create invitation context with nil group ID
686+
invitationContext := &biz.OrgInvitationContext{
687+
GroupIDToJoin: &uuid.Nil,
688+
}
689+
690+
// Create invitation with combined context
691+
invite, err := s.Repos.OrgInvitationRepo.Create(
692+
ctx,
693+
uuid.MustParse(s.org1.ID),
694+
uuid.MustParse(s.user.ID),
695+
newReceiverEmail,
696+
authz.RoleViewer,
697+
invitationContext,
698+
)
699+
require.NoError(t, err)
700+
require.NotNil(t, invite)
701+
702+
// Accept the invitation and check that there is no error
703+
err = s.OrgInvitation.AcceptPendingInvitations(ctx, newReceiverEmail)
704+
require.NoError(t, err, "Accepting invitation with nil group ID should not fail just skip the project context")
705+
})
648706
}
649707

650708
// Utility struct to hold the test suite

app/controlplane/pkg/biz/project.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -392,7 +392,7 @@ func (uc *ProjectUseCase) handleNonExistingUser(ctx context.Context, orgID, proj
392392

393393
// Create an organization invitation with project context
394394
invitationContext := &OrgInvitationContext{
395-
ProjectIDToJoin: projectID,
395+
ProjectIDToJoin: &projectID,
396396
ProjectRole: opts.Role,
397397
}
398398

app/controlplane/pkg/biz/project_integration_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -330,7 +330,7 @@ func (s *projectMembersIntegrationTestSuite) TestAddMemberToProject() {
330330

331331
// Verify the invitation has project context
332332
s.NotNil(foundInvitation.Context, "Invitation should have context")
333-
s.Equal(projectID, foundInvitation.Context.ProjectIDToJoin)
333+
s.Equal(projectID, *foundInvitation.Context.ProjectIDToJoin)
334334
s.Equal(authz.RoleProjectViewer, foundInvitation.Context.ProjectRole)
335335
})
336336

@@ -1807,7 +1807,7 @@ func (s *projectMembersIntegrationTestSuite) TestAddNonExistingMemberToProject()
18071807

18081808
// Verify the invitation has project context
18091809
s.NotNil(foundInvitation.Context, "Invitation should have context")
1810-
s.Equal(projectID, foundInvitation.Context.ProjectIDToJoin)
1810+
s.Equal(projectID, *foundInvitation.Context.ProjectIDToJoin)
18111811
s.Equal(authz.RoleProjectViewer, foundInvitation.Context.ProjectRole)
18121812
s.Equal(authz.RoleOrgMember, foundInvitation.Role)
18131813
})

app/controlplane/pkg/biz/testhelpers/database.go

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -82,13 +82,14 @@ type TestingUseCases struct {
8282
}
8383

8484
type TestingRepos struct {
85-
Membership biz.MembershipRepo
86-
Referrer biz.ReferrerRepo
87-
Workflow biz.WorkflowRepo
88-
WorkflowRunRepo biz.WorkflowRunRepo
89-
AttestationState biz.AttestationStateRepo
90-
OrganizationRepo biz.OrganizationRepo
91-
GroupRepo biz.GroupRepo
85+
Membership biz.MembershipRepo
86+
Referrer biz.ReferrerRepo
87+
Workflow biz.WorkflowRepo
88+
WorkflowRunRepo biz.WorkflowRunRepo
89+
AttestationState biz.AttestationStateRepo
90+
OrganizationRepo biz.OrganizationRepo
91+
GroupRepo biz.GroupRepo
92+
OrgInvitationRepo biz.OrgInvitationRepo
9293
}
9394

9495
type newTestingOpts struct {

app/controlplane/pkg/biz/testhelpers/wire_gen.go

Lines changed: 8 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)