Skip to content

Commit ca1e956

Browse files
authored
Create a strong password for the reviewer account (#390)
- Create a strong password for the reviewer account.
1 parent 85aecb9 commit ca1e956

File tree

4 files changed

+125
-17
lines changed

4 files changed

+125
-17
lines changed

panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicModule.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
import org.labkey.panoramapublic.catalog.CatalogImageAttachmentType;
4646
import org.labkey.panoramapublic.model.Journal;
4747
import org.labkey.panoramapublic.model.speclib.SpecLibKey;
48+
import org.labkey.panoramapublic.pipeline.CopyExperimentFinalTask;
4849
import org.labkey.panoramapublic.pipeline.CopyExperimentPipelineProvider;
4950
import org.labkey.panoramapublic.pipeline.PxValidationPipelineProvider;
5051
import org.labkey.panoramapublic.proteomexchange.ExperimentModificationGetter;

panoramapublic/src/org/labkey/panoramapublic/PanoramaPublicNotification.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -321,12 +321,12 @@ private static void appendUserDetails(User user, String userType, StringBuilder
321321

322322
public static String getUserName(User user)
323323
{
324-
return !StringUtils.isBlank(user.getFullName()) ? user.getFullName() : user.getFriendlyName();
324+
return escape(!StringUtils.isBlank(user.getFullName()) ? user.getFullName() : user.getFriendlyName());
325325
}
326326

327327
private static String getUserDetails(User user)
328328
{
329-
return escape(getUserName(user)) + " (" + escape(user.getEmail()) + ")";
329+
return getUserName(user) + escape(" (" + user.getEmail() + ")");
330330
}
331331

332332
private static String getContainerLink(Container container)
@@ -385,7 +385,7 @@ public static String getExperimentCopiedMessageBody(ExperimentAnnotations source
385385
User fromUser,
386386
boolean recopy)
387387
{
388-
String journalName = journal.getName();
388+
String journalName = escape(journal.getName());
389389

390390
String accessUrlLink = link(targetExperiment.getShortUrl().renderShortURL());
391391

@@ -405,7 +405,7 @@ public static String getExperimentCopiedMessageBody(ExperimentAnnotations source
405405
message.append(NL2)
406406
.append("As requested, your data on ").append(journalName).append(" is private. Here are the reviewer account details:")
407407
.append(NL).append("Email: ").append(reviewer.getEmail())
408-
.append(NL).append("Password: ").append(reviewerPassword);
408+
.append(NL).append("Password: ").append(escape(reviewerPassword));
409409
}
410410
else
411411
{

panoramapublic/src/org/labkey/panoramapublic/pipeline/CopyExperimentFinalTask.java

Lines changed: 99 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
*/
1616
package org.labkey.panoramapublic.pipeline;
1717

18-
import org.apache.commons.lang3.RandomStringUtils;
1918
import org.apache.commons.lang3.StringUtils;
2019
import org.apache.logging.log4j.Logger;
2120
import org.jetbrains.annotations.NotNull;
@@ -38,6 +37,7 @@
3837
import org.labkey.api.security.InvalidGroupMembershipException;
3938
import org.labkey.api.security.MemberType;
4039
import org.labkey.api.security.MutableSecurityPolicy;
40+
import org.labkey.api.security.PasswordRule;
4141
import org.labkey.api.security.SecurityManager;
4242
import org.labkey.api.security.SecurityPolicy;
4343
import org.labkey.api.security.SecurityPolicyManager;
@@ -81,6 +81,8 @@
8181
import java.io.IOException;
8282
import java.nio.file.Files;
8383
import java.nio.file.Path;
84+
import java.security.SecureRandom;
85+
import java.util.ArrayList;
8486
import java.util.Arrays;
8587
import java.util.Collections;
8688
import java.util.Date;
@@ -145,7 +147,7 @@ private void finishUp(PipelineJob job, CopyExperimentJobSupport jobSupport) thro
145147
ExperimentAnnotations previousCopy = getPreviousCopyRemoveShortUrl(latestCopiedSubmission, user);
146148

147149
// Update the row in panoramapublic.ExperimentAnnotations - set the shortURL and version
148-
ExperimentAnnotations targetExperiment = updateExperimentAnnotations(container, sourceExperiment, js, previousCopy, jobSupport, user, log);
150+
ExperimentAnnotations targetExperiment = updateExperimentAnnotations(container, sourceExperiment, js, user, log);
149151

150152
// If there is a Panorama Public data catalog entry associated with the previous copy of the experiment, move it to the
151153
// new container.
@@ -340,11 +342,10 @@ private Pair<User, String> assignReviewer(JournalSubmission js, ExperimentAnnota
340342
{
341343
if (previousCopy == null || previousCopy.isPublic())
342344
{
343-
String reviewerPassword = createPassword();
344-
User reviewer;
345+
ReviewerAndPassword reviewerAndPassword;
345346
try
346347
{
347-
reviewer = createReviewerAccount(jobSupport.getReviewerEmailPrefix(), reviewerPassword, user, log);
348+
reviewerAndPassword = createReviewerAccount(jobSupport.getReviewerEmailPrefix(), user, log);
348349
}
349350
catch (ValidEmail.InvalidEmailException e)
350351
{
@@ -354,6 +355,7 @@ private Pair<User, String> assignReviewer(JournalSubmission js, ExperimentAnnota
354355
{
355356
throw new PipelineJobException("Error creating a new account for reviewer", e);
356357
}
358+
User reviewer = reviewerAndPassword.getReviewer();
357359
assignReader(reviewer, targetExperiment.getContainer(), user);
358360
js.getJournalExperiment().setReviewer(reviewer.getUserId());
359361
SubmissionManager.updateJournalExperiment(js.getJournalExperiment(), user);
@@ -363,7 +365,7 @@ private Pair<User, String> assignReviewer(JournalSubmission js, ExperimentAnnota
363365
// CONSIDER: Make this configurable through the Panorama Public admin console.
364366
addToGroup(reviewer, "Reviewers", targetExperiment.getContainer().getProject(), log);
365367

366-
return new Pair<>(reviewer, reviewerPassword);
368+
return new Pair<>(reviewer, reviewerAndPassword.getPassword());
367369
}
368370
}
369371
else
@@ -463,10 +465,8 @@ private ExperimentAnnotations getPreviousCopyRemoveShortUrl(Submission latestCop
463465

464466
@NotNull
465467
private ExperimentAnnotations updateExperimentAnnotations(Container targetContainer, ExperimentAnnotations sourceExperiment, JournalSubmission js,
466-
ExperimentAnnotations previousCopy, CopyExperimentJobSupport jobSupport,
467468
User user, Logger log) throws PipelineJobException
468469
{
469-
470470
log.info("Updating TargetedMS experiment entry in target folder " + targetContainer.getPath());
471471
ExperimentAnnotations targetExperiment = ExperimentAnnotationsManager.getExperimentInContainer(targetContainer);
472472
if (targetExperiment == null)
@@ -625,7 +625,7 @@ private Set<User> getUsersWithRole(Container container, Role role)
625625

626626
}
627627

628-
private User createReviewerAccount(String reviewerEmailPrefix, String password, User user, Logger log) throws ValidEmail.InvalidEmailException, SecurityManager.UserManagementException
628+
private ReviewerAndPassword createReviewerAccount(String reviewerEmailPrefix, User user, Logger log) throws ValidEmail.InvalidEmailException, SecurityManager.UserManagementException
629629
{
630630
if(StringUtils.isBlank(reviewerEmailPrefix))
631631
{
@@ -644,10 +644,14 @@ private User createReviewerAccount(String reviewerEmailPrefix, String password,
644644

645645
log.info("Creating a reviewer account.");
646646
SecurityManager.NewUserStatus newUser = SecurityManager.addUser(email, user, true);
647+
log.info("Created reviewer with email: " + newUser.getUser().getEmail());
648+
649+
log.info("Generating password.");
650+
String password = createPassword(newUser.getUser());
647651
SecurityManager.setPassword(email, password);
652+
log.info("Set reviewer password successfully.");
648653

649-
log.info("Created reviewer with email: User " + newUser.getUser().getEmail());
650-
return newUser.getUser();
654+
return new ReviewerAndPassword(newUser.getUser(), password);
651655
}
652656

653657
private void assignReader(UserPrincipal reader, Container target, User pipelineJobUser)
@@ -657,9 +661,91 @@ private void assignReader(UserPrincipal reader, Container target, User pipelineJ
657661
SecurityPolicyManager.savePolicy(newPolicy, pipelineJobUser);
658662
}
659663

660-
public static String createPassword()
664+
private static class ReviewerAndPassword
665+
{
666+
private final User _reviewer;
667+
private final String _password;
668+
669+
public ReviewerAndPassword(@NotNull User reviewer, @NotNull String password)
670+
{
671+
_reviewer = reviewer;
672+
_password = password;
673+
}
674+
675+
public User getReviewer()
676+
{
677+
return _reviewer;
678+
}
679+
680+
public String getPassword()
681+
{
682+
return _password;
683+
}
684+
}
685+
686+
private static String createPassword(User user)
687+
{
688+
String password;
689+
do {
690+
password = PasswordGenerator.generate();
691+
} while (!PasswordRule.Strong.isValidForLogin(password, user, null));
692+
693+
return password;
694+
}
695+
696+
private static class PasswordGenerator
661697
{
662-
return RandomStringUtils.randomAlphabetic(8);
698+
private static final List<Character> LOWERCASE = "abcdefghijklmnopqrstuvwxyz".chars().mapToObj(c -> (char)c).collect(Collectors.toList());
699+
private static final List<Character> UPPERCASE = "ABCDEFGHIJKLMNOPQRSTUVWXYZ".chars().mapToObj(c -> (char)c).collect(Collectors.toList());
700+
private static final List<Character> DIGITS = "0123456789".chars().mapToObj(c -> (char)c).collect(Collectors.toList());
701+
private static final List<Character> SYMBOLS = "!@#$%^&*+=?".chars().mapToObj(c -> (char)c).collect(Collectors.toList());
702+
703+
private static final int PASSWORD_LEN = 14;
704+
705+
public static String generate()
706+
{
707+
SecureRandom random = new SecureRandom();
708+
709+
List<Character> passwordChars = new ArrayList<>(PASSWORD_LEN);
710+
List<Character> allChars = new ArrayList<>();
711+
712+
// Initialize the list with all possible characters
713+
allChars.addAll(LOWERCASE);
714+
allChars.addAll(UPPERCASE);
715+
allChars.addAll(DIGITS);
716+
allChars.addAll(SYMBOLS);
717+
718+
// Ensure that there is at least one character from each character category.
719+
addChar(LOWERCASE, passwordChars, allChars, random);
720+
addChar(UPPERCASE, passwordChars, allChars, random);
721+
addChar(DIGITS, passwordChars, allChars, random);
722+
addChar(SYMBOLS, passwordChars, allChars, random);
723+
724+
// Shuffle the list of remaining characters
725+
Collections.shuffle(allChars);
726+
727+
// Add more characters until we are at the desired password length
728+
while (passwordChars.size() < PASSWORD_LEN)
729+
{
730+
addChar(allChars, passwordChars, allChars, random);
731+
}
732+
733+
Collections.shuffle(passwordChars);
734+
735+
return passwordChars.stream().map(String::valueOf).collect(Collectors.joining());
736+
}
737+
738+
/**
739+
* Pick a random character from the given character category, add it to the list of password characters, and
740+
* remove it from the list of all available characters to ensure character uniqueness in the password.
741+
*/
742+
private static void addChar(List<Character> categoryChars, List<Character> passwordChars, List<Character> allChars, SecureRandom random)
743+
{
744+
int randomIdx = random.nextInt(0, categoryChars.size());
745+
Character selected = categoryChars.get(randomIdx);
746+
passwordChars.add(selected);
747+
allChars.remove(selected); // Remove from the list of all available chars so that we have unique characters in the password
748+
}
663749
}
664750

665751
private void assignPxId(ExperimentAnnotations targetExpt, boolean useTestDb) throws ProteomeXchangeServiceException

panoramapublic/test/src/org/labkey/test/tests/panoramapublic/PanoramaPublicBaseTest.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,12 @@
3737
import java.util.ArrayList;
3838
import java.util.Collections;
3939
import java.util.List;
40+
import java.util.regex.Matcher;
41+
import java.util.regex.Pattern;
4042

4143
import static org.junit.Assert.assertEquals;
4244
import static org.junit.Assert.assertNotNull;
45+
import static org.junit.Assert.assertTrue;
4346

4447
public class PanoramaPublicBaseTest extends TargetedMSTest implements PostgresOnlyTest
4548
{
@@ -75,6 +78,10 @@ public class PanoramaPublicBaseTest extends TargetedMSTest implements PostgresOn
7578

7679
static final String SAMPLEDATA_FOLDER = "panoramapublic/";
7780

81+
private static final Pattern REVIEWER_PASSWORD_LINE = Pattern.compile("Password:\s(\\S+)");
82+
private static final String PASSWORD_SPECIAL_CHARS = "!@#$%^&*+=?";
83+
private static final int REVIEWER_PASSWORD_LEN = 14;
84+
7885
PortalHelper portalHelper = new PortalHelper(this);
7986

8087
@Override
@@ -439,6 +446,20 @@ private void verifyCopy(String shortAccessUrl, String experimentTitle, @Nullable
439446
String messageText = new BodyWebPart(getDriver(), "View Message").getComponentElement().getText();
440447
var srcFolderTxt = "Source folder: " + "/" + projectName + "/" + folderName;
441448
assertTextPresent(new TextSearcher(messageText), text, srcFolderTxt);
449+
450+
// Unescaped special Markdown characters in the message may cause the password to render incorrectly.
451+
// Extract the reviewer password and check that it has the correct length and expected characters.
452+
Matcher match = REVIEWER_PASSWORD_LINE.matcher(messageText);
453+
assertTrue("Could not find reviewer password in the message", match.find());
454+
String password = match.group(1);
455+
assertEquals("Unexpected length of reviewer password", REVIEWER_PASSWORD_LEN, password.length());
456+
for (int i = 0; i < password.length(); i++)
457+
{
458+
char c = password.charAt(i);
459+
assertTrue("Unexpected character '"+ c + "' in reviewer password " + password,
460+
Character.isUpperCase(c) || Character.isLowerCase(c)
461+
|| Character.isDigit(c) || PASSWORD_SPECIAL_CHARS.contains(String.valueOf(c)));
462+
}
442463
}
443464

444465
@Override

0 commit comments

Comments
 (0)