diff --git a/app/controlplane/internal/server/grpc.go b/app/controlplane/internal/server/grpc.go index 4fac0922a..49d9a96c9 100644 --- a/app/controlplane/internal/server/grpc.go +++ b/app/controlplane/internal/server/grpc.go @@ -193,14 +193,14 @@ func craftMiddleware(opts *Opts) []middleware.Middleware { usercontext.WithAPITokenUsageUpdater(opts.APITokenUseCase, logHelper), // 2.c - Set its user usercontext.WithCurrentUserMiddleware(opts.UserUseCase, logHelper), + // Store all memberships in the context + usercontext.WithCurrentMembershipsMiddleware(opts.MembershipUseCase), selector.Server( // 2.d- Set its organization - usercontext.WithCurrentOrganizationMiddleware(opts.UserUseCase, logHelper), + usercontext.WithCurrentOrganizationMiddleware(opts.UserUseCase, opts.OrganizationUseCase, logHelper), // 3 - Check user/token authorization authzMiddleware.WithAuthzMiddleware(opts.AuthzUseCase, logHelper), ).Match(requireAllButOrganizationOperationsMatcher()).Build(), - // Store all memberships in the context - usercontext.WithCurrentMembershipsMiddleware(opts.MembershipUseCase), // 4 - Make sure the account is fully functional selector.Server( usercontext.CheckUserHasAccess(opts.AuthConfig.AllowList, opts.UserUseCase), @@ -232,7 +232,7 @@ func craftMiddleware(opts *Opts) []middleware.Middleware { // 2.b - Set its API token and Robot Account as alternative to the user usercontext.WithAttestationContextFromAPIToken(opts.APITokenUseCase, opts.OrganizationUseCase, logHelper), // 2.c - Set Attestation context from user token - usercontext.WithAttestationContextFromUser(opts.UserUseCase, logHelper), + usercontext.WithAttestationContextFromUser(opts.UserUseCase, opts.OrganizationUseCase, logHelper), // 2.d - Set its robot account from federated delegation usercontext.WithAttestationContextFromFederatedInfo(opts.OrganizationUseCase, logHelper), // Store all memberships in the context diff --git a/app/controlplane/internal/service/context.go b/app/controlplane/internal/service/context.go index a9dd6aac5..ef8a3c7d8 100644 --- a/app/controlplane/internal/service/context.go +++ b/app/controlplane/internal/service/context.go @@ -127,6 +127,9 @@ func bizOrgToPb(m *biz.Organization) *pb.OrgItem { } func bizUserToPb(u *biz.User) *pb.User { + if u == nil { + return nil + } return &pb.User{Id: u.ID, Email: u.Email, CreatedAt: timestamppb.New(*u.CreatedAt), FirstName: u.FirstName, LastName: u.LastName, UpdatedAt: timestamppb.New(*u.UpdatedAt), diff --git a/app/controlplane/internal/service/orginvitation.go b/app/controlplane/internal/service/orginvitation.go index c05b00829..bbec4a48e 100644 --- a/app/controlplane/internal/service/orginvitation.go +++ b/app/controlplane/internal/service/orginvitation.go @@ -20,6 +20,7 @@ import ( pb "github.com/chainloop-dev/chainloop/app/controlplane/api/controlplane/v1" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/biz" + "github.com/google/uuid" "google.golang.org/protobuf/types/known/timestamppb" ) @@ -38,7 +39,7 @@ func NewOrgInvitationService(uc *biz.OrgInvitationUseCase, opts ...NewOpt) *OrgI } func (s *OrgInvitationService) Create(ctx context.Context, req *pb.OrgInvitationServiceCreateRequest) (*pb.OrgInvitationServiceCreateResponse, error) { - user, err := requireCurrentUser(ctx) + user, _, err := requireCurrentUserOrAPIToken(ctx) if err != nil { return nil, err } @@ -48,8 +49,17 @@ func (s *OrgInvitationService) Create(ctx context.Context, req *pb.OrgInvitation return nil, err } - // Validations and rbac checks are done in the biz layer - i, err := s.useCase.Create(ctx, org.ID, user.ID, req.ReceiverEmail, biz.WithInvitationRole(biz.PbRoleToBiz(req.Role))) + opts := []biz.InvitationCreateOpt{biz.WithInvitationRole(biz.PbRoleToBiz(req.Role))} + if user != nil { + userID, err := uuid.Parse(user.ID) + if err != nil { + return nil, handleUseCaseErr(err, s.log) + } + opts = append(opts, biz.WithSender(userID)) + } + + // Validations are done in the biz layer + i, err := s.useCase.Create(ctx, org.ID, req.ReceiverEmail, opts...) if err != nil { return nil, handleUseCaseErr(err, s.log) } diff --git a/app/controlplane/internal/usercontext/currentorganization_middleware.go b/app/controlplane/internal/usercontext/currentorganization_middleware.go index da1139d4b..754eb427d 100644 --- a/app/controlplane/internal/usercontext/currentorganization_middleware.go +++ b/app/controlplane/internal/usercontext/currentorganization_middleware.go @@ -20,10 +20,12 @@ import ( "encoding/json" "errors" "fmt" + "slices" "time" v1 "github.com/chainloop-dev/chainloop/app/controlplane/api/controlplane/v1" "github.com/chainloop-dev/chainloop/app/controlplane/internal/usercontext/entities" + "github.com/chainloop-dev/chainloop/app/controlplane/pkg/authz" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/biz" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/unmarshal" "github.com/go-kratos/kratos/v2/log" @@ -56,12 +58,13 @@ func WithCurrentMembershipsMiddleware(membershipUC biz.MembershipsRBAC) middlewa } } -func WithCurrentOrganizationMiddleware(userUseCase biz.UserOrgFinder, logger *log.Helper) middleware.Middleware { +func WithCurrentOrganizationMiddleware(userUseCase biz.UserOrgFinder, orgUC *biz.OrganizationUseCase, logger *log.Helper) middleware.Middleware { return func(handler middleware.Handler) middleware.Handler { return func(ctx context.Context, req interface{}) (interface{}, error) { // Get the current user and return if not found, meaning we are probably coming from an API Token u := entities.CurrentUser(ctx) if u == nil { + // For API tokens, the organization is already set in WithCurrentAPITokenAndOrgMiddleware return handler(ctx, req) } @@ -78,7 +81,7 @@ func WithCurrentOrganizationMiddleware(userUseCase biz.UserOrgFinder, logger *lo } if orgName != "" { - ctx, err = setCurrentMembershipFromOrgName(ctx, u, orgName, userUseCase) + ctx, err = setCurrentMembershipFromOrgName(ctx, u, orgName, userUseCase, orgUC) if err != nil { return nil, v1.ErrorUserNotMemberOfOrgErrorNotInOrg("user is not a member of organization %s", orgName) } @@ -140,15 +143,49 @@ func ResetMembershipsCache() { membershipsCache.Purge() } -func setCurrentMembershipFromOrgName(ctx context.Context, user *entities.User, orgName string, userUC biz.UserOrgFinder) (context.Context, error) { +func setCurrentMembershipFromOrgName(ctx context.Context, user *entities.User, orgName string, userUC biz.UserOrgFinder, orgUC *biz.OrganizationUseCase) (context.Context, error) { membership, err := userUC.MembershipInOrg(ctx, user.ID, orgName) - if err != nil { + if err != nil && !biz.IsNotFound(err) { return nil, fmt.Errorf("failed to find membership: %w", err) } - ctx = entities.WithCurrentOrg(ctx, &entities.Org{Name: membership.Org.Name, ID: membership.Org.ID, CreatedAt: membership.CreatedAt}) + var role authz.Role + if membership == nil { + // if not found, check if the user is instance admin + ctx, err = setMembershipIfInstanceAdmin(ctx, orgName, orgUC) + if err != nil { + return nil, err + } + role = authz.RoleInstanceAdmin + } else { + role = membership.Role + ctx = entities.WithCurrentOrg(ctx, &entities.Org{Name: membership.Org.Name, ID: membership.Org.ID, CreatedAt: membership.CreatedAt}) + } + // Set the authorization subject that will be used to check the policies - return WithAuthzSubject(ctx, string(membership.Role)), nil + return WithAuthzSubject(ctx, string(role)), nil +} + +// sets membership to any organization if the user is an instance admin +func setMembershipIfInstanceAdmin(ctx context.Context, orgName string, orgUC *biz.OrganizationUseCase) (context.Context, error) { + // look for user membership with instance admin role + m := entities.CurrentMembership(ctx) + if m != nil { + if slices.ContainsFunc(m.Resources, func(r *entities.ResourceMembership) bool { + return r.Role == authz.RoleInstanceAdmin && r.ResourceType == authz.ResourceTypeInstance + }) { + org, err := orgUC.FindByName(ctx, orgName) + if err != nil { + return nil, fmt.Errorf("failed to find organization: %w", err) + } + ctx = entities.WithCurrentOrg(ctx, &entities.Org{Name: org.Name, ID: org.ID, CreatedAt: org.CreatedAt}) + } + } else { + // if no membership and no instance admin, return error + return nil, errors.New("user membership not found") + } + + return ctx, nil } // Find the current membership of the user and sets it on the context diff --git a/app/controlplane/internal/usercontext/currentorganization_middleware_test.go b/app/controlplane/internal/usercontext/currentorganization_middleware_test.go index 6b30be92b..14ee3bc4c 100644 --- a/app/controlplane/internal/usercontext/currentorganization_middleware_test.go +++ b/app/controlplane/internal/usercontext/currentorganization_middleware_test.go @@ -80,7 +80,7 @@ func TestWithCurrentOrganizationMiddleware(t *testing.T) { usecase.On("CurrentMembership", ctx, wantUser.ID).Maybe().Return(nil, nil) } - m := WithCurrentOrganizationMiddleware(usecase, logger) + m := WithCurrentOrganizationMiddleware(usecase, nil, logger) _, err := m( func(ctx context.Context, _ interface{}) (interface{}, error) { if tc.wantErr { diff --git a/app/controlplane/internal/usercontext/currentuser_middleware.go b/app/controlplane/internal/usercontext/currentuser_middleware.go index cf1975268..b52ae27f4 100644 --- a/app/controlplane/internal/usercontext/currentuser_middleware.go +++ b/app/controlplane/internal/usercontext/currentuser_middleware.go @@ -88,7 +88,7 @@ func setCurrentUser(ctx context.Context, userUC biz.UserOrgFinder, userID string // WithAttestationContextFromUser injects the current user + organization to the context during the attestation process // it leverages the existing middlewares to set the current user and organization // but with a skipping behavior since that's the one required by the attMiddleware multi-selector -func WithAttestationContextFromUser(userUC *biz.UserUseCase, logger *log.Helper) middleware.Middleware { +func WithAttestationContextFromUser(userUC *biz.UserUseCase, orgUC *biz.OrganizationUseCase, logger *log.Helper) middleware.Middleware { return func(handler middleware.Handler) middleware.Handler { return func(ctx context.Context, req interface{}) (interface{}, error) { // If the token is not an user token, we don't need to do anything @@ -114,7 +114,7 @@ func WithAttestationContextFromUser(userUC *biz.UserUseCase, logger *log.Helper) // NOTE: we reuse the existing middlewares to set the current user and organization by wrapping the call // Now we can load the organization using the other middleware we have set return WithCurrentUserMiddleware(userUC, logger)(func(ctx context.Context, req any) (any, error) { - return WithCurrentOrganizationMiddleware(userUC, logger)(func(ctx context.Context, req any) (any, error) { + return WithCurrentOrganizationMiddleware(userUC, orgUC, logger)(func(ctx context.Context, req any) (any, error) { org := entities.CurrentOrg(ctx) if org == nil { return nil, errors.New("organization not found") diff --git a/app/controlplane/pkg/authz/authz.go b/app/controlplane/pkg/authz/authz.go index 25d84e1fc..ab081eaa3 100644 --- a/app/controlplane/pkg/authz/authz.go +++ b/app/controlplane/pkg/authz/authz.go @@ -63,7 +63,6 @@ const ( ResourceAPIToken = "api_token" ResourceProjectMembership = "project_membership" ResourceOrganizationInvitations = "organization_invitations" - ResourceGroupProjects = "group_projects" // Top level instance admin role // this is used to know if an user is a super admin of the chainloop instance @@ -111,6 +110,7 @@ var ( // CAS backend PolicyCASBackendList = &Policy{ResourceCASBackend, ActionList} PolicyCASBackendUpdate = &Policy{ResourceCASBackend, ActionUpdate} + PolicyCASBackendCreate = &Policy{ResourceCASBackend, ActionCreate} // Available integrations PolicyAvailableIntegrationList = &Policy{ResourceAvailableIntegration, ActionList} PolicyAvailableIntegrationRead = &Policy{ResourceAvailableIntegration, ActionRead} @@ -180,7 +180,13 @@ var ( var RolesMap = map[Role][]*Policy{ // Organizations in chainloop might be restricted to instance admins RoleInstanceAdmin: { + // Instance admins can create new organizations PolicyOrganizationCreate, + // Instance admins can invite users to organizations + PolicyOrganizationInvitationsCreate, + // Instance admins can configure CAS Backends in all organizations + PolicyCASBackendList, + PolicyCASBackendCreate, }, RoleOwner: { PolicyOrganizationDelete, @@ -352,6 +358,7 @@ var ServerOperationsMap = map[string][]*Policy{ // CAS Backend listing "/controlplane.v1.CASBackendService/List": {PolicyCASBackendList}, "/controlplane.v1.CASBackendService/Revalidate": {PolicyCASBackendUpdate}, + "/controlplane.v1.CASBackendService/Create": {PolicyCASBackendCreate}, // Available integrations "/controlplane.v1.IntegrationsService/ListAvailable": {PolicyAvailableIntegrationList, PolicyAvailableIntegrationRead}, // Registered integrations @@ -429,6 +436,9 @@ var ServerOperationsMap = map[string][]*Policy{ "/controlplane.v1.APITokenService/List": {PolicyAPITokenList}, "/controlplane.v1.APITokenService/Create": {PolicyAPITokenCreate}, "/controlplane.v1.APITokenService/Revoke": {PolicyAPITokenRevoke}, + + // Org invitations + "/controlplane.v1.OrgInvitationService/Create": {PolicyOrganizationInvitationsCreate}, } // Implements https://pkg.go.dev/entgo.io/ent/schema/field#EnumValues diff --git a/app/controlplane/pkg/authz/middleware/middleware.go b/app/controlplane/pkg/authz/middleware/middleware.go index f7d7d6d2c..a55afb294 100644 --- a/app/controlplane/pkg/authz/middleware/middleware.go +++ b/app/controlplane/pkg/authz/middleware/middleware.go @@ -81,6 +81,7 @@ func checkPolicies(ctx context.Context, subject, apiOperation string, enforcer E } // For users, use role-based enforcement via Casbin + // For tokens, check for specific policies in the database for _, p := range policies { ok, err := enforcer.Enforce(ctx, subject, p) if err != nil { diff --git a/app/controlplane/pkg/biz/group.go b/app/controlplane/pkg/biz/group.go index 7a2f53c2e..3dcf2aced 100644 --- a/app/controlplane/pkg/biz/group.go +++ b/app/controlplane/pkg/biz/group.go @@ -564,7 +564,10 @@ func (uc *GroupUseCase) handleNonExistingUser(ctx context.Context, orgID, groupI } // Create an invitation for the user to join the organization - if _, err := uc.orgInvitationUC.Create(ctx, orgID.String(), opts.RequesterID.String(), opts.UserEmail, WithInvitationRole(authz.RoleOrgContributor), WithInvitationContext(invitationContext)); err != nil { + if _, err := uc.orgInvitationUC.Create(ctx, orgID.String(), opts.UserEmail, + WithSender(opts.RequesterID), + WithInvitationRole(authz.RoleOrgContributor), + WithInvitationContext(invitationContext)); err != nil { return nil, fmt.Errorf("failed to create invitation: %w", err) } diff --git a/app/controlplane/pkg/biz/orginvitation.go b/app/controlplane/pkg/biz/orginvitation.go index ddab056fa..7496aba7a 100644 --- a/app/controlplane/pkg/biz/orginvitation.go +++ b/app/controlplane/pkg/biz/orginvitation.go @@ -71,7 +71,7 @@ type OrgInvitationContext struct { } type OrgInvitationRepo interface { - Create(ctx context.Context, orgID, senderID uuid.UUID, receiverEmail string, role authz.Role, invCtx *OrgInvitationContext) (*OrgInvitation, error) + Create(ctx context.Context, orgID uuid.UUID, senderID *uuid.UUID, receiverEmail string, role authz.Role, invCtx *OrgInvitationContext) (*OrgInvitation, error) FindByID(ctx context.Context, ID uuid.UUID) (*OrgInvitation, error) PendingInvitation(ctx context.Context, orgID uuid.UUID, receiverEmail string) (*OrgInvitation, error) PendingInvitations(ctx context.Context, receiverEmail string) ([]*OrgInvitation, error) @@ -88,8 +88,9 @@ func NewOrgInvitationUseCase(r OrgInvitationRepo, mRepo MembershipRepo, uRepo Us } type invitationCreateOpts struct { - role authz.Role - ctx *OrgInvitationContext + role authz.Role + ctx *OrgInvitationContext + senderID *uuid.UUID } type InvitationCreateOpt func(*invitationCreateOpts) @@ -108,7 +109,13 @@ func WithInvitationContext(ctx *OrgInvitationContext) InvitationCreateOpt { } } -func (uc *OrgInvitationUseCase) Create(ctx context.Context, orgID, senderID, receiverEmail string, createOpts ...InvitationCreateOpt) (*OrgInvitation, error) { +func WithSender(senderID uuid.UUID) InvitationCreateOpt { + return func(o *invitationCreateOpts) { + o.senderID = &senderID + } +} + +func (uc *OrgInvitationUseCase) Create(ctx context.Context, orgID, receiverEmail string, createOpts ...InvitationCreateOpt) (*OrgInvitation, error) { receiverEmail = strings.ToLower(receiverEmail) // 1 - Static Validation @@ -135,29 +142,18 @@ func (uc *OrgInvitationUseCase) Create(ctx context.Context, orgID, senderID, rec return nil, NewErrInvalidUUID(err) } - senderUUID, err := uuid.Parse(senderID) - if err != nil { - return nil, NewErrInvalidUUID(err) - } - // 2 - the sender exists and it's not the same than the receiver of the invitation - sender, err := uc.userRepo.FindByID(ctx, senderUUID) - if err != nil { - return nil, fmt.Errorf("error finding sender %s: %w", senderUUID.String(), err) - } else if sender == nil { - return nil, NewErrNotFound("sender") - } - - if sender.Email == receiverEmail { - return nil, NewErrValidationStr("sender and receiver emails cannot be the same") - } + if opts.senderID != nil { + sender, err := uc.userRepo.FindByID(ctx, *opts.senderID) + if err != nil { + return nil, fmt.Errorf("error finding sender %s: %w", opts.senderID.String(), err) + } else if sender == nil { + return nil, NewErrNotFound("sender") + } - // 3 - Check that the user is a member of the given org - // NOTE: this check is not necessary, as the user is already a member of the org - if membership, err := uc.mRepo.FindByOrgAndUser(ctx, orgUUID, senderUUID); err != nil { - return nil, fmt.Errorf("failed to find memberships: %w", err) - } else if membership == nil { - return nil, NewErrNotFound("user does not have permission to invite to this org") + if sender.Email == receiverEmail { + return nil, NewErrValidationStr("sender and receiver emails cannot be the same") + } } // 4 - The receiver does exist in the org already @@ -165,7 +161,7 @@ func (uc *OrgInvitationUseCase) Create(ctx context.Context, orgID, senderID, rec Email: &receiverEmail, }, pagination.NewDefaultOffsetPaginationOpts()) if err != nil { - return nil, fmt.Errorf("error finding memberships for user %s: %w", senderUUID.String(), err) + return nil, fmt.Errorf("error finding memberships for user %s: %w", receiverEmail, err) } if membershipCount > 0 { @@ -183,7 +179,7 @@ func (uc *OrgInvitationUseCase) Create(ctx context.Context, orgID, senderID, rec } // 6 - Create the invitation - invitation, err := uc.repo.Create(ctx, orgUUID, senderUUID, receiverEmail, opts.role, opts.ctx) + invitation, err := uc.repo.Create(ctx, orgUUID, opts.senderID, receiverEmail, opts.role, opts.ctx) if err != nil { return nil, fmt.Errorf("error creating invitation: %w", err) } diff --git a/app/controlplane/pkg/biz/orginvitation_integration_test.go b/app/controlplane/pkg/biz/orginvitation_integration_test.go index 1d33644b7..d372ea9fc 100644 --- a/app/controlplane/pkg/biz/orginvitation_integration_test.go +++ b/app/controlplane/pkg/biz/orginvitation_integration_test.go @@ -34,12 +34,12 @@ const receiverEmail = "sarah@cyberdyne.io" func (s *OrgInvitationIntegrationTestSuite) TestList() { ctx := context.Background() - inviteOrg1A, err := s.OrgInvitation.Create(ctx, s.org1.ID, s.user.ID, receiverEmail) + inviteOrg1A, err := s.OrgInvitation.Create(ctx, s.org1.ID, receiverEmail, biz.WithSender(uuid.MustParse(s.user.ID))) s.NoError(err) // same org but another user - inviteOrg1B, err := s.OrgInvitation.Create(ctx, s.org1.ID, s.user2.ID, "another-email@cyberdyne.io") + inviteOrg1B, err := s.OrgInvitation.Create(ctx, s.org1.ID, "another-email@cyberdyne.io", biz.WithSender(uuid.MustParse(s.user2.ID))) s.NoError(err) - inviteOrg2A, err := s.OrgInvitation.Create(ctx, s.org2.ID, s.user.ID, receiverEmail) + inviteOrg2A, err := s.OrgInvitation.Create(ctx, s.org2.ID, receiverEmail, biz.WithSender(uuid.MustParse(s.user.ID))) s.NoError(err) testCases := []struct { @@ -75,69 +75,46 @@ func (s *OrgInvitationIntegrationTestSuite) TestList() { func (s *OrgInvitationIntegrationTestSuite) TestCreate() { ctx := context.Background() - s.T().Run("invalid org ID", func(t *testing.T) { - invite, err := s.OrgInvitation.Create(ctx, "deadbeef", s.user.ID, receiverEmail) + s.T().Run("invalid org ID", func(_ *testing.T) { + invite, err := s.OrgInvitation.Create(ctx, "deadbeef", receiverEmail, biz.WithSender(uuid.MustParse(s.user.ID))) s.Error(err) s.True(biz.IsErrInvalidUUID(err)) s.Nil(invite) }) - s.T().Run("invalid user ID", func(t *testing.T) { - invite, err := s.OrgInvitation.Create(ctx, s.org1.ID, "deadbeef", receiverEmail) - s.Error(err) - s.True(biz.IsErrInvalidUUID(err)) - s.Nil(invite) - }) - - s.T().Run("missing receiver email", func(t *testing.T) { - invite, err := s.OrgInvitation.Create(ctx, s.org1.ID, s.user.ID, "") + s.T().Run("missing receiver email", func(_ *testing.T) { + invite, err := s.OrgInvitation.Create(ctx, s.org1.ID, "", biz.WithSender(uuid.MustParse(s.user.ID))) s.Error(err) s.True(biz.IsErrValidation(err)) s.Nil(invite) }) - s.T().Run("receiver email same than sender", func(t *testing.T) { - invite, err := s.OrgInvitation.Create(ctx, s.org1.ID, s.user.ID, s.user.Email) + s.T().Run("receiver email same than sender", func(_ *testing.T) { + invite, err := s.OrgInvitation.Create(ctx, s.org1.ID, s.user.Email, biz.WithSender(uuid.MustParse(s.user.ID))) s.Error(err) s.ErrorContains(err, "sender and receiver emails cannot be the same") s.True(biz.IsErrValidation(err)) s.Nil(invite) }) - s.T().Run("receiver is already a member", func(t *testing.T) { - invite, err := s.OrgInvitation.Create(ctx, s.org1.ID, s.user.ID, s.user2.Email) + s.T().Run("receiver is already a member", func(_ *testing.T) { + invite, err := s.OrgInvitation.Create(ctx, s.org1.ID, s.user2.Email, biz.WithSender(uuid.MustParse(s.user.ID))) s.Error(err) s.ErrorContains(err, "user already exists in the org") s.True(biz.IsErrValidation(err)) s.Nil(invite) }) - s.T().Run("org not found", func(t *testing.T) { - invite, err := s.OrgInvitation.Create(ctx, s.org1.ID, uuid.NewString(), receiverEmail) - s.Error(err) - s.True(biz.IsNotFound(err)) - s.Nil(invite) - }) - - s.T().Run("sender is not member of that org", func(t *testing.T) { - invite, err := s.OrgInvitation.Create(ctx, s.org3.ID, s.user.ID, receiverEmail) - s.Error(err) - s.ErrorContains(err, "user does not have permission to invite to this org") - s.True(biz.IsNotFound(err)) - s.Nil(invite) - }) - - s.T().Run("sender is not member of that org but receiver is", func(t *testing.T) { - invite, err := s.OrgInvitation.Create(ctx, s.org3.ID, s.user.ID, s.user2.Email) + s.T().Run("org not found", func(_ *testing.T) { + invite, err := s.OrgInvitation.Create(ctx, s.org1.ID, receiverEmail, biz.WithSender(uuid.Nil)) s.Error(err) - s.ErrorContains(err, "user does not have permission to invite to this org") s.True(biz.IsNotFound(err)) s.Nil(invite) }) - s.T().Run("can create invites for org1 and 2", func(t *testing.T) { + s.T().Run("can create invites for org1 and 2", func(_ *testing.T) { for _, org := range []*biz.Organization{s.org1, s.org2} { - invite, err := s.OrgInvitation.Create(ctx, org.ID, s.user.ID, receiverEmail) + invite, err := s.OrgInvitation.Create(ctx, org.ID, receiverEmail, biz.WithSender(uuid.MustParse(s.user.ID))) s.NoError(err) s.Equal(org, invite.Org) s.Equal(s.user, invite.Sender) @@ -147,37 +124,37 @@ func (s *OrgInvitationIntegrationTestSuite) TestCreate() { } }) - s.T().Run("but can't create if there is one pending", func(t *testing.T) { - invite, err := s.OrgInvitation.Create(ctx, s.org1.ID, s.user.ID, receiverEmail) + s.T().Run("but can't create if there is one pending", func(_ *testing.T) { + invite, err := s.OrgInvitation.Create(ctx, s.org1.ID, receiverEmail, biz.WithSender(uuid.MustParse(s.user.ID))) s.Error(err) s.ErrorContains(err, "already exists") s.True(biz.IsErrValidation(err)) s.Nil(invite) }) - s.T().Run("but it can if it's another email", func(t *testing.T) { - invite, err := s.OrgInvitation.Create(ctx, s.org1.ID, s.user.ID, "anotheremail@cyberdyne.io") + s.T().Run("but it can if it's another email", func(_ *testing.T) { + invite, err := s.OrgInvitation.Create(ctx, s.org1.ID, "anotheremail@cyberdyne.io", biz.WithSender(uuid.MustParse(s.user.ID))) s.Equal("anotheremail@cyberdyne.io", invite.ReceiverEmail) s.Equal(s.org1, invite.Org) s.NoError(err) }) - s.T().Run("the default role is viewer", func(t *testing.T) { - invite, err := s.OrgInvitation.Create(ctx, s.org1.ID, s.user.ID, "viewer@cyberdyne.io") + s.T().Run("the default role is viewer", func(_ *testing.T) { + invite, err := s.OrgInvitation.Create(ctx, s.org1.ID, "viewer@cyberdyne.io", biz.WithSender(uuid.MustParse(s.user.ID))) s.NoError(err) s.Equal(authz.RoleViewer, invite.Role) }) - s.T().Run("but can have other roles", func(t *testing.T) { + s.T().Run("but can have other roles", func(_ *testing.T) { for _, r := range []authz.Role{authz.RoleOwner, authz.RoleAdmin, authz.RoleViewer} { - invite, err := s.OrgInvitation.Create(ctx, s.org1.ID, s.user.ID, fmt.Sprintf("%s@cyberdyne.io", r), biz.WithInvitationRole(r)) + invite, err := s.OrgInvitation.Create(ctx, s.org1.ID, fmt.Sprintf("%s@cyberdyne.io", r), biz.WithInvitationRole(r), biz.WithSender(uuid.MustParse(s.user.ID))) s.NoError(err) s.Equal(r, invite.Role) } }) s.Run("and the email address is downcased", func() { - invite, err := s.OrgInvitation.Create(ctx, s.org1.ID, s.user.ID, "WasCamelCase@cyberdyne.io") + invite, err := s.OrgInvitation.Create(ctx, s.org1.ID, "WasCamelCase@cyberdyne.io", biz.WithSender(uuid.MustParse(s.user.ID))) s.NoError(err) s.Equal("wascamelcase@cyberdyne.io", invite.ReceiverEmail) }) @@ -188,12 +165,12 @@ func (s *OrgInvitationIntegrationTestSuite) TestAcceptPendingInvitations() { receiver, err := s.User.UpsertByEmail(ctx, receiverEmail, nil) require.NoError(s.T(), err) - s.T().Run("user doesn't exist", func(t *testing.T) { + s.T().Run("user doesn't exist", func(_ *testing.T) { err := s.OrgInvitation.AcceptPendingInvitations(ctx, "non-existant@cyberdyne.io") s.ErrorContains(err, "not found") }) - s.T().Run("no invites for user", func(t *testing.T) { + s.T().Run("no invites for user", func(_ *testing.T) { err = s.OrgInvitation.AcceptPendingInvitations(ctx, receiverEmail) s.NoError(err) @@ -202,8 +179,8 @@ func (s *OrgInvitationIntegrationTestSuite) TestAcceptPendingInvitations() { s.Len(memberships, 0) }) - s.T().Run("user is invited to org 1 as viewer", func(t *testing.T) { - invite, err := s.OrgInvitation.Create(ctx, s.org1.ID, s.user.ID, receiverEmail) + s.T().Run("user is invited to org 1 as viewer", func(_ *testing.T) { + invite, err := s.OrgInvitation.Create(ctx, s.org1.ID, receiverEmail, biz.WithSender(uuid.MustParse(s.user.ID))) require.NoError(s.T(), err) err = s.OrgInvitation.AcceptPendingInvitations(ctx, receiverEmail) s.NoError(err) @@ -221,13 +198,13 @@ func (s *OrgInvitationIntegrationTestSuite) TestAcceptPendingInvitations() { s.Equal(biz.OrgInvitationStatusAccepted, invite.Status) }) - s.T().Run("or take any other role", func(t *testing.T) { + s.T().Run("or take any other role", func(_ *testing.T) { for i, r := range []authz.Role{authz.RoleOwner, authz.RoleAdmin, authz.RoleViewer} { // Create user and invite it with different roles receiverEmail := fmt.Sprintf("user%d@cyberdyne.io", i) receiver, err := s.User.UpsertByEmail(ctx, receiverEmail, nil) s.NoError(err) - invite, err := s.OrgInvitation.Create(ctx, s.org1.ID, s.user.ID, receiverEmail, biz.WithInvitationRole(r)) + invite, err := s.OrgInvitation.Create(ctx, s.org1.ID, receiverEmail, biz.WithInvitationRole(r), biz.WithSender(uuid.MustParse(s.user.ID))) s.NoError(err) s.Equal(r, invite.Role) // accept the invite and make sure the new membership has the role @@ -244,28 +221,28 @@ func (s *OrgInvitationIntegrationTestSuite) TestAcceptPendingInvitations() { func (s *OrgInvitationIntegrationTestSuite) TestRevoke() { ctx := context.Background() - s.T().Run("invalid ID", func(t *testing.T) { + s.T().Run("invalid ID", func(_ *testing.T) { err := s.OrgInvitation.Revoke(ctx, s.org1.ID, "deadbeef") s.Error(err) s.True(biz.IsErrInvalidUUID(err)) }) - s.T().Run("invitation not found", func(t *testing.T) { + s.T().Run("invitation not found", func(_ *testing.T) { err := s.OrgInvitation.Revoke(ctx, s.org1.ID, uuid.NewString()) s.Error(err) s.True(biz.IsNotFound(err)) }) - s.T().Run("invitation in another org", func(t *testing.T) { - _, err := s.OrgInvitation.Create(ctx, s.org2.ID, s.user.ID, receiverEmail) + s.T().Run("invitation in another org", func(_ *testing.T) { + _, err := s.OrgInvitation.Create(ctx, s.org2.ID, receiverEmail, biz.WithSender(uuid.MustParse(s.user.ID))) s.NoError(err) err = s.OrgInvitation.Revoke(ctx, s.org1.ID, uuid.NewString()) s.Error(err) s.True(biz.IsNotFound(err)) }) - s.T().Run("invitation not in pending state", func(t *testing.T) { - invite, err := s.OrgInvitation.Create(ctx, s.org1.ID, s.user.ID, receiverEmail) + s.T().Run("invitation not in pending state", func(_ *testing.T) { + invite, err := s.OrgInvitation.Create(ctx, s.org1.ID, receiverEmail, biz.WithSender(uuid.MustParse(s.user.ID))) require.NoError(s.T(), err) err = s.OrgInvitation.AcceptInvitation(ctx, invite.ID.String()) require.NoError(s.T(), err) @@ -277,8 +254,8 @@ func (s *OrgInvitationIntegrationTestSuite) TestRevoke() { s.True(biz.IsErrValidation(err)) }) - s.T().Run("happy path", func(t *testing.T) { - invite, err := s.OrgInvitation.Create(ctx, s.org1.ID, s.user.ID, receiverEmail) + s.T().Run("happy path", func(_ *testing.T) { + invite, err := s.OrgInvitation.Create(ctx, s.org1.ID, receiverEmail, biz.WithSender(uuid.MustParse(s.user.ID))) require.NoError(s.T(), err) err = s.OrgInvitation.Revoke(ctx, s.org1.ID, invite.ID.String()) s.NoError(err) @@ -322,10 +299,10 @@ func (s *OrgInvitationIntegrationTestSuite) TestInvitationWithGroupContext() { invite, err := s.OrgInvitation.Create( ctx, s.org1.ID, - s.user.ID, receiverForGroupEmail, biz.WithInvitationRole(authz.RoleViewer), biz.WithInvitationContext(invitationContext), + biz.WithSender(uuid.MustParse(s.user.ID)), ) require.NoError(t, err) require.NotNil(t, invite) @@ -388,10 +365,10 @@ func (s *OrgInvitationIntegrationTestSuite) TestInvitationWithGroupContext() { invite, err := s.OrgInvitation.Create( ctx, s.org1.ID, - s.user.ID, anotherReceiverEmail, biz.WithInvitationRole(authz.RoleViewer), biz.WithInvitationContext(invitationContext), + biz.WithSender(uuid.MustParse(s.user.ID)), ) require.NoError(t, err) require.NotNil(t, invite) @@ -455,9 +432,9 @@ func (s *OrgInvitationIntegrationTestSuite) TestInvitationWithProjectContext() { invite, err := s.OrgInvitation.Create( ctx, s.org1.ID, - s.user.ID, receiverForProjectEmail, biz.WithInvitationRole(authz.RoleViewer), + biz.WithSender(uuid.MustParse(s.user.ID)), biz.WithInvitationContext(invitationContext), ) require.NoError(t, err) @@ -519,8 +496,8 @@ func (s *OrgInvitationIntegrationTestSuite) TestInvitationWithProjectContext() { invite, err := s.OrgInvitation.Create( ctx, s.org1.ID, - s.user.ID, anotherReceiverEmail, + biz.WithSender(uuid.MustParse(s.user.ID)), biz.WithInvitationRole(authz.RoleViewer), biz.WithInvitationContext(invitationContext), ) @@ -580,8 +557,8 @@ func (s *OrgInvitationIntegrationTestSuite) TestInvitationWithProjectContext() { invite, err := s.OrgInvitation.Create( ctx, s.org1.ID, - s.user.ID, combinedReceiverEmail, + biz.WithSender(uuid.MustParse(s.user.ID)), biz.WithInvitationRole(authz.RoleViewer), biz.WithInvitationContext(invitationContext), ) @@ -662,7 +639,7 @@ func (s *OrgInvitationIntegrationTestSuite) TestInvitationWithProjectContext() { invite, err := s.Repos.OrgInvitationRepo.Create( ctx, uuid.MustParse(s.org1.ID), - uuid.MustParse(s.user.ID), + biz.ToPtr(uuid.MustParse(s.user.ID)), newReceiverEmail, authz.RoleViewer, invitationContext, @@ -691,7 +668,7 @@ func (s *OrgInvitationIntegrationTestSuite) TestInvitationWithProjectContext() { invite, err := s.Repos.OrgInvitationRepo.Create( ctx, uuid.MustParse(s.org1.ID), - uuid.MustParse(s.user.ID), + biz.ToPtr(uuid.MustParse(s.user.ID)), newReceiverEmail, authz.RoleViewer, invitationContext, diff --git a/app/controlplane/pkg/biz/project.go b/app/controlplane/pkg/biz/project.go index bc29b2077..b1c29fc09 100644 --- a/app/controlplane/pkg/biz/project.go +++ b/app/controlplane/pkg/biz/project.go @@ -410,7 +410,10 @@ func (uc *ProjectUseCase) handleNonExistingUser(ctx context.Context, orgID, proj } // Create an invitation for the user to join the organization with project context - if _, err := uc.orgInvitationUC.Create(ctx, orgID.String(), opts.RequesterID.String(), opts.UserEmail, WithInvitationRole(authz.RoleOrgMember), WithInvitationContext(invitationContext)); err != nil { + if _, err := uc.orgInvitationUC.Create(ctx, orgID.String(), opts.UserEmail, + WithSender(opts.RequesterID), + WithInvitationRole(authz.RoleOrgMember), + WithInvitationContext(invitationContext)); err != nil { return nil, fmt.Errorf("failed to create invitation: %w", err) } diff --git a/app/controlplane/pkg/biz/project_integration_test.go b/app/controlplane/pkg/biz/project_integration_test.go index 3503023fa..5d7e28493 100644 --- a/app/controlplane/pkg/biz/project_integration_test.go +++ b/app/controlplane/pkg/biz/project_integration_test.go @@ -1816,7 +1816,7 @@ func (s *projectMembersIntegrationTestSuite) TestAddNonExistingMemberToProject() alreadyInvitedEmail := "already-invited@example.com" // First create an invitation - _, err := s.OrgInvitation.Create(ctx, s.org.ID, s.user.ID, alreadyInvitedEmail) + _, err := s.OrgInvitation.Create(ctx, s.org.ID, alreadyInvitedEmail, biz.WithSender(uuid.MustParse(s.user.ID))) s.NoError(err) // Now try to add the same email to a project diff --git a/app/controlplane/pkg/data/ent/migrate/migrations/20260204113827.sql b/app/controlplane/pkg/data/ent/migrate/migrations/20260204113827.sql new file mode 100644 index 000000000..e473b37db --- /dev/null +++ b/app/controlplane/pkg/data/ent/migrate/migrations/20260204113827.sql @@ -0,0 +1,2 @@ +-- Modify "org_invitations" table +ALTER TABLE "org_invitations" ALTER COLUMN "sender_id" DROP NOT NULL; diff --git a/app/controlplane/pkg/data/ent/migrate/migrations/atlas.sum b/app/controlplane/pkg/data/ent/migrate/migrations/atlas.sum index 80dc1e7eb..04f33f296 100644 --- a/app/controlplane/pkg/data/ent/migrate/migrations/atlas.sum +++ b/app/controlplane/pkg/data/ent/migrate/migrations/atlas.sum @@ -1,4 +1,4 @@ -h1:Xgi8kkxc2dgEdAYSi1CmSXcczC+eiY2CIURHYEjCH3c= +h1:HfuhzadCjBbwi+tW82Z8bEYHpv2onjcYBfwov7Z/9II= 20230706165452_init-schema.sql h1:VvqbNFEQnCvUVyj2iDYVQQxDM0+sSXqocpt/5H64k8M= 20230710111950-cas-backend.sql h1:A8iBuSzZIEbdsv9ipBtscZQuaBp3V5/VMw7eZH6GX+g= 20230712094107-cas-backends-workflow-runs.sql h1:a5rzxpVGyd56nLRSsKrmCFc9sebg65RWzLghKHh5xvI= @@ -123,3 +123,4 @@ h1:Xgi8kkxc2dgEdAYSi1CmSXcczC+eiY2CIURHYEjCH3c= 20251212115308.sql h1:CmwHDA9X91++2dnThzk57++5sBDAGw2IQnHzO3/bRlk= 20251217164302.sql h1:OL3OCqWsMtv06RfIlQNcdLMbt4Tz91Lijpbkxqwt7zM= 20260112115927.sql h1:/RKhzT5dRphgeBitxBfo3a3fqLVgvmVZxxqe9fH8lkg= +20260204113827.sql h1:rlJNf8QRfqOfDHf2GUi+59Rgv2BkSbMTPuMalPsMkZg= diff --git a/app/controlplane/pkg/data/ent/migrate/schema.go b/app/controlplane/pkg/data/ent/migrate/schema.go index 14fff21d2..bd6167e14 100644 --- a/app/controlplane/pkg/data/ent/migrate/schema.go +++ b/app/controlplane/pkg/data/ent/migrate/schema.go @@ -399,7 +399,7 @@ var ( {Name: "role", Type: field.TypeEnum, Nullable: true, Enums: []string{"role:instance:admin", "role:org:owner", "role:org:admin", "role:org:viewer", "role:org:member", "role:org:contributor", "role:project:admin", "role:project:viewer", "role:group:maintainer", "role:product:admin", "role:product:viewer"}}, {Name: "context", Type: field.TypeJSON, Nullable: true}, {Name: "organization_id", Type: field.TypeUUID}, - {Name: "sender_id", Type: field.TypeUUID}, + {Name: "sender_id", Type: field.TypeUUID, Nullable: true}, } // OrgInvitationsTable holds the schema information for the "org_invitations" table. OrgInvitationsTable = &schema.Table{ diff --git a/app/controlplane/pkg/data/ent/mutation.go b/app/controlplane/pkg/data/ent/mutation.go index f51b8dc01..aa2ed5599 100644 --- a/app/controlplane/pkg/data/ent/mutation.go +++ b/app/controlplane/pkg/data/ent/mutation.go @@ -8192,9 +8192,22 @@ func (m *OrgInvitationMutation) OldSenderID(ctx context.Context) (v uuid.UUID, e return oldValue.SenderID, nil } +// ClearSenderID clears the value of the "sender_id" field. +func (m *OrgInvitationMutation) ClearSenderID() { + m.sender = nil + m.clearedFields[orginvitation.FieldSenderID] = struct{}{} +} + +// SenderIDCleared returns if the "sender_id" field was cleared in this mutation. +func (m *OrgInvitationMutation) SenderIDCleared() bool { + _, ok := m.clearedFields[orginvitation.FieldSenderID] + return ok +} + // ResetSenderID resets all changes to the "sender_id" field. func (m *OrgInvitationMutation) ResetSenderID() { m.sender = nil + delete(m.clearedFields, orginvitation.FieldSenderID) } // SetRole sets the "role" field. @@ -8330,7 +8343,7 @@ func (m *OrgInvitationMutation) ClearSender() { // SenderCleared reports if the "sender" edge to the User entity was cleared. func (m *OrgInvitationMutation) SenderCleared() bool { - return m.clearedsender + return m.SenderIDCleared() || m.clearedsender } // SenderIDs returns the "sender" edge IDs in the mutation. @@ -8555,6 +8568,9 @@ func (m *OrgInvitationMutation) ClearedFields() []string { if m.FieldCleared(orginvitation.FieldDeletedAt) { fields = append(fields, orginvitation.FieldDeletedAt) } + if m.FieldCleared(orginvitation.FieldSenderID) { + fields = append(fields, orginvitation.FieldSenderID) + } if m.FieldCleared(orginvitation.FieldRole) { fields = append(fields, orginvitation.FieldRole) } @@ -8578,6 +8594,9 @@ func (m *OrgInvitationMutation) ClearField(name string) error { case orginvitation.FieldDeletedAt: m.ClearDeletedAt() return nil + case orginvitation.FieldSenderID: + m.ClearSenderID() + return nil case orginvitation.FieldRole: m.ClearRole() return nil diff --git a/app/controlplane/pkg/data/ent/orginvitation/where.go b/app/controlplane/pkg/data/ent/orginvitation/where.go index 99da61cda..4a4dcbc2a 100644 --- a/app/controlplane/pkg/data/ent/orginvitation/where.go +++ b/app/controlplane/pkg/data/ent/orginvitation/where.go @@ -308,6 +308,16 @@ func SenderIDNotIn(vs ...uuid.UUID) predicate.OrgInvitation { return predicate.OrgInvitation(sql.FieldNotIn(FieldSenderID, vs...)) } +// SenderIDIsNil applies the IsNil predicate on the "sender_id" field. +func SenderIDIsNil() predicate.OrgInvitation { + return predicate.OrgInvitation(sql.FieldIsNull(FieldSenderID)) +} + +// SenderIDNotNil applies the NotNil predicate on the "sender_id" field. +func SenderIDNotNil() predicate.OrgInvitation { + return predicate.OrgInvitation(sql.FieldNotNull(FieldSenderID)) +} + // RoleEQ applies the EQ predicate on the "role" field. func RoleEQ(v authz.Role) predicate.OrgInvitation { vc := v diff --git a/app/controlplane/pkg/data/ent/orginvitation_create.go b/app/controlplane/pkg/data/ent/orginvitation_create.go index b7fb77850..10d4a93c1 100644 --- a/app/controlplane/pkg/data/ent/orginvitation_create.go +++ b/app/controlplane/pkg/data/ent/orginvitation_create.go @@ -88,6 +88,14 @@ func (_c *OrgInvitationCreate) SetSenderID(v uuid.UUID) *OrgInvitationCreate { return _c } +// SetNillableSenderID sets the "sender_id" field if the given value is not nil. +func (_c *OrgInvitationCreate) SetNillableSenderID(v *uuid.UUID) *OrgInvitationCreate { + if v != nil { + _c.SetSenderID(*v) + } + return _c +} + // SetRole sets the "role" field. func (_c *OrgInvitationCreate) SetRole(v authz.Role) *OrgInvitationCreate { _c.mutation.SetRole(v) @@ -208,9 +216,6 @@ func (_c *OrgInvitationCreate) check() error { if _, ok := _c.mutation.OrganizationID(); !ok { return &ValidationError{Name: "organization_id", err: errors.New(`ent: missing required field "OrgInvitation.organization_id"`)} } - if _, ok := _c.mutation.SenderID(); !ok { - return &ValidationError{Name: "sender_id", err: errors.New(`ent: missing required field "OrgInvitation.sender_id"`)} - } if v, ok := _c.mutation.Role(); ok { if err := orginvitation.RoleValidator(v); err != nil { return &ValidationError{Name: "role", err: fmt.Errorf(`ent: validator failed for field "OrgInvitation.role": %w`, err)} @@ -219,9 +224,6 @@ func (_c *OrgInvitationCreate) check() error { if len(_c.mutation.OrganizationIDs()) == 0 { return &ValidationError{Name: "organization", err: errors.New(`ent: missing required edge "OrgInvitation.organization"`)} } - if len(_c.mutation.SenderIDs()) == 0 { - return &ValidationError{Name: "sender", err: errors.New(`ent: missing required edge "OrgInvitation.sender"`)} - } return nil } @@ -422,6 +424,12 @@ func (u *OrgInvitationUpsert) UpdateSenderID() *OrgInvitationUpsert { return u } +// ClearSenderID clears the value of the "sender_id" field. +func (u *OrgInvitationUpsert) ClearSenderID() *OrgInvitationUpsert { + u.SetNull(orginvitation.FieldSenderID) + return u +} + // SetRole sets the "role" field. func (u *OrgInvitationUpsert) SetRole(v authz.Role) *OrgInvitationUpsert { u.Set(orginvitation.FieldRole, v) @@ -575,6 +583,13 @@ func (u *OrgInvitationUpsertOne) UpdateSenderID() *OrgInvitationUpsertOne { }) } +// ClearSenderID clears the value of the "sender_id" field. +func (u *OrgInvitationUpsertOne) ClearSenderID() *OrgInvitationUpsertOne { + return u.Update(func(s *OrgInvitationUpsert) { + s.ClearSenderID() + }) +} + // SetRole sets the "role" field. func (u *OrgInvitationUpsertOne) SetRole(v authz.Role) *OrgInvitationUpsertOne { return u.Update(func(s *OrgInvitationUpsert) { @@ -901,6 +916,13 @@ func (u *OrgInvitationUpsertBulk) UpdateSenderID() *OrgInvitationUpsertBulk { }) } +// ClearSenderID clears the value of the "sender_id" field. +func (u *OrgInvitationUpsertBulk) ClearSenderID() *OrgInvitationUpsertBulk { + return u.Update(func(s *OrgInvitationUpsert) { + s.ClearSenderID() + }) +} + // SetRole sets the "role" field. func (u *OrgInvitationUpsertBulk) SetRole(v authz.Role) *OrgInvitationUpsertBulk { return u.Update(func(s *OrgInvitationUpsert) { diff --git a/app/controlplane/pkg/data/ent/orginvitation_update.go b/app/controlplane/pkg/data/ent/orginvitation_update.go index 5bee3b482..1d3f3dcbe 100644 --- a/app/controlplane/pkg/data/ent/orginvitation_update.go +++ b/app/controlplane/pkg/data/ent/orginvitation_update.go @@ -96,6 +96,12 @@ func (_u *OrgInvitationUpdate) SetNillableSenderID(v *uuid.UUID) *OrgInvitationU return _u } +// ClearSenderID clears the value of the "sender_id" field. +func (_u *OrgInvitationUpdate) ClearSenderID() *OrgInvitationUpdate { + _u.mutation.ClearSenderID() + return _u +} + // SetRole sets the "role" field. func (_u *OrgInvitationUpdate) SetRole(v authz.Role) *OrgInvitationUpdate { _u.mutation.SetRole(v) @@ -205,9 +211,6 @@ func (_u *OrgInvitationUpdate) check() error { if _u.mutation.OrganizationCleared() && len(_u.mutation.OrganizationIDs()) > 0 { return errors.New(`ent: clearing a required unique edge "OrgInvitation.organization"`) } - if _u.mutation.SenderCleared() && len(_u.mutation.SenderIDs()) > 0 { - return errors.New(`ent: clearing a required unique edge "OrgInvitation.sender"`) - } return nil } @@ -392,6 +395,12 @@ func (_u *OrgInvitationUpdateOne) SetNillableSenderID(v *uuid.UUID) *OrgInvitati return _u } +// ClearSenderID clears the value of the "sender_id" field. +func (_u *OrgInvitationUpdateOne) ClearSenderID() *OrgInvitationUpdateOne { + _u.mutation.ClearSenderID() + return _u +} + // SetRole sets the "role" field. func (_u *OrgInvitationUpdateOne) SetRole(v authz.Role) *OrgInvitationUpdateOne { _u.mutation.SetRole(v) @@ -514,9 +523,6 @@ func (_u *OrgInvitationUpdateOne) check() error { if _u.mutation.OrganizationCleared() && len(_u.mutation.OrganizationIDs()) > 0 { return errors.New(`ent: clearing a required unique edge "OrgInvitation.organization"`) } - if _u.mutation.SenderCleared() && len(_u.mutation.SenderIDs()) > 0 { - return errors.New(`ent: clearing a required unique edge "OrgInvitation.sender"`) - } return nil } diff --git a/app/controlplane/pkg/data/ent/schema/orginvitation.go b/app/controlplane/pkg/data/ent/schema/orginvitation.go index dc24fd329..0b421fa9b 100644 --- a/app/controlplane/pkg/data/ent/schema/orginvitation.go +++ b/app/controlplane/pkg/data/ent/schema/orginvitation.go @@ -46,7 +46,7 @@ func (OrgInvitation) Fields() []ent.Field { // edge fields to be able to access to them directly field.UUID("organization_id", uuid.UUID{}), - field.UUID("sender_id", uuid.UUID{}), + field.UUID("sender_id", uuid.UUID{}).Optional(), // Role that will be assigned to the user when they accept the invitation field.Enum("role").GoType(authz.Role("")).Optional(), // Context is a JSON field that can be used to store additional information @@ -57,6 +57,6 @@ func (OrgInvitation) Fields() []ent.Field { func (OrgInvitation) Edges() []ent.Edge { return []ent.Edge{ edge.To("organization", Organization.Type).Unique().Required().Field("organization_id").Annotations(entsql.Annotation{OnDelete: entsql.Cascade}), - edge.To("sender", User.Type).Unique().Required().Field("sender_id").Annotations(entsql.Annotation{OnDelete: entsql.Cascade}), + edge.To("sender", User.Type).Unique().Field("sender_id").Annotations(entsql.Annotation{OnDelete: entsql.Cascade}), } } diff --git a/app/controlplane/pkg/data/orginvitation.go b/app/controlplane/pkg/data/orginvitation.go index f7823122a..51f221d4c 100644 --- a/app/controlplane/pkg/data/orginvitation.go +++ b/app/controlplane/pkg/data/orginvitation.go @@ -40,15 +40,18 @@ func NewOrgInvitation(data *Data, logger log.Logger) biz.OrgInvitationRepo { } } -func (r *OrgInvitation) Create(ctx context.Context, orgID, senderID uuid.UUID, receiverEmail string, role authz.Role, invCtx *biz.OrgInvitationContext) (*biz.OrgInvitation, error) { - invite, err := r.data.DB.OrgInvitation.Create(). +func (r *OrgInvitation) Create(ctx context.Context, orgID uuid.UUID, senderID *uuid.UUID, receiverEmail string, role authz.Role, invCtx *biz.OrgInvitationContext) (*biz.OrgInvitation, error) { + update := r.data.DB.OrgInvitation.Create(). SetOrganizationID(orgID). - SetSenderID(senderID). SetRole(role). SetReceiverEmail(receiverEmail). - SetNillableContext(invCtx). - Save(ctx) + SetNillableContext(invCtx) + if senderID != nil { + update.SetSenderID(*senderID) + } + + invite, err := update.Save(ctx) if err != nil { return nil, err }