diff --git a/rm-server/config/alfresco/module/org_alfresco_module_rm/classification/rm-classification-levels.json b/rm-server/config/alfresco/module/org_alfresco_module_rm/classification/rm-classification-levels.json index bb61285975..a605b07857 100644 --- a/rm-server/config/alfresco/module/org_alfresco_module_rm/classification/rm-classification-levels.json +++ b/rm-server/config/alfresco/module/org_alfresco_module_rm/classification/rm-classification-levels.json @@ -1,14 +1,14 @@ [ { - "name" : "TopSecret", + "name" : "TS", "displayLabel" : "rm.classification.topSecret" }, { - "name" : "Secret", + "name" : "S", "displayLabel" : "rm.classification.secret" }, { - "name" : "Confidential", + "name" : "C", "displayLabel" : "rm.classification.confidential" } ] diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/classification/ClassificationException.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/classification/ClassificationException.java index 0d271025b3..0ebe62ac61 100644 --- a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/classification/ClassificationException.java +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/classification/ClassificationException.java @@ -21,6 +21,9 @@ package org.alfresco.module.org_alfresco_module_rm.classification; import org.alfresco.error.AlfrescoRuntimeException; import org.alfresco.service.cmr.repository.NodeRef; +import java.util.Collections; +import java.util.List; + /** * Generic class for any runtime exception to do with classified records. * @@ -54,6 +57,22 @@ public class ClassificationException extends AlfrescoRuntimeException public IllegalConfiguration(String msgId) { super(msgId); } } + /** Represents a fatal error due to illegal {@link ClassificationLevel#getId() classification level ID} configuration. + * The configuration was understood by the server, but was rejected as illegal. */ + public static class IllegalAbbreviationChars extends IllegalConfiguration + { + /** serial version uid */ + private static final long serialVersionUID = 98787676565465454L; + + private final List illegalChars; + public IllegalAbbreviationChars(String msgId, List illegalChars) + { + super(msgId); + this.illegalChars = illegalChars; + } + public List getIllegalChars() { return Collections.unmodifiableList(illegalChars); } + } + /** Represents a fatal error due to malformed configuration. * The configuration could not be understood by the server. */ public static class MalformedConfiguration extends ClassificationException diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/classification/ClassificationLevelValidation.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/classification/ClassificationLevelValidation.java new file mode 100644 index 0000000000..31abbda3f1 --- /dev/null +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/classification/ClassificationLevelValidation.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2005-2015 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.module.org_alfresco_module_rm.classification; + +import static java.util.Arrays.asList; + +import org.alfresco.module.org_alfresco_module_rm.classification.ClassificationException.IllegalAbbreviationChars; +import org.alfresco.module.org_alfresco_module_rm.classification.ClassificationException.IllegalConfiguration; +import org.alfresco.module.org_alfresco_module_rm.classification.ClassificationException.MissingConfiguration; + +import java.util.ArrayList; +import java.util.List; + +/** + * This class is responsible for validating {@link ClassificationLevel}s. + * + * @author Neil Mc Erlean + * @since 3.0 + */ +public class ClassificationLevelValidation +{ + /** The maximum number of chatacters allowed in a {@link ClassificationLevel#getId() level ID}. */ + private static final int ABBREVIATION_LENGTH_LIMIT = 10; + + /** + * Illegal characters in a {@link ClassificationLevel#getId() level ID}. + * Equals to Alfresco's disallowed filename characters. + */ + // See in contentModel.xml + static final List ILLEGAL_ABBREVIATION_CHARS = asList('"', '*', '\\', '>', '<', '?', '/', ':', '|'); + + /** + * Validates the provided {@link ClassificationLevel}. + * @param level the level to validate. + * @throws MissingConfiguration if the level abbreviation is missing. + * @throws IllegalConfiguration if the level abbreviation violates the standard restrictions. + * @throws IllegalAbbreviationChars if the level abbreviation contains illegal characters. + */ + void validateLevel(ClassificationLevel level) + { + final String levelId = level.getId(); + + if (levelId == null || levelId.equals("")) + { + throw new MissingConfiguration("Classification level ID is missing."); + } + else if (levelId.length() > ABBREVIATION_LENGTH_LIMIT) + { + throw new IllegalConfiguration("Illegal classification level abbreviation. Length " + + levelId.length() + " > " + ABBREVIATION_LENGTH_LIMIT); + } + else if (!getIllegalAbbreviationChars(levelId).isEmpty()) + { + throw new IllegalAbbreviationChars("Illegal character(s) in level abbreviation", + getIllegalAbbreviationChars(levelId)); + } + } + + /** + * Validates the provided {@link ClassificationLevel}s as a whole and individually. + * @param levels the levels to validate. + * @throws MissingConfiguration if the levels or any of their abbreviations are missing. + * @throws IllegalConfiguration if any of the level abbreviations violate the standard restrictions. + * @throws IllegalAbbreviationChars if the level abbreviation contains illegal characters. + */ + public void validateLevels(List levels) + { + if (levels == null || levels.isEmpty()) + { + throw new MissingConfiguration("Classification level configuration is missing."); + } + + for (ClassificationLevel level : levels) + { + validateLevel(level); + } + } + + private List getIllegalAbbreviationChars(String s) + { + final List result = new ArrayList<>(); + for (Character c : ILLEGAL_ABBREVIATION_CHARS) + { + if (s.contains(c.toString())) result.add(c); + } + return result; + } +} diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/classification/ClassificationServiceBootstrap.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/classification/ClassificationServiceBootstrap.java index 0070dd882b..c92fcf2051 100644 --- a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/classification/ClassificationServiceBootstrap.java +++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/classification/ClassificationServiceBootstrap.java @@ -56,6 +56,7 @@ public class ClassificationServiceBootstrap extends AbstractLifecycleBean implem /** The clearance levels currently configured in this server. */ private ClearanceLevelManager clearanceLevelManager = new ClearanceLevelManager(); private ClassificationServiceDAO classificationServiceDAO; + private final ClassificationLevelValidation levelValidation = new ClassificationLevelValidation(); public ClassificationServiceBootstrap(AuthenticationUtil authUtil, TransactionService txService, @@ -113,11 +114,9 @@ public class ClassificationServiceBootstrap extends AbstractLifecycleBean implem LOGGER.debug("Persisted classification levels: {}", loggableStatusOf(allPersistedLevels)); LOGGER.debug("Classpath classification levels: {}", loggableStatusOf(configurationLevels)); - if (configurationLevels == null || configurationLevels.isEmpty()) - { - throw new MissingConfiguration("Classification level configuration is missing."); - } - else if (!configurationLevels.equals(allPersistedLevels)) + levelValidation.validateLevels(configurationLevels); + + if (!configurationLevels.equals(allPersistedLevels)) { attributeService.setAttribute((Serializable) configurationLevels, LEVELS_KEY); this.classificationLevelManager.setClassificationLevels(configurationLevels); diff --git a/rm-server/unit-test/java/org/alfresco/module/org_alfresco_module_rm/classification/ClassificationLevelValidationUnitTest.java b/rm-server/unit-test/java/org/alfresco/module/org_alfresco_module_rm/classification/ClassificationLevelValidationUnitTest.java new file mode 100644 index 0000000000..4b5fd0d3b4 --- /dev/null +++ b/rm-server/unit-test/java/org/alfresco/module/org_alfresco_module_rm/classification/ClassificationLevelValidationUnitTest.java @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2005-2015 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ +package org.alfresco.module.org_alfresco_module_rm.classification; + +import static java.util.Arrays.asList; +import static org.alfresco.module.org_alfresco_module_rm.classification.ClassificationLevelValidation.ILLEGAL_ABBREVIATION_CHARS; +import static org.alfresco.module.org_alfresco_module_rm.test.util.ExceptionUtils.expectedException; +import static org.hamcrest.CoreMatchers.allOf; +import static org.hamcrest.core.IsCollectionContaining.hasItem; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; + +import org.alfresco.module.org_alfresco_module_rm.classification.ClassificationException.IllegalAbbreviationChars; +import org.alfresco.module.org_alfresco_module_rm.classification.ClassificationException.IllegalConfiguration; +import org.alfresco.module.org_alfresco_module_rm.classification.ClassificationException.MissingConfiguration; +import org.hamcrest.Matcher; +import org.junit.Test; + +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Unit tests for the {@link ClassificationLevelValidation}. + * + * @author Neil Mc Erlean + */ +public class ClassificationLevelValidationUnitTest +{ + private final ClassificationLevelValidation validation = new ClassificationLevelValidation(); + + @Test(expected=MissingConfiguration.class) + public void classificationLevelsAreRequired() + { + validation.validateLevels(Collections.emptyList()); + } + + @Test public void nonEmptyAbbreviationsAreMandatory() + { + // A missing or empty level ID is illegal. + for (String illegalID : asList(null, "", " ", "\t")) + { + expectedException(IllegalArgumentException.class, () -> + { + validation.validateLevel(new ClassificationLevel(illegalID, "value.does.not.matter")); + return null; + }); + } + } + + @Test(expected=IllegalConfiguration.class) + public void longAbbreviationsAreIllegal() + { + validation.validateLevel(new ClassificationLevel("12345678901", "value.does.not.matter")); + } + + /** + * This test ensures that validation will catch any and all illegal characters in a + * {@link ClassificationLevel#getId() level ID} and report them all. + */ + @Test public void someCharactersAreBannedInAbbreviations() + { + for (Character illegalChar : ILLEGAL_ABBREVIATION_CHARS) + { + IllegalAbbreviationChars e = expectedException(IllegalAbbreviationChars.class, () -> + { + validation.validateLevel(new ClassificationLevel("Hello" + illegalChar, "value.does.not.matter")); + return null; + }); + assertTrue("Exception did not contain helpful example of illegal character", + e.getIllegalChars().contains(illegalChar)); + } + + // We also expect an abbreviation with multiple illegal chars in it to have them all reported in the exception. + final List someIllegalChars = ILLEGAL_ABBREVIATION_CHARS.subList(0, 3); + + IllegalAbbreviationChars e = expectedException(IllegalAbbreviationChars.class, () -> + { + validation.validateLevel(new ClassificationLevel(someIllegalChars.toString(), + "value.does.not.matter")); + return null; + }); + + // Construct a sequence of Matchers - one for each illegal char we expect to see. + // Apologies for the Java generics madness here. This is really a List of Matchers of List + List>> containsCharMatchers = someIllegalChars.stream() + .map(c -> hasItem(c)) + .collect(Collectors.toList()); + assertThat(e.getIllegalChars(), allOf(containsCharMatchers)); + } +} diff --git a/rm-server/unit-test/java/org/alfresco/module/org_alfresco_module_rm/classification/ClassificationServiceBootstrapUnitTest.java b/rm-server/unit-test/java/org/alfresco/module/org_alfresco_module_rm/classification/ClassificationServiceBootstrapUnitTest.java index b694542972..0f58d8605a 100644 --- a/rm-server/unit-test/java/org/alfresco/module/org_alfresco_module_rm/classification/ClassificationServiceBootstrapUnitTest.java +++ b/rm-server/unit-test/java/org/alfresco/module/org_alfresco_module_rm/classification/ClassificationServiceBootstrapUnitTest.java @@ -57,14 +57,14 @@ import org.mockito.MockitoAnnotations; */ public class ClassificationServiceBootstrapUnitTest { - private static final List DEFAULT_CLASSIFICATION_LEVELS = asLevelList("Top Secret", "rm.classification.topSecret", - "Secret", "rm.classification.secret", - "Confidential", "rm.classification.confidential", - "No Clearance", "rm.classification.noClearance"); - private static final List ALT_CLASSIFICATION_LEVELS = asLevelList("Board", "B", - "Executive Management", "EM", - "Employee", "E", - "Public", "P"); + private static final List DEFAULT_CLASSIFICATION_LEVELS = asLevelList("TS", "rm.classification.topSecret", + "S", "rm.classification.secret", + "C", "rm.classification.confidential", + "NC", "rm.classification.noClearance"); + private static final List ALT_CLASSIFICATION_LEVELS = asLevelList("B", "Board", + "EM", "ExecutiveManagement", + "E", "Employee", + "P", "Public"); private static final List PLACEHOLDER_CLASSIFICATION_REASONS = asList(new ClassificationReason("id1", "label1"), new ClassificationReason("id2", "label2")); private static final List ALTERNATIVE_CLASSIFICATION_REASONS = asList(new ClassificationReason("id8", "label8"),