6464import org .labkey .api .security .User ;
6565import org .labkey .api .security .permissions .AbstractContainerScopingTest ;
6666import org .labkey .api .security .permissions .ReadPermission ;
67+ import org .labkey .api .security .roles .AuthorRole ;
6768import org .labkey .api .security .roles .ReaderRole ;
6869import org .labkey .api .survey .model .Survey ;
6970import org .labkey .api .survey .model .SurveyDesign ;
7071import org .labkey .api .survey .model .SurveyListener ;
7172import org .labkey .api .util .JsonUtil ;
7273import org .labkey .api .util .PageFlowUtil ;
7374import org .labkey .api .util .Path ;
75+ import org .labkey .api .view .ActionURL ;
7476import org .labkey .api .view .ViewContext ;
7577import org .springframework .validation .BindException ;
7678
@@ -268,16 +270,36 @@ public Survey saveSurvey(Container container, User user, Survey survey)
268270 }
269271 }
270272
271- /** Checks that the user has read permission to the container that owns the design, but we accept cross-container references */
273+ /**
274+ * Checks that the user has read permission to the container that owns the design, but we accept cross-container references
275+ */
276+ @ Nullable
277+ public SurveyDesign getSurveyDesignForRead (Container container , User user , int surveyId )
278+ {
279+ SurveyDesign surveyDesign = _getSurveyDesign (new SimpleFilter (), surveyId );
280+
281+ if (surveyDesign != null )
282+ {
283+ Container actualContainer = ContainerManager .getForId (surveyDesign .getContainerId ());
284+
285+ // A survey design can be requested from a different folder provided the user has read permission.
286+ return actualContainer == null || !actualContainer .hasPermission (user , ReadPermission .class ) ? null : surveyDesign ;
287+ }
288+ return null ;
289+ }
290+
291+ @ Nullable
292+ public SurveyDesign getSurveyDesignForWrite (Container container , User user , int surveyId )
293+ {
294+ // Container scoping is enforced when updating a survey design.
295+ return _getSurveyDesign (SimpleFilter .createContainerFilter (container ), surveyId );
296+ }
297+
272298 @ Nullable
273- public SurveyDesign getSurveyDesign ( Container container , User user , int surveyId )
299+ private SurveyDesign _getSurveyDesign ( SimpleFilter filter , int surveyId )
274300 {
275- SimpleFilter filter = new SimpleFilter (FieldKey .fromParts ("rowId" ), surveyId );
276- SurveyDesign result = new TableSelector (SurveySchema .getInstance ().getSurveyDesignsTable (), filter , null ).getObject (SurveyDesign .class );
277- if (result == null )
278- return null ;
279- Container actualContainer = ContainerManager .getForId (result .getContainerId ());
280- return actualContainer == null || !actualContainer .hasPermission (user , ReadPermission .class ) ? null : result ;
301+ filter .addCondition (FieldKey .fromParts ("rowId" ), surveyId );
302+ return new TableSelector (SurveySchema .getInstance ().getSurveyDesignsTable (), filter , null ).getObject (SurveyDesign .class );
281303 }
282304
283305 /**
@@ -462,7 +484,7 @@ public List<Throwable> fireBeforeDeleteSurvey(Container c, User user, Survey sur
462484 public static List <Throwable > fireDeleteSurvey (Container c , User user , Survey survey )
463485 {
464486 List <Throwable > errors = new ArrayList <>();
465- SurveyDesign design = SurveyManager .get ().getSurveyDesign (c , user , survey .getSurveyDesignId ());
487+ SurveyDesign design = SurveyManager .get ().getSurveyDesignForRead (c , user , survey .getSurveyDesignId ());
466488
467489 for (SurveyListener l : _surveyListeners )
468490 {
@@ -825,26 +847,59 @@ public void testSurveyDesignContainerScoping() throws Exception
825847 // 1. Same container: a user with read access in the design's container sees it.
826848 User readerA = createUserInRole (_projectA , ReaderRole .class );
827849 assertNotNull ("Design should be visible from its own container to a user with read access" ,
828- sm .getSurveyDesign (_projectA , readerA , designId ));
850+ sm .getSurveyDesignForRead (_projectA , readerA , designId ));
829851
830852 // 2. Different container, caller can read the design's container: tolerated, design is returned.
831853 User readerAB = createUserInRole (_projectA , ReaderRole .class );
832854 grantRole (readerAB , _projectB , ReaderRole .class );
833855 assertNotNull ("Design should be visible from another container when the caller can read the design's container" ,
834- sm .getSurveyDesign (_projectB , readerAB , designId ));
856+ sm .getSurveyDesignForRead (_projectB , readerAB , designId ));
835857
836858 // 3. Different container, caller cannot read the design's container: must return null.
837859 User readerB = createUserInRole (_projectB , ReaderRole .class );
838860 assertNull ("Design must NOT be visible to a caller without read access to the design's container" ,
839- sm .getSurveyDesign (_projectB , readerB , designId ));
861+ sm .getSurveyDesignForRead (_projectB , readerB , designId ));
840862
841863 // A delete issued from the wrong container must not remove the design
842864 sm .deleteSurveyDesign (_projectB , _user , designId , true );
843- assertNotNull ("Cross-container delete must be a no-op" , sm .getSurveyDesign (_projectA , _user , designId ));
865+ assertNotNull ("Cross-container delete must be a no-op" , sm .getSurveyDesignForRead (_projectA , _user , designId ));
844866
845867 // A delete from the correct container removes it
846868 sm .deleteSurveyDesign (_projectA , _user , designId , true );
847- assertNull ("Same-container delete should remove the design" , sm .getSurveyDesign (_projectA , _user , designId ));
869+ assertNull ("Same-container delete should remove the design" , sm .getSurveyDesignForRead (_projectA , _user , designId ));
870+ }
871+
872+ // GH Issue 1308: SaveSurveyTemplateAction must not let a caller in one folder overwrite and reparent a survey
873+ // design owned by another folder.
874+ @ Test
875+ public void testSaveSurveyTemplateActionContainerScoping () throws Exception
876+ {
877+ SurveyManager sm = SurveyManager .get ();
878+
879+ SurveyDesign design = new SurveyDesign ();
880+ design .setLabel ("Design owned by A" );
881+ design .setDescription ("original description" );
882+ design = sm .saveSurveyDesign (_projectA , _user , design );
883+ int designId = design .getRowId ();
884+
885+ User attacker = createUserInRole (_projectA , ReaderRole .class );
886+ grantRole (attacker , _projectB , AuthorRole .class );
887+
888+ ActionURL url = new ActionURL (SurveyController .SaveSurveyTemplateAction .class , _projectB )
889+ .addParameter ("rowId" , designId )
890+ .addParameter ("label" , "STOLEN" )
891+ .addParameter ("description" , "hijacked" );
892+ post (url , attacker );
893+
894+ // The design must still belong to folder A with its original field values: not reparented, not overwritten.
895+ SurveyDesign after = sm .getSurveyDesignForRead (_projectA , _user , designId );
896+ assertNotNull ("Design must still exist after the cross-container save attempt" , after );
897+ assertEquals ("Design must NOT be reparented into the attacker's container" ,
898+ _projectA .getId (), after .getContainerId ());
899+ assertEquals ("Design label must NOT be overwritten from another container" ,
900+ "Design owned by A" , after .getLabel ());
901+ assertEquals ("Design description must NOT be overwritten from another container" ,
902+ "original description" , after .getDescription ());
848903 }
849904
850905 @ Test
@@ -854,7 +909,7 @@ public void testSurveyContainerScoping()
854909
855910 SurveyDesign design = new SurveyDesign ();
856911 design .setLabel ("Scoping test design for survey" );
857- design = sm .saveSurveyDesign (_projectA , _user , design );
912+ design = sm .saveSurveyDesign (_projectA , _user , design );
858913
859914 Survey survey = new Survey ();
860915 survey .setLabel ("Scoping test survey" );
0 commit comments