# HG changeset patch # User ymh # Date 1365004043 -7200 # Node ID 01c862ada33c85f5643502ce7854668ca5cb09c6 # Parent 93a1fbe6a848f4b37d00b688e2917dc0f91aa067 Add delete for spaces, check that there is no linked projects diff -r 93a1fbe6a848 -r 01c862ada33c server/pom.xml --- a/server/pom.xml Tue Apr 02 14:05:56 2013 +0200 +++ b/server/pom.xml Wed Apr 03 17:47:23 2013 +0200 @@ -398,6 +398,11 @@ hibernate-validator 4.2.0.Final + + com.fasterxml.uuid + java-uuid-generator + 3.1.3 + IRI diff -r 93a1fbe6a848 -r 01c862ada33c server/src/main/java/org/iri_research/renkan/controller/AdminController.java --- a/server/src/main/java/org/iri_research/renkan/controller/AdminController.java Tue Apr 02 14:05:56 2013 +0200 +++ b/server/src/main/java/org/iri_research/renkan/controller/AdminController.java Wed Apr 03 17:47:23 2013 +0200 @@ -1,13 +1,20 @@ package org.iri_research.renkan.controller; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.Arrays; +import java.util.Map; + import javax.servlet.http.HttpServletRequest; import javax.validation.Valid; +import org.apache.commons.codec.binary.Hex; import org.iri_research.renkan.Constants; import org.iri_research.renkan.RenkanException; import org.iri_research.renkan.forms.SpaceForm; import org.iri_research.renkan.forms.SpaceFormValidator; import org.iri_research.renkan.models.Space; +import org.iri_research.renkan.repositories.ProjectsRepository; import org.iri_research.renkan.repositories.SpacesRepository; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -26,6 +33,7 @@ import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.client.HttpClientErrorException; @Controller @@ -37,6 +45,8 @@ @Autowired private SpacesRepository spacesRepository; + @Autowired + private ProjectsRepository projectsRepository; @InitBinder(value={"space"}) protected void initBinder(WebDataBinder binder) { @@ -59,6 +69,7 @@ model.addAttribute("page", page); model.addAttribute("baseUrl", Utils.buildBaseUrl(request)); + model.addAttribute("projectsCount", this.projectsRepository.getCountBySpace()); return "admin/spacesList"; } @@ -108,7 +119,78 @@ throw new HttpClientErrorException(HttpStatus.NOT_FOUND, "space " + spaceForm.getId() + " not found"); } - return "redirect:"; + return "redirect:/admin/spaces"; + } + + //@RequestMapping(value="/spaces/confirmdelete/{spaceId}", method = RequestMethod.GET) + //public String askDeleteSpace(Model model, @PathVariable(value="spaceId") String spaceId) { + + + //} + + @RequestMapping(value="/spaces/delete/{spaceId}") + public String deleteSpace( + HttpServletRequest request, + Model model, + @PathVariable(value="spaceId") String spaceId, + @RequestParam(value="key", required=false) String key, + @RequestParam(value="salt", required=false) String salt) throws NoSuchAlgorithmException, RenkanException + { + + if(spaceId == null || spaceId.length() == 0) { + throw new HttpClientErrorException(HttpStatus.BAD_REQUEST, "Null or empty space id"); + } + + RequestMethod method = RequestMethod.valueOf(request.getMethod()); + + Map nbProj = this.projectsRepository.getCountBySpace(Arrays.asList(spaceId)); + if(nbProj.containsKey(spaceId) && nbProj.get(spaceId).intValue()>0) { + throw new HttpClientErrorException(HttpStatus.BAD_REQUEST, "This space have projects"); + } + + if(RequestMethod.GET.equals(method)) { + + Space space = this.spacesRepository.findOne(spaceId); + + if(space == null) { + throw new HttpClientErrorException(HttpStatus.NOT_FOUND, "space " + spaceId + " not found"); + } + + SecureRandom rand = SecureRandom.getInstance("SHA1PRNG"); + rand.setSeed(System.currentTimeMillis()); + byte[] rawSalt = new byte[50]; + rand.nextBytes(rawSalt); + String newSalt = Hex.encodeHexString(rawSalt); + + + model.addAttribute("spaceObj", space); + model.addAttribute("salt", newSalt); + model.addAttribute("key", space.getKey(newSalt)); + + return "admin/spaceDeleteConfirm"; + } + else if (RequestMethod.POST.equals(method) && key != null && !key.isEmpty() && salt != null && !salt.isEmpty()) { + + if(spaceId != null && spaceId.length() > 0) { + + Space space = this.spacesRepository.findOne(spaceId); + if(space != null) { + if(space.checkKey(key, salt)) { + this.spacesRepository.delete(spaceId); + } + else { + throw new HttpClientErrorException(HttpStatus.BAD_REQUEST, "Key not ckecked"); + } + } + + } + return "redirect:/admin/spaces"; + + } + else { + throw new HttpClientErrorException(HttpStatus.BAD_REQUEST, "Bad request method or parameters"); + } + } } diff -r 93a1fbe6a848 -r 01c862ada33c server/src/main/java/org/iri_research/renkan/models/Space.java --- a/server/src/main/java/org/iri_research/renkan/models/Space.java Tue Apr 02 14:05:56 2013 +0200 +++ b/server/src/main/java/org/iri_research/renkan/models/Space.java Wed Apr 03 17:47:23 2013 +0200 @@ -1,7 +1,15 @@ package org.iri_research.renkan.models; +import java.io.UnsupportedEncodingException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; import java.util.Date; +import javax.crypto.spec.SecretKeySpec; + +import org.apache.commons.codec.binary.Hex; +import org.iri_research.renkan.Constants; +import org.iri_research.renkan.RenkanException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.mongodb.core.mapping.Document; import org.springframework.data.mongodb.core.mapping.Field; @@ -69,4 +77,48 @@ this.image = image; } + private String getRawKey(String salt) { + StringBuffer key = new StringBuffer(salt!=null?salt+"|":""); + key.append(this.getId()); + key.append('|'); + key.append(this.getCreated().getTime()); + return key.toString(); + } + + public String getKey(String salt) throws RenkanException { + + String rawKey = this.getRawKey(salt); + + MessageDigest md; + try { + md = MessageDigest.getInstance("SHA-256"); + } catch (NoSuchAlgorithmException e) { + throw new RenkanException("NoSuchAlgorithmException digest: " + e.getMessage(), e); + } + String key; + final SecretKeySpec secret_key = new SecretKeySpec(Constants.KEYHEX.getBytes(), "HmacSHA256"); + md.update(secret_key.getEncoded()); + try { + key = Hex.encodeHexString(md.digest(rawKey.getBytes("UTF-8"))); + } catch (UnsupportedEncodingException e) { + throw new RenkanException("UnsupportedEncodingException digest: " + e.getMessage(), e); + } + + return key; + } + + public boolean checkKey(String key, String salt) throws RenkanException { + + + if(key == null || key.isEmpty()) { + return false; + } + + String signature = key; + + String new_key = this.getKey(salt); + + return new_key.equals(signature); + } + } \ No newline at end of file diff -r 93a1fbe6a848 -r 01c862ada33c server/src/main/java/org/iri_research/renkan/repositories/ProjectsRepositoryCustom.java --- a/server/src/main/java/org/iri_research/renkan/repositories/ProjectsRepositoryCustom.java Tue Apr 02 14:05:56 2013 +0200 +++ b/server/src/main/java/org/iri_research/renkan/repositories/ProjectsRepositoryCustom.java Wed Apr 03 17:47:23 2013 +0200 @@ -1,5 +1,6 @@ package org.iri_research.renkan.repositories; +import java.util.Collection; import java.util.Map; import org.iri_research.renkan.models.Project; @@ -8,6 +9,7 @@ public int getRevCounter(String projectId); public Map getCountBySpace(); + public Map getCountBySpace(Collection spaceIds); public void deleteRecursive(String projectId); public void deleteRecursive(Project project); diff -r 93a1fbe6a848 -r 01c862ada33c server/src/main/java/org/iri_research/renkan/repositories/ProjectsRepositoryImpl.java --- a/server/src/main/java/org/iri_research/renkan/repositories/ProjectsRepositoryImpl.java Tue Apr 02 14:05:56 2013 +0200 +++ b/server/src/main/java/org/iri_research/renkan/repositories/ProjectsRepositoryImpl.java Wed Apr 03 17:47:23 2013 +0200 @@ -1,6 +1,7 @@ package org.iri_research.renkan.repositories; import java.util.Arrays; +import java.util.Collection; import java.util.HashMap; import java.util.Map; @@ -14,6 +15,7 @@ import org.springframework.data.mongodb.core.mapreduce.GroupBy; import org.springframework.data.mongodb.core.mapreduce.GroupByResults; +import org.springframework.data.mongodb.core.query.Criteria; import org.springframework.data.mongodb.core.query.Update; @Component @@ -49,11 +51,19 @@ } return p.getRevCounter(); } + @Override - public Map getCountBySpace() { + public Map getCountBySpace(Collection spaceIds) { + + Criteria filter = null; + + if(spaceIds != null) { + filter = Criteria.where("space_id").in(spaceIds); + } GroupByResults groupResult = this.mongoTemplate.group( + filter, this.mongoTemplate.getCollectionName(Project.class), GroupBy.key("space_id").initialDocument("{ count: 0 }").reduceFunction("function(doc, prev) { prev.count += 1; }"), GroupResult.class); @@ -66,6 +76,12 @@ return res; } + + @Override + public Map getCountBySpace() { + return this.getCountBySpace(null); + } + @Override public Project copy(Project p, String newTitle) { @@ -107,5 +123,4 @@ this.projectsRepository.delete(p); } } - } diff -r 93a1fbe6a848 -r 01c862ada33c server/src/main/webapp/WEB-INF/i18n/messages_en.properties --- a/server/src/main/webapp/WEB-INF/i18n/messages_en.properties Tue Apr 02 14:05:56 2013 +0200 +++ b/server/src/main/webapp/WEB-INF/i18n/messages_en.properties Wed Apr 03 17:47:23 2013 +0200 @@ -1,5 +1,7 @@ date.format = yyyy/MM/dd HH:mm +question.yes = yes +question.no = no renkanIndex.renkan_exp = Create a Renkan renkanIndex.project_list = Renkan list @@ -40,6 +42,8 @@ renkanAdmin.space_add = Add space renkanAdmin.space_edit = Edit space +renkanAdmin.space_delete = Delete space +renkanAdmin.space_confirm_delete = Do you want to delete the space entitled "{0}" ? renkanAdmin.object_name = Name renkanAdmin.object_edit = Edit diff -r 93a1fbe6a848 -r 01c862ada33c server/src/main/webapp/WEB-INF/i18n/messages_fr.properties --- a/server/src/main/webapp/WEB-INF/i18n/messages_fr.properties Tue Apr 02 14:05:56 2013 +0200 +++ b/server/src/main/webapp/WEB-INF/i18n/messages_fr.properties Wed Apr 03 17:47:23 2013 +0200 @@ -1,5 +1,7 @@ date.format = dd/MM/yyyy HH:mm +question.yes = oui +question.no = non renkanIndex.renkan_exp = Créer un Renkan @@ -38,6 +40,8 @@ renkanAdmin.space_objects_name = Espaces renkanAdmin.space_add = Nouvel espace renkanAdmin.space_edit = Edition espaces +renkanAdmin.space_delete = Supression espace +renkanAdmin.space_confirm_delete = Confirmez-vous l'effacement de l'espace intitulé "{0}" ? renkanAdmin.object_name = Nom renkanAdmin.object_edit = Modif. diff -r 93a1fbe6a848 -r 01c862ada33c server/src/main/webapp/WEB-INF/spring-servlet.xml --- a/server/src/main/webapp/WEB-INF/spring-servlet.xml Tue Apr 02 14:05:56 2013 +0200 +++ b/server/src/main/webapp/WEB-INF/spring-servlet.xml Wed Apr 03 17:47:23 2013 +0200 @@ -40,6 +40,7 @@ + @@ -59,6 +60,7 @@ + diff -r 93a1fbe6a848 -r 01c862ada33c server/src/main/webapp/WEB-INF/templates/admin/spaceDeleteConfirm.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/server/src/main/webapp/WEB-INF/templates/admin/spaceDeleteConfirm.html Wed Apr 03 17:47:23 2013 +0200 @@ -0,0 +1,32 @@ + + + + Renkan Admin - edit space + + + + + + + + + + + + + +
+
+

