@@ -16,6 +16,10 @@ import {
1616 inviteCollaboratorWithoutAccount ,
1717 removeCollaborator ,
1818} from "@cocalc/server/projects/collaborators" ;
19+ import {
20+ add_collaborators_to_projects ,
21+ remove_collaborators_from_projects ,
22+ } from "@cocalc/server/projects/collab" ;
1923import {
2024 resetServerSettingsCache ,
2125 getServerSettings ,
@@ -320,3 +324,218 @@ describe("invite permissions", () => {
320324 ) . rejects . toMatchObject ( { code : OwnershipErrorCode . NOT_OWNER } ) ;
321325 } ) ;
322326} ) ;
327+
328+ describe ( "REST API level protection (collab.ts functions)" , ( ) => {
329+ test ( "add_collaborators_to_projects bypasses check when using tokens" , async ( ) => {
330+ const { projectId } = await createProjectWithOwner ( ) ;
331+ const outsideUserId = await createUser ( "outsider" ) ;
332+
333+ // Create a project invite token
334+ const { rows } = await getPool ( ) . query (
335+ "INSERT INTO project_invite_tokens (token, project_id, usage_limit) VALUES ($1, $2, $3) RETURNING token" ,
336+ [ "test-token-123" , projectId , 10 ] ,
337+ ) ;
338+ const token = rows [ 0 ] . token ;
339+
340+ // Enable site-wide strict collaborator management
341+ await setSiteStrictCollab ( "yes" ) ;
342+
343+ // Outside user should be able to add themselves using a token
344+ // even though they're not a collaborator or owner
345+ await expect (
346+ add_collaborators_to_projects (
347+ db ( ) ,
348+ outsideUserId ,
349+ [ outsideUserId ] ,
350+ [ "" ] , // empty project_id because token determines the project
351+ [ token ] ,
352+ ) ,
353+ ) . resolves . toBeUndefined ( ) ;
354+
355+ // Verify the user was actually added
356+ const { rows : projectRows } = await getPool ( ) . query (
357+ "SELECT users FROM projects WHERE project_id=$1" ,
358+ [ projectId ] ,
359+ ) ;
360+ expect ( projectRows [ 0 ] . users [ outsideUserId ] ) . toBeDefined ( ) ;
361+ } ) ;
362+
363+ test ( "add_collaborators_to_projects enforces strict_collaborator_management site setting" , async ( ) => {
364+ const { ownerId, projectId } = await createProjectWithOwner ( ) ;
365+ const collaboratorId = await createUser ( "collab" ) ;
366+ const newCollabId = await createUser ( "newcollab" ) ;
367+
368+ // Add first collaborator as owner
369+ await add_collaborators_to_projects (
370+ db ( ) ,
371+ ownerId ,
372+ [ collaboratorId ] ,
373+ [ projectId ] ,
374+ ) ;
375+
376+ // Enable site-wide strict collaborator management
377+ await setSiteStrictCollab ( "yes" ) ;
378+
379+ // Collaborator should not be able to add another user
380+ await expect (
381+ add_collaborators_to_projects (
382+ db ( ) ,
383+ collaboratorId ,
384+ [ newCollabId ] ,
385+ [ projectId ] ,
386+ ) ,
387+ ) . rejects . toThrow ( "Only owners can manage collaborators" ) ;
388+ } ) ;
389+
390+ test ( "add_collaborators_to_projects enforces project-level manage_users_owner_only" , async ( ) => {
391+ const { ownerId, projectId } = await createProjectWithOwner ( ) ;
392+ const collaboratorId = await createUser ( "collab" ) ;
393+ const newCollabId = await createUser ( "newcollab" ) ;
394+
395+ // Add first collaborator as owner
396+ await add_collaborators_to_projects (
397+ db ( ) ,
398+ ownerId ,
399+ [ collaboratorId ] ,
400+ [ projectId ] ,
401+ ) ;
402+
403+ // Enable project-level strict management
404+ await getPool ( ) . query (
405+ "UPDATE projects SET manage_users_owner_only=$1 WHERE project_id=$2" ,
406+ [ true , projectId ] ,
407+ ) ;
408+
409+ // Collaborator should not be able to add another user
410+ await expect (
411+ add_collaborators_to_projects (
412+ db ( ) ,
413+ collaboratorId ,
414+ [ newCollabId ] ,
415+ [ projectId ] ,
416+ ) ,
417+ ) . rejects . toThrow ( "Only owners can manage collaborators" ) ;
418+ } ) ;
419+
420+ test ( "remove_collaborators_from_projects enforces strict_collaborator_management site setting" , async ( ) => {
421+ const { ownerId, projectId } = await createProjectWithOwner ( ) ;
422+ const collaborator1Id = await createUser ( "collab1" ) ;
423+ const collaborator2Id = await createUser ( "collab2" ) ;
424+
425+ // Add collaborators as owner
426+ await add_collaborators_to_projects (
427+ db ( ) ,
428+ ownerId ,
429+ [ collaborator1Id , collaborator2Id ] ,
430+ [ projectId , projectId ] ,
431+ ) ;
432+
433+ // Enable site-wide strict collaborator management
434+ await setSiteStrictCollab ( "yes" ) ;
435+
436+ // Collaborator should not be able to remove another collaborator
437+ await expect (
438+ remove_collaborators_from_projects (
439+ db ( ) ,
440+ collaborator1Id ,
441+ [ collaborator2Id ] ,
442+ [ projectId ] ,
443+ ) ,
444+ ) . rejects . toThrow ( "Only owners can manage collaborators" ) ;
445+ } ) ;
446+
447+ test ( "remove_collaborators_from_projects allows self-removal even with strict_collaborator_management" , async ( ) => {
448+ const { ownerId, projectId } = await createProjectWithOwner ( ) ;
449+ const collaboratorId = await createUser ( "collab" ) ;
450+
451+ // Add collaborator as owner
452+ await add_collaborators_to_projects (
453+ db ( ) ,
454+ ownerId ,
455+ [ collaboratorId ] ,
456+ [ projectId ] ,
457+ ) ;
458+
459+ // Enable site-wide strict collaborator management
460+ await setSiteStrictCollab ( "yes" ) ;
461+
462+ // Collaborator should be able to remove themselves
463+ await expect (
464+ remove_collaborators_from_projects (
465+ db ( ) ,
466+ collaboratorId ,
467+ [ collaboratorId ] ,
468+ [ projectId ] ,
469+ ) ,
470+ ) . resolves . toBeUndefined ( ) ;
471+ } ) ;
472+
473+ test ( "remove_collaborators_from_projects enforces project-level manage_users_owner_only" , async ( ) => {
474+ const { ownerId, projectId } = await createProjectWithOwner ( ) ;
475+ const collaborator1Id = await createUser ( "collab1" ) ;
476+ const collaborator2Id = await createUser ( "collab2" ) ;
477+
478+ // Add collaborators as owner
479+ await add_collaborators_to_projects (
480+ db ( ) ,
481+ ownerId ,
482+ [ collaborator1Id , collaborator2Id ] ,
483+ [ projectId , projectId ] ,
484+ ) ;
485+
486+ // Enable project-level strict management
487+ await getPool ( ) . query (
488+ "UPDATE projects SET manage_users_owner_only=$1 WHERE project_id=$2" ,
489+ [ true , projectId ] ,
490+ ) ;
491+
492+ // Collaborator should not be able to remove another collaborator
493+ await expect (
494+ remove_collaborators_from_projects (
495+ db ( ) ,
496+ collaborator1Id ,
497+ [ collaborator2Id ] ,
498+ [ projectId ] ,
499+ ) ,
500+ ) . rejects . toThrow ( "Only owners can manage collaborators" ) ;
501+ } ) ;
502+
503+ test ( "add_collaborators_to_projects allows owners to add when strict management is enabled" , async ( ) => {
504+ const { ownerId, projectId } = await createProjectWithOwner ( ) ;
505+ const newCollabId = await createUser ( "newcollab" ) ;
506+
507+ // Enable site-wide strict collaborator management
508+ await setSiteStrictCollab ( "yes" ) ;
509+
510+ // Owner should be able to add a collaborator
511+ await expect (
512+ add_collaborators_to_projects ( db ( ) , ownerId , [ newCollabId ] , [ projectId ] ) ,
513+ ) . resolves . toBeUndefined ( ) ;
514+ } ) ;
515+
516+ test ( "remove_collaborators_from_projects allows owners to remove when strict management is enabled" , async ( ) => {
517+ const { ownerId, projectId } = await createProjectWithOwner ( ) ;
518+ const collaboratorId = await createUser ( "collab" ) ;
519+
520+ // Add collaborator
521+ await add_collaborators_to_projects (
522+ db ( ) ,
523+ ownerId ,
524+ [ collaboratorId ] ,
525+ [ projectId ] ,
526+ ) ;
527+
528+ // Enable site-wide strict collaborator management
529+ await setSiteStrictCollab ( "yes" ) ;
530+
531+ // Owner should be able to remove a collaborator
532+ await expect (
533+ remove_collaborators_from_projects (
534+ db ( ) ,
535+ ownerId ,
536+ [ collaboratorId ] ,
537+ [ projectId ] ,
538+ ) ,
539+ ) . resolves . toBeUndefined ( ) ;
540+ } ) ;
541+ } ) ;
0 commit comments