Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/controlplane/pkg/biz/group.go
Original file line number Diff line number Diff line change
Expand Up @@ -557,7 +557,7 @@ func (uc *GroupUseCase) handleNonExistingUser(ctx context.Context, orgID, groupI

// Create an organization invitation with group context
invitationContext := &OrgInvitationContext{
GroupIDToJoin: groupID,
GroupIDToJoin: &groupID,
GroupMaintainer: opts.Maintainer,
}

Expand Down
25 changes: 14 additions & 11 deletions app/controlplane/pkg/biz/orginvitation.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package biz

import (
"context"
"encoding/json"
"fmt"
"strings"
"time"
Expand Down Expand Up @@ -58,13 +59,15 @@ type OrgInvitation struct {
// OrgInvitationContext is used to pass additional context when accepting an invitation
type OrgInvitationContext struct {
// GroupIDToJoin is the ID of the group to join when accepting the invitation
GroupIDToJoin uuid.UUID `json:"group_id_to_join,omitempty"`
GroupIDToJoin *uuid.UUID `json:"group_id_to_join,omitempty"`
// GroupMaintainer indicates if the user should be added as a maintainer of the group
GroupMaintainer bool `json:"group_maintainer,omitempty"`
// ProjectIDToJoin is the ID of the project to join when accepting the invitation
ProjectIDToJoin uuid.UUID `json:"project_id_to_join,omitempty"`
ProjectIDToJoin *uuid.UUID `json:"project_id_to_join,omitempty"`
// ProjectRole is the role to assign to the user in the project
ProjectRole authz.Role `json:"project_role,omitempty"`
// ExternalMetadata can be used to store additional information
ExternalMetadata json.RawMessage `json:"external_metadata,omitempty"`
}

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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

much better!

return nil
}

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

// Check if the group exists
gr, err := uc.groupRepo.FindByOrgAndID(ctx, orgUUID, groupID)
gr, err := uc.groupRepo.FindByOrgAndID(ctx, orgUUID, *groupID)
if err != nil {
return fmt.Errorf("error finding group %s: %w", groupID.String(), err)
}

if _, err := uc.groupRepo.AddMemberToGroup(ctx, orgUUID, groupID, userUUID, invitation.Context.GroupMaintainer); err != nil {
if _, err := uc.groupRepo.AddMemberToGroup(ctx, orgUUID, *groupID, userUUID, invitation.Context.GroupMaintainer); err != nil {
if IsErrAlreadyExists(err) {
// User is already a member of the group, nothing to do
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())
Expand All @@ -364,7 +367,7 @@ func (uc *OrgInvitationUseCase) processGroupMembership(ctx context.Context, invi
// Dispatch event to the audit log for group membership addition
uc.auditor.Dispatch(ctx, &events.GroupMemberAdded{
GroupBase: &events.GroupBase{
GroupID: &groupID,
GroupID: groupID,
GroupName: gr.Name,
},
UserID: &userUUID,
Expand All @@ -380,15 +383,15 @@ func (uc *OrgInvitationUseCase) processGroupMembership(ctx context.Context, invi
// processProjectMembership adds a user to a project if the invitation context contains a project to join
func (uc *OrgInvitationUseCase) processProjectMembership(ctx context.Context, invitation *OrgInvitation, orgUUID uuid.UUID, userUUID uuid.UUID) error {
// Skip if there's no group to join in the invitation context
if invitation.Context == nil || invitation.Context.ProjectIDToJoin == uuid.Nil {
if invitation.Context == nil || invitation.Context.ProjectIDToJoin == nil || *invitation.Context.ProjectIDToJoin == uuid.Nil {
return nil
}

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

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

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

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

// Dispatch event to the audit log for project membership addition
uc.auditor.Dispatch(ctx, &events.ProjectMembershipAdded{
ProjectBase: &events.ProjectBase{
ProjectID: &projectID,
ProjectID: projectID,
ProjectName: project.Name,
},
UserID: &userUUID,
Expand Down
78 changes: 68 additions & 10 deletions app/controlplane/pkg/biz/orginvitation_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,7 @@ func (s *OrgInvitationIntegrationTestSuite) TestInvitationWithGroupContext() {
s.T().Run("invitation with group context adds user to group when accepted", func(t *testing.T) {
// Create invitation context with group information
invitationContext := &biz.OrgInvitationContext{
GroupIDToJoin: group.ID,
GroupIDToJoin: &group.ID,
GroupMaintainer: true,
}

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

// Verify context was saved properly
assert.NotNil(t, invite.Context)
assert.Equal(t, group.ID, invite.Context.GroupIDToJoin)
assert.Equal(t, group.ID, *invite.Context.GroupIDToJoin)
assert.Equal(t, true, invite.Context.GroupMaintainer)

// Accept the invitation
Expand Down Expand Up @@ -380,7 +380,7 @@ func (s *OrgInvitationIntegrationTestSuite) TestInvitationWithGroupContext() {

// Create invitation context with group information, but not as maintainer
invitationContext := &biz.OrgInvitationContext{
GroupIDToJoin: group.ID,
GroupIDToJoin: &group.ID,
GroupMaintainer: false,
}

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

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

// Verify context was saved properly
assert.NotNil(t, invite.Context)
assert.Equal(t, project.ID, invite.Context.ProjectIDToJoin)
assert.Equal(t, project.ID, *invite.Context.ProjectIDToJoin)
assert.Equal(t, authz.RoleProjectAdmin, invite.Context.ProjectRole)

// Accept the invitation
Expand Down Expand Up @@ -511,7 +511,7 @@ func (s *OrgInvitationIntegrationTestSuite) TestInvitationWithProjectContext() {

// Create invitation context with project information, but with viewer role
invitationContext := &biz.OrgInvitationContext{
ProjectIDToJoin: project.ID,
ProjectIDToJoin: &project.ID,
ProjectRole: authz.RoleProjectViewer,
}

Expand Down Expand Up @@ -570,9 +570,9 @@ func (s *OrgInvitationIntegrationTestSuite) TestInvitationWithProjectContext() {

// Create invitation context with both group and project information
invitationContext := &biz.OrgInvitationContext{
GroupIDToJoin: group.ID,
GroupIDToJoin: &group.ID,
GroupMaintainer: true,
ProjectIDToJoin: project.ID,
ProjectIDToJoin: &project.ID,
ProjectRole: authz.RoleProjectViewer,
}

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

// Verify context was saved properly
assert.NotNil(t, invite.Context)
assert.Equal(t, group.ID, invite.Context.GroupIDToJoin)
assert.Equal(t, group.ID, *invite.Context.GroupIDToJoin)
assert.True(t, invite.Context.GroupMaintainer)
assert.Equal(t, project.ID, invite.Context.ProjectIDToJoin)
assert.Equal(t, project.ID, *invite.Context.ProjectIDToJoin)
assert.Equal(t, authz.RoleProjectViewer, invite.Context.ProjectRole)

// Accept the invitation
Expand Down Expand Up @@ -645,6 +645,64 @@ func (s *OrgInvitationIntegrationTestSuite) TestInvitationWithProjectContext() {
assert.True(t, foundProjectMember, "The user should be a member of the project")
assert.Equal(t, authz.RoleProjectViewer, projectRole, "The user should have the project contributor role")
})

s.T().Run("invitation with nil UUID on project is rejected", func(t *testing.T) {
// Create a new receiver that isn't a member of any org yet
newReceiverEmail := "combined-receiver@cyberdyne.io"
newReceiver, err := s.User.UpsertByEmail(ctx, newReceiverEmail, nil)
require.NoError(t, err)
require.NotNil(t, newReceiver)

// Create invitation context with nil project ID
invitationContext := &biz.OrgInvitationContext{
ProjectIDToJoin: &uuid.Nil,
}

// Create invitation with combined context
invite, err := s.Repos.OrgInvitationRepo.Create(
ctx,
uuid.MustParse(s.org1.ID),
uuid.MustParse(s.user.ID),
newReceiverEmail,
authz.RoleViewer,
invitationContext,
)
require.NoError(t, err)
require.NotNil(t, invite)

// Accept the invitation and check that there is no error
err = s.OrgInvitation.AcceptPendingInvitations(ctx, newReceiverEmail)
require.NoError(t, err, "Accepting invitation with nil project ID should not fail just skip the project context")
})

s.T().Run("invitation with nil UUID on group is rejected", func(t *testing.T) {
// Create a new receiver that isn't a member of any org yet
newReceiverEmail := "combined-receiver@cyberdyne.io"
newReceiver, err := s.User.UpsertByEmail(ctx, newReceiverEmail, nil)
require.NoError(t, err)
require.NotNil(t, newReceiver)

// Create invitation context with nil group ID
invitationContext := &biz.OrgInvitationContext{
GroupIDToJoin: &uuid.Nil,
}

// Create invitation with combined context
invite, err := s.Repos.OrgInvitationRepo.Create(
ctx,
uuid.MustParse(s.org1.ID),
uuid.MustParse(s.user.ID),
newReceiverEmail,
authz.RoleViewer,
invitationContext,
)
require.NoError(t, err)
require.NotNil(t, invite)

// Accept the invitation and check that there is no error
err = s.OrgInvitation.AcceptPendingInvitations(ctx, newReceiverEmail)
require.NoError(t, err, "Accepting invitation with nil group ID should not fail just skip the project context")
})
}

// Utility struct to hold the test suite
Expand Down
2 changes: 1 addition & 1 deletion app/controlplane/pkg/biz/project.go
Original file line number Diff line number Diff line change
Expand Up @@ -392,7 +392,7 @@ func (uc *ProjectUseCase) handleNonExistingUser(ctx context.Context, orgID, proj

// Create an organization invitation with project context
invitationContext := &OrgInvitationContext{
ProjectIDToJoin: projectID,
ProjectIDToJoin: &projectID,
ProjectRole: opts.Role,
}

Expand Down
4 changes: 2 additions & 2 deletions app/controlplane/pkg/biz/project_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,7 @@ func (s *projectMembersIntegrationTestSuite) TestAddMemberToProject() {

// Verify the invitation has project context
s.NotNil(foundInvitation.Context, "Invitation should have context")
s.Equal(projectID, foundInvitation.Context.ProjectIDToJoin)
s.Equal(projectID, *foundInvitation.Context.ProjectIDToJoin)
s.Equal(authz.RoleProjectViewer, foundInvitation.Context.ProjectRole)
})

Expand Down Expand Up @@ -1807,7 +1807,7 @@ func (s *projectMembersIntegrationTestSuite) TestAddNonExistingMemberToProject()

// Verify the invitation has project context
s.NotNil(foundInvitation.Context, "Invitation should have context")
s.Equal(projectID, foundInvitation.Context.ProjectIDToJoin)
s.Equal(projectID, *foundInvitation.Context.ProjectIDToJoin)
s.Equal(authz.RoleProjectViewer, foundInvitation.Context.ProjectRole)
s.Equal(authz.RoleOrgMember, foundInvitation.Role)
})
Expand Down
15 changes: 8 additions & 7 deletions app/controlplane/pkg/biz/testhelpers/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,13 +82,14 @@ type TestingUseCases struct {
}

type TestingRepos struct {
Membership biz.MembershipRepo
Referrer biz.ReferrerRepo
Workflow biz.WorkflowRepo
WorkflowRunRepo biz.WorkflowRunRepo
AttestationState biz.AttestationStateRepo
OrganizationRepo biz.OrganizationRepo
GroupRepo biz.GroupRepo
Membership biz.MembershipRepo
Referrer biz.ReferrerRepo
Workflow biz.WorkflowRepo
WorkflowRunRepo biz.WorkflowRunRepo
AttestationState biz.AttestationStateRepo
OrganizationRepo biz.OrganizationRepo
GroupRepo biz.GroupRepo
OrgInvitationRepo biz.OrgInvitationRepo
}

type newTestingOpts struct {
Expand Down
15 changes: 8 additions & 7 deletions app/controlplane/pkg/biz/testhelpers/wire_gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading