496 lines
18 KiB
Go
496 lines
18 KiB
Go
// monorepo/cloud/maplefile-backend/internal/maplefile/repo/collection/share.go
|
|
package collection
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/gocql/gocql"
|
|
dom_collection "codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/internal/domain/collection"
|
|
"codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/pkg/validation"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
func (impl *collectionRepositoryImpl) AddMember(ctx context.Context, collectionID gocql.UUID, membership *dom_collection.CollectionMembership) error {
|
|
if membership == nil {
|
|
return fmt.Errorf("membership cannot be nil")
|
|
}
|
|
|
|
impl.Logger.Info("starting add member process",
|
|
zap.String("collection_id", collectionID.String()),
|
|
zap.String("recipient_id", membership.RecipientID.String()),
|
|
zap.String("recipient_email", validation.MaskEmail(membership.RecipientEmail)),
|
|
zap.String("permission_level", membership.PermissionLevel))
|
|
|
|
// Validate membership data with enhanced checks
|
|
if !impl.isValidUUID(membership.RecipientID) {
|
|
return fmt.Errorf("invalid recipient ID")
|
|
}
|
|
if membership.RecipientEmail == "" {
|
|
return fmt.Errorf("recipient email is required")
|
|
}
|
|
if membership.PermissionLevel == "" {
|
|
membership.PermissionLevel = dom_collection.CollectionPermissionReadOnly
|
|
}
|
|
|
|
// CRITICAL: Validate encrypted collection key for shared members
|
|
if len(membership.EncryptedCollectionKey) == 0 {
|
|
impl.Logger.Error("CRITICAL: Attempt to add member without encrypted collection key",
|
|
zap.String("collection_id", collectionID.String()),
|
|
zap.String("recipient_id", membership.RecipientID.String()),
|
|
zap.String("recipient_email", validation.MaskEmail(membership.RecipientEmail)),
|
|
zap.Int("encrypted_key_length", len(membership.EncryptedCollectionKey)))
|
|
return fmt.Errorf("encrypted collection key is required for shared members")
|
|
}
|
|
|
|
// Additional validation: ensure the encrypted key is reasonable size
|
|
if len(membership.EncryptedCollectionKey) < 32 {
|
|
impl.Logger.Error("encrypted collection key appears too short",
|
|
zap.String("collection_id", collectionID.String()),
|
|
zap.String("recipient_id", membership.RecipientID.String()),
|
|
zap.Int("encrypted_key_length", len(membership.EncryptedCollectionKey)))
|
|
return fmt.Errorf("encrypted collection key appears invalid (got %d bytes, expected at least 32)", len(membership.EncryptedCollectionKey))
|
|
}
|
|
|
|
impl.Logger.Info("validated encrypted collection key for new member",
|
|
zap.String("collection_id", collectionID.String()),
|
|
zap.String("recipient_id", membership.RecipientID.String()),
|
|
zap.Int("encrypted_key_length", len(membership.EncryptedCollectionKey)))
|
|
|
|
// Load collection
|
|
collection, err := impl.Get(ctx, collectionID)
|
|
if err != nil {
|
|
impl.Logger.Error("failed to get collection for member addition",
|
|
zap.String("collection_id", collectionID.String()),
|
|
zap.Error(err))
|
|
return fmt.Errorf("failed to get collection: %w", err)
|
|
}
|
|
|
|
if collection == nil {
|
|
return fmt.Errorf("collection not found")
|
|
}
|
|
|
|
impl.Logger.Info("loaded collection for member addition",
|
|
zap.String("collection_id", collection.ID.String()),
|
|
zap.String("collection_state", collection.State),
|
|
zap.Int("existing_members", len(collection.Members)))
|
|
|
|
// Ensure member has an ID BEFORE adding to collection
|
|
if !impl.isValidUUID(membership.ID) {
|
|
membership.ID = gocql.TimeUUID()
|
|
impl.Logger.Debug("generated new member ID", zap.String("member_id", membership.ID.String()))
|
|
}
|
|
|
|
// Set creation time if not set
|
|
if membership.CreatedAt.IsZero() {
|
|
membership.CreatedAt = time.Now()
|
|
}
|
|
|
|
// Set collection ID (ensure it matches)
|
|
membership.CollectionID = collectionID
|
|
|
|
// Check if member already exists and update or add
|
|
memberExists := false
|
|
for i, existingMember := range collection.Members {
|
|
if existingMember.RecipientID == membership.RecipientID {
|
|
impl.Logger.Info("updating existing collection member",
|
|
zap.String("collection_id", collectionID.String()),
|
|
zap.String("recipient_id", membership.RecipientID.String()),
|
|
zap.String("old_permission", existingMember.PermissionLevel),
|
|
zap.String("new_permission", membership.PermissionLevel))
|
|
|
|
// IMPORTANT: Preserve the existing member ID to avoid creating a new one
|
|
membership.ID = existingMember.ID
|
|
collection.Members[i] = *membership
|
|
memberExists = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !memberExists {
|
|
impl.Logger.Info("adding new collection member",
|
|
zap.String("collection_id", collectionID.String()),
|
|
zap.String("recipient_id", membership.RecipientID.String()),
|
|
zap.String("permission_level", membership.PermissionLevel))
|
|
|
|
collection.Members = append(collection.Members, *membership)
|
|
|
|
impl.Logger.Info("DEBUGGING: Member added to collection.Members slice",
|
|
zap.String("collection_id", collectionID.String()),
|
|
zap.String("new_member_id", membership.ID.String()),
|
|
zap.String("recipient_id", membership.RecipientID.String()),
|
|
zap.Int("total_members_now", len(collection.Members)))
|
|
}
|
|
|
|
// Update version
|
|
collection.Version++
|
|
collection.ModifiedAt = time.Now()
|
|
|
|
impl.Logger.Info("prepared collection for update with member",
|
|
zap.String("collection_id", collection.ID.String()),
|
|
zap.Int("total_members", len(collection.Members)),
|
|
zap.Uint64("version", collection.Version))
|
|
|
|
// DEBUGGING: Log all members that will be sent to Update method
|
|
impl.Logger.Info("DEBUGGING: About to call Update() with these members:")
|
|
for debugIdx, debugMember := range collection.Members {
|
|
isOwner := debugMember.RecipientID == collection.OwnerID
|
|
impl.Logger.Info("DEBUGGING: Member in collection.Members slice",
|
|
zap.Int("index", debugIdx),
|
|
zap.String("member_id", debugMember.ID.String()),
|
|
zap.String("recipient_id", debugMember.RecipientID.String()),
|
|
zap.String("recipient_email", validation.MaskEmail(debugMember.RecipientEmail)),
|
|
zap.String("permission_level", debugMember.PermissionLevel),
|
|
zap.Bool("is_owner", isOwner),
|
|
zap.Int("encrypted_key_length", len(debugMember.EncryptedCollectionKey)))
|
|
}
|
|
|
|
// Log all members for debugging
|
|
for i, member := range collection.Members {
|
|
isOwner := member.RecipientID == collection.OwnerID
|
|
impl.Logger.Debug("collection member details",
|
|
zap.Int("member_index", i),
|
|
zap.String("member_id", member.ID.String()),
|
|
zap.String("recipient_id", member.RecipientID.String()),
|
|
zap.String("recipient_email", validation.MaskEmail(member.RecipientEmail)),
|
|
zap.String("permission_level", member.PermissionLevel),
|
|
zap.Bool("is_inherited", member.IsInherited),
|
|
zap.Bool("is_owner", isOwner),
|
|
zap.Int("encrypted_key_length", len(member.EncryptedCollectionKey)))
|
|
}
|
|
|
|
// Call update - the Update method itself is atomic and reliable
|
|
err = impl.Update(ctx, collection)
|
|
if err != nil {
|
|
impl.Logger.Error("failed to update collection with new member",
|
|
zap.String("collection_id", collectionID.String()),
|
|
zap.String("recipient_id", membership.RecipientID.String()),
|
|
zap.Error(err))
|
|
return fmt.Errorf("failed to update collection: %w", err)
|
|
}
|
|
|
|
impl.Logger.Info("successfully added member to collection",
|
|
zap.String("collection_id", collectionID.String()),
|
|
zap.String("recipient_id", membership.RecipientID.String()),
|
|
zap.String("member_id", membership.ID.String()))
|
|
|
|
// DEVELOPER NOTE:
|
|
// Remove the immediate verification after update since Cassandra needs time to propagate:
|
|
|
|
// // DEBUGGING: Test if we can query the members table directly
|
|
// impl.Logger.Info("DEBUGGING: Testing direct access to members table")
|
|
// err = impl.testMembersTableAccess(ctx, collectionID)
|
|
// if err != nil {
|
|
// impl.Logger.Error("DEBUGGING: Failed to access members table",
|
|
// zap.String("collection_id", collectionID.String()),
|
|
// zap.Error(err))
|
|
// } else {
|
|
// impl.Logger.Info("DEBUGGING: Members table access test successful",
|
|
// zap.String("collection_id", collectionID.String()))
|
|
// }
|
|
|
|
return nil
|
|
}
|
|
|
|
// testDirectMemberInsert tests inserting directly into the members table (for debugging)
|
|
func (impl *collectionRepositoryImpl) testDirectMemberInsert(ctx context.Context, collectionID gocql.UUID, membership *dom_collection.CollectionMembership) error {
|
|
impl.Logger.Info("DEBUGGING: Testing direct insert into members table",
|
|
zap.String("collection_id", collectionID.String()),
|
|
zap.String("recipient_id", membership.RecipientID.String()))
|
|
|
|
query := `INSERT INTO collection_members_by_collection_id_and_recipient_id
|
|
(collection_id, recipient_id, member_id, recipient_email, granted_by_id,
|
|
encrypted_collection_key, permission_level, created_at,
|
|
is_inherited, inherited_from_id)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
|
|
err := impl.Session.Query(query,
|
|
collectionID, membership.RecipientID, membership.ID, membership.RecipientEmail,
|
|
membership.GrantedByID, membership.EncryptedCollectionKey,
|
|
membership.PermissionLevel, membership.CreatedAt,
|
|
membership.IsInherited, membership.InheritedFromID).WithContext(ctx).Exec()
|
|
|
|
if err != nil {
|
|
impl.Logger.Error("DEBUGGING: Direct insert failed",
|
|
zap.String("collection_id", collectionID.String()),
|
|
zap.String("recipient_id", membership.RecipientID.String()),
|
|
zap.Error(err))
|
|
return fmt.Errorf("direct insert failed: %w", err)
|
|
}
|
|
|
|
impl.Logger.Info("DEBUGGING: Direct insert successful",
|
|
zap.String("collection_id", collectionID.String()),
|
|
zap.String("recipient_id", membership.RecipientID.String()))
|
|
|
|
// Verify the insert worked
|
|
var foundMemberID gocql.UUID
|
|
verifyQuery := `SELECT member_id FROM collection_members_by_collection_id_and_recipient_id
|
|
WHERE collection_id = ? AND recipient_id = ?`
|
|
|
|
err = impl.Session.Query(verifyQuery, collectionID, membership.RecipientID).WithContext(ctx).Scan(&foundMemberID)
|
|
if err != nil {
|
|
if err == gocql.ErrNotFound {
|
|
impl.Logger.Error("DEBUGGING: Direct insert verification failed - member not found",
|
|
zap.String("collection_id", collectionID.String()),
|
|
zap.String("recipient_id", membership.RecipientID.String()))
|
|
return fmt.Errorf("direct insert verification failed - member not found")
|
|
}
|
|
impl.Logger.Error("DEBUGGING: Direct insert verification error",
|
|
zap.String("collection_id", collectionID.String()),
|
|
zap.String("recipient_id", membership.RecipientID.String()),
|
|
zap.Error(err))
|
|
return fmt.Errorf("verification query failed: %w", err)
|
|
}
|
|
|
|
impl.Logger.Info("DEBUGGING: Direct insert verification successful",
|
|
zap.String("collection_id", collectionID.String()),
|
|
zap.String("recipient_id", membership.RecipientID.String()),
|
|
zap.String("found_member_id", foundMemberID.String()))
|
|
|
|
return nil
|
|
}
|
|
|
|
// testMembersTableAccess verifies we can read from the members table
|
|
func (impl *collectionRepositoryImpl) testMembersTableAccess(ctx context.Context, collectionID gocql.UUID) error {
|
|
query := `SELECT COUNT(*) FROM collection_members_by_collection_id_and_recipient_id WHERE collection_id = ?`
|
|
|
|
var count int
|
|
err := impl.Session.Query(query, collectionID).WithContext(ctx).Scan(&count)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to query members table: %w", err)
|
|
}
|
|
|
|
impl.Logger.Info("DEBUGGING: Members table query successful",
|
|
zap.String("collection_id", collectionID.String()),
|
|
zap.Int("member_count", count))
|
|
|
|
return nil
|
|
}
|
|
|
|
func (impl *collectionRepositoryImpl) RemoveMember(ctx context.Context, collectionID, recipientID gocql.UUID) error {
|
|
// Load collection, remove member, and save
|
|
collection, err := impl.Get(ctx, collectionID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get collection: %w", err)
|
|
}
|
|
|
|
if collection == nil {
|
|
return fmt.Errorf("collection not found")
|
|
}
|
|
|
|
// Remove member from collection
|
|
var updatedMembers []dom_collection.CollectionMembership
|
|
found := false
|
|
|
|
for _, member := range collection.Members {
|
|
if member.RecipientID != recipientID {
|
|
updatedMembers = append(updatedMembers, member)
|
|
} else {
|
|
found = true
|
|
}
|
|
}
|
|
|
|
if !found {
|
|
return fmt.Errorf("member not found in collection")
|
|
}
|
|
|
|
collection.Members = updatedMembers
|
|
collection.Version++
|
|
|
|
return impl.Update(ctx, collection)
|
|
}
|
|
|
|
func (impl *collectionRepositoryImpl) UpdateMemberPermission(ctx context.Context, collectionID, recipientID gocql.UUID, newPermission string) error {
|
|
// Load collection, update member permission, and save
|
|
collection, err := impl.Get(ctx, collectionID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get collection: %w", err)
|
|
}
|
|
|
|
if collection == nil {
|
|
return fmt.Errorf("collection not found")
|
|
}
|
|
|
|
// Update member permission
|
|
found := false
|
|
for i, member := range collection.Members {
|
|
if member.RecipientID == recipientID {
|
|
collection.Members[i].PermissionLevel = newPermission
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !found {
|
|
return fmt.Errorf("member not found in collection")
|
|
}
|
|
|
|
collection.Version++
|
|
return impl.Update(ctx, collection)
|
|
}
|
|
|
|
func (impl *collectionRepositoryImpl) GetCollectionMembership(ctx context.Context, collectionID, recipientID gocql.UUID) (*dom_collection.CollectionMembership, error) {
|
|
var membership dom_collection.CollectionMembership
|
|
|
|
query := `SELECT recipient_id, member_id, recipient_email, granted_by_id,
|
|
encrypted_collection_key, permission_level, created_at,
|
|
is_inherited, inherited_from_id
|
|
FROM collection_members_by_collection_id_and_recipient_id
|
|
WHERE collection_id = ? AND recipient_id = ?`
|
|
|
|
err := impl.Session.Query(query, collectionID, recipientID).WithContext(ctx).Scan(
|
|
&membership.RecipientID, &membership.ID, &membership.RecipientEmail, &membership.GrantedByID,
|
|
&membership.EncryptedCollectionKey, &membership.PermissionLevel,
|
|
&membership.CreatedAt, &membership.IsInherited, &membership.InheritedFromID)
|
|
|
|
if err != nil {
|
|
if err == gocql.ErrNotFound {
|
|
return nil, nil
|
|
}
|
|
return nil, err
|
|
}
|
|
|
|
membership.CollectionID = collectionID
|
|
|
|
return &membership, nil
|
|
}
|
|
|
|
func (impl *collectionRepositoryImpl) AddMemberToHierarchy(ctx context.Context, rootID gocql.UUID, membership *dom_collection.CollectionMembership) error {
|
|
// Get all descendants of the root collection
|
|
descendants, err := impl.FindDescendants(ctx, rootID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to find descendants: %w", err)
|
|
}
|
|
|
|
impl.Logger.Info("adding member to collection hierarchy",
|
|
zap.String("root_collection_id", rootID.String()),
|
|
zap.String("recipient_id", membership.RecipientID.String()),
|
|
zap.Int("descendants_count", len(descendants)))
|
|
|
|
// Add to root collection
|
|
if err := impl.AddMember(ctx, rootID, membership); err != nil {
|
|
return fmt.Errorf("failed to add member to root collection: %w", err)
|
|
}
|
|
|
|
// Add to all descendants with inherited flag
|
|
inheritedMembership := *membership
|
|
inheritedMembership.IsInherited = true
|
|
inheritedMembership.InheritedFromID = rootID
|
|
|
|
successCount := 0
|
|
for _, descendant := range descendants {
|
|
// Generate new ID for each inherited membership
|
|
inheritedMembership.ID = gocql.TimeUUID()
|
|
|
|
if err := impl.AddMember(ctx, descendant.ID, &inheritedMembership); err != nil {
|
|
impl.Logger.Warn("failed to add inherited member to descendant",
|
|
zap.String("descendant_id", descendant.ID.String()),
|
|
zap.String("recipient_id", membership.RecipientID.String()),
|
|
zap.Error(err))
|
|
} else {
|
|
successCount++
|
|
}
|
|
}
|
|
|
|
impl.Logger.Info("completed hierarchy member addition",
|
|
zap.String("root_collection_id", rootID.String()),
|
|
zap.String("recipient_id", membership.RecipientID.String()),
|
|
zap.Int("total_descendants", len(descendants)),
|
|
zap.Int("successful_additions", successCount))
|
|
|
|
return nil
|
|
}
|
|
|
|
func (impl *collectionRepositoryImpl) RemoveMemberFromHierarchy(ctx context.Context, rootID, recipientID gocql.UUID) error {
|
|
// Get all descendants of the root collection
|
|
descendants, err := impl.FindDescendants(ctx, rootID)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to find descendants: %w", err)
|
|
}
|
|
|
|
// Remove from root collection
|
|
if err := impl.RemoveMember(ctx, rootID, recipientID); err != nil {
|
|
return fmt.Errorf("failed to remove member from root collection: %w", err)
|
|
}
|
|
|
|
// Remove from all descendants where access was inherited from this root
|
|
for _, descendant := range descendants {
|
|
// Only remove if the membership was inherited from this root
|
|
membership, err := impl.GetCollectionMembership(ctx, descendant.ID, recipientID)
|
|
if err != nil {
|
|
impl.Logger.Warn("failed to get membership for descendant",
|
|
zap.String("descendant_id", descendant.ID.String()),
|
|
zap.Error(err))
|
|
continue
|
|
}
|
|
|
|
if membership != nil && membership.IsInherited && membership.InheritedFromID == rootID {
|
|
if err := impl.RemoveMember(ctx, descendant.ID, recipientID); err != nil {
|
|
impl.Logger.Warn("failed to remove inherited member from descendant",
|
|
zap.String("descendant_id", descendant.ID.String()),
|
|
zap.String("recipient_id", recipientID.String()),
|
|
zap.Error(err))
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// RemoveUserFromAllCollections removes a user from all collections they are a member of
|
|
// Used for GDPR right-to-be-forgotten implementation
|
|
// Returns a list of collection IDs that were modified
|
|
func (impl *collectionRepositoryImpl) RemoveUserFromAllCollections(ctx context.Context, userID gocql.UUID, userEmail string) ([]gocql.UUID, error) {
|
|
impl.Logger.Info("Removing user from all shared collections",
|
|
zap.String("user_id", userID.String()),
|
|
zap.String("user_email", validation.MaskEmail(userEmail)))
|
|
|
|
// Get all collections shared with the user
|
|
sharedCollections, err := impl.GetCollectionsSharedWithUser(ctx, userID)
|
|
if err != nil {
|
|
impl.Logger.Error("Failed to get collections shared with user",
|
|
zap.String("user_id", userID.String()),
|
|
zap.Error(err))
|
|
return nil, fmt.Errorf("failed to get shared collections: %w", err)
|
|
}
|
|
|
|
impl.Logger.Info("Found shared collections for user",
|
|
zap.String("user_id", userID.String()),
|
|
zap.Int("collection_count", len(sharedCollections)))
|
|
|
|
var modifiedCollections []gocql.UUID
|
|
successCount := 0
|
|
failureCount := 0
|
|
|
|
// Remove user from each collection
|
|
for _, collection := range sharedCollections {
|
|
err := impl.RemoveMember(ctx, collection.ID, userID)
|
|
if err != nil {
|
|
impl.Logger.Warn("Failed to remove user from collection",
|
|
zap.String("collection_id", collection.ID.String()),
|
|
zap.String("user_id", userID.String()),
|
|
zap.Error(err))
|
|
failureCount++
|
|
// Continue with other collections despite error
|
|
continue
|
|
}
|
|
|
|
modifiedCollections = append(modifiedCollections, collection.ID)
|
|
successCount++
|
|
|
|
impl.Logger.Debug("Removed user from collection",
|
|
zap.String("collection_id", collection.ID.String()),
|
|
zap.String("user_id", userID.String()))
|
|
}
|
|
|
|
impl.Logger.Info("✅ Completed removing user from shared collections",
|
|
zap.String("user_id", userID.String()),
|
|
zap.Int("total_collections", len(sharedCollections)),
|
|
zap.Int("success_count", successCount),
|
|
zap.Int("failure_count", failureCount),
|
|
zap.Int("modified_collections", len(modifiedCollections)))
|
|
|
|
// Return success even if some removals failed - partial success is acceptable
|
|
return modifiedCollections, nil
|
|
}
|