Renkan administration

+

Spaces List / Delete space

+
+
Do you want to delete space with title
+
+
+
+
+
© 2013 IRI - Version 0.0
+
+
+ + \ No newline at end of file diff -r 93a1fbe6a848 -r 01c862ada33c server/src/main/webapp/WEB-INF/templates/admin/spacesList.html --- a/server/src/main/webapp/WEB-INF/templates/admin/spacesList.html Tue Apr 02 14:05:56 2013 +0200 +++ b/server/src/main/webapp/WEB-INF/templates/admin/spacesList.html Wed Apr 03 17:47:23 2013 +0200 @@ -43,16 +43,18 @@ Name Created + Project count Edit Delete - + title created + nb. proj Edit - Delete + DeleteDelete diff -r 93a1fbe6a848 -r 01c862ada33c server/src/main/webapp/static/css/index.css --- a/server/src/main/webapp/static/css/index.css Tue Apr 02 14:05:56 2013 +0200 +++ b/server/src/main/webapp/static/css/index.css Wed Apr 03 17:47:23 2013 +0200 @@ -212,6 +212,12 @@ width: 40px; } +.spaces-table-actions-disabled, .spaces-table-actions-disabled:link, .spaces-table-actions-disabled:visited, .spaces-table-actions-disabled:hover, .spaces-table-actions-disabled:active, .spaces-table-actions-disabled:focus { + color: gray; + text-decoration: none; + cursor: default; +} + td.spaces-table-created { text-align: center; } @@ -247,6 +253,27 @@ width: 650px; height: 150px; } + #binConfigDiv div { margin-bottom: 0; } + +#space-delete-container { + margin-left: 12px; + margin-top: 1em; +} + + +#space-delete-confirm-buttons { + margin-top: 1em; +} + +#space-delete-confirm-buttons form { + margin: 0; + padding: 0; + display: inline; +} + +#space-delete-confirm-buttons input[type=submit] { + margin-right: 12px; +} diff -r 93a1fbe6a848 -r 01c862ada33c server/src/test/java/org/iri_research/renkan/test/controller/AdminControllerTest.java --- a/server/src/test/java/org/iri_research/renkan/test/controller/AdminControllerTest.java Tue Apr 02 14:05:56 2013 +0200 +++ b/server/src/test/java/org/iri_research/renkan/test/controller/AdminControllerTest.java Wed Apr 03 17:47:23 2013 +0200 @@ -1,5 +1,6 @@ package org.iri_research.renkan.test.controller; +import java.security.SecureRandom; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; @@ -7,7 +8,10 @@ import java.util.Map; import java.util.UUID; +import org.apache.commons.codec.binary.Hex; +import org.iri_research.renkan.models.Project; import org.iri_research.renkan.models.Space; +import org.iri_research.renkan.repositories.ProjectsRepository; import org.iri_research.renkan.repositories.SpacesRepository; import org.junit.After; import org.junit.Assert; @@ -17,6 +21,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.web.WebAppConfiguration; @@ -26,7 +31,9 @@ import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import org.springframework.test.web.servlet.result.MockMvcResultMatchers; import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.client.HttpClientErrorException; import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.util.NestedServletException; @RunWith(SpringJUnit4ClassRunner.class) @WebAppConfiguration @@ -39,10 +46,14 @@ @Autowired private SpacesRepository spacesRepository; + @Autowired + private ProjectsRepository projectsRepository; private Map spacesList = new HashMap(SPACE_NB); private List spacesUuids = new ArrayList<>(SPACE_NB); + private ArrayList testProjects = new ArrayList(); + @Autowired private WebApplicationContext context; private MockMvc mvc; @@ -53,6 +64,9 @@ logger.debug("Setup"); spacesRepository.deleteAll(); + projectsRepository.deleteAll(); + + ArrayList pl = new ArrayList(); for(int i=0;i model = res.getModelAndView().getModel(); + + Space space = (Space)model.get("spaceObj"); + Assert.assertNotNull("Space is not null", space); + Assert.assertEquals("Must be first space id", this.spacesUuids.get(SPACE_NB-1), space.getId()); + + String key = (String)model.get("key"); + Assert.assertNotNull("key is not null", key); + + String salt = (String)model.get("salt"); + Assert.assertNotNull("salt is not null", salt); + + Assert.assertTrue("Key must be checked", space.checkKey(key, salt)); + + } + + @Test + public void testDeleteFakeSpace() throws Exception { + + MockHttpServletRequestBuilder get = MockMvcRequestBuilders.get("/admin/spaces/delete/" + UUID.randomUUID().toString()); + + try { + this.mvc.perform(get) + .andExpect(MockMvcResultMatchers.status().isNotFound()); + } + catch(NestedServletException e) { + Assert.assertNotNull("Nested exception must not be null", e.getCause()); + Assert.assertEquals("Inner exception must be a HttpClientErrorException", HttpClientErrorException.class, e.getCause().getClass()); + Assert.assertEquals("Exception error status must be not found", HttpStatus.NOT_FOUND, ((HttpClientErrorException)e.getCause()).getStatusCode()); + } + + } - @After - public void teardown() { - spacesRepository.deleteAll(); + @Test + public void testDeleteSpaceProject() throws Exception { + + MockHttpServletRequestBuilder get = MockMvcRequestBuilders.get("/admin/spaces/delete/" + this.spacesUuids.get(0)); + + try { + this.mvc.perform(get) + .andExpect(MockMvcResultMatchers.status().isBadRequest()); + } + catch(NestedServletException e) { + Assert.assertNotNull("Nested exception must not be null", e.getCause()); + Assert.assertEquals("Inner exception must be a HttpClientErrorException", HttpClientErrorException.class, e.getCause().getClass()); + Assert.assertEquals("Exception error status must be not found", HttpStatus.BAD_REQUEST, ((HttpClientErrorException)e.getCause()).getStatusCode()); + } + + } + + @Test + public void testDoDeleteSpaceNoKey() throws Exception { + MockHttpServletRequestBuilder post = MockMvcRequestBuilders.post("/admin/spaces/delete/"+this.spacesUuids.get(SPACE_NB-1)); + + try { + this.mvc.perform(post) + .andExpect(MockMvcResultMatchers.status().isBadRequest()); + } + catch(NestedServletException e) { + Assert.assertNotNull("Nested exception must not be null", e.getCause()); + Assert.assertEquals("Inner exception must be a HttpClientErrorException", HttpClientErrorException.class, e.getCause().getClass()); + Assert.assertEquals("Exception error status must be not found", HttpStatus.BAD_REQUEST, ((HttpClientErrorException)e.getCause()).getStatusCode()); + } + + + Assert.assertEquals("Must have same nb of space", SPACE_NB, this.spacesRepository.count()); + + } + + @Test + public void testDoDeleteSpace() throws Exception { + + Space space = this.spacesList.get(this.spacesUuids.get(SPACE_NB-1)); + + SecureRandom rand = SecureRandom.getInstance("SHA1PRNG"); + rand.setSeed(System.currentTimeMillis()); + byte[] rawSalt = new byte[50]; + rand.nextBytes(rawSalt); + String salt = Hex.encodeHexString(rawSalt); + String key = space.getKey(salt); + + + MockHttpServletRequestBuilder post = MockMvcRequestBuilders.post(String.format("/admin/spaces/delete/%s?key=%s&salt=%s",this.spacesUuids.get(SPACE_NB-1), key, salt)); + + this.mvc.perform(post) + .andExpect(MockMvcResultMatchers.status().isSeeOther()) + .andExpect(MockMvcResultMatchers.redirectedUrl("/admin/spaces")); + + Assert.assertEquals("Must have one less space", SPACE_NB-1, this.spacesRepository.count()); + + space = this.spacesRepository.findOne(this.spacesUuids.get(SPACE_NB-1)); + + Assert.assertNull("Space " + this.spacesUuids.get(SPACE_NB-1) + " deleted", space); + + } + + + @Test + public void testDoDeleteSpaceFake() throws Exception { + + Space space = this.spacesList.get(this.spacesUuids.get(SPACE_NB-1)); + + SecureRandom rand = SecureRandom.getInstance("SHA1PRNG"); + rand.setSeed(System.currentTimeMillis()); + byte[] rawSalt = new byte[50]; + rand.nextBytes(rawSalt); + String salt = Hex.encodeHexString(rawSalt); + String key = space.getKey(salt); + + + MockHttpServletRequestBuilder post = MockMvcRequestBuilders.post(String.format("/admin/spaces/delete/%s?key=%s&salt=%s",UUID.randomUUID(), key, salt)); + + this.mvc.perform(post) + .andExpect(MockMvcResultMatchers.status().isSeeOther()) + .andExpect(MockMvcResultMatchers.redirectedUrl("/admin/spaces")); + + Assert.assertEquals("Must have the same nb of space", SPACE_NB, this.spacesRepository.count()); + + } + + @Test + public void testDoDeleteSpaceProject() throws Exception { + + Space space = this.spacesList.get(this.spacesUuids.get(0)); + + SecureRandom rand = SecureRandom.getInstance("SHA1PRNG"); + rand.setSeed(System.currentTimeMillis()); + byte[] rawSalt = new byte[50]; + rand.nextBytes(rawSalt); + String salt = Hex.encodeHexString(rawSalt); + String key = space.getKey(salt); + + + MockHttpServletRequestBuilder post = MockMvcRequestBuilders.post(String.format("/admin/spaces/delete/%s?key=%s&salt=%s",this.spacesUuids.get(0), key, salt)); + + try { + this.mvc.perform(post) + .andExpect(MockMvcResultMatchers.status().isBadRequest()); + } + catch(NestedServletException e) { + Assert.assertNotNull("Nested exception must not be null", e.getCause()); + Assert.assertEquals("Inner exception must be a HttpClientErrorException", HttpClientErrorException.class, e.getCause().getClass()); + Assert.assertEquals("Exception error status must be not found", HttpStatus.BAD_REQUEST, ((HttpClientErrorException)e.getCause()).getStatusCode()); + } + + Assert.assertEquals("Must have the same nb of space", SPACE_NB, this.spacesRepository.count()); + } } diff -r 93a1fbe6a848 -r 01c862ada33c server/src/test/java/org/iri_research/renkan/test/repositories/ProjectsRepositoryTest.java --- a/server/src/test/java/org/iri_research/renkan/test/repositories/ProjectsRepositoryTest.java Tue Apr 02 14:05:56 2013 +0200 +++ b/server/src/test/java/org/iri_research/renkan/test/repositories/ProjectsRepositoryTest.java Wed Apr 03 17:47:23 2013 +0200 @@ -2,6 +2,7 @@ import java.util.ArrayList; +import java.util.Arrays; import java.util.Date; import java.util.List; import java.util.Map; @@ -206,6 +207,25 @@ } @Test + public void testGetCountBySpaceFilter() { + + List spacesIdsFilter = Arrays.asList(this.spaceIds.get(0)); + + Map groupRes = projectsRepository.getCountBySpace(spacesIdsFilter); + + Assert.assertNotNull("GroupRes not null", groupRes); + Assert.assertEquals("Group res size", 1, groupRes.size()); + + Integer count = groupRes.get(this.spaceIds.get(0)); + Assert.assertNotNull("count not null", count); + Assert.assertEquals("Nb of project/space", 2, count.intValue()); + + for(int i=1; i