Skip to content

Commit

Permalink
Merge pull request #327 from phoenixctms/file_active_permission
Browse files Browse the repository at this point in the history
access control for "approved" files/hyperlinks
  • Loading branch information
rkrenn authored Oct 11, 2024
2 parents ec6026f + e41abfa commit 21803b8
Show file tree
Hide file tree
Showing 23 changed files with 8,084 additions and 7,680 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,7 @@ public interface AuthorisationExceptionCodes {
public static final String PARAMETER_DISJUNCTIVE_RESTRICTION_NOT_SATISFIED = "parameter_disjunctive_restriction_not_satisfied";
public static final String PARAMETER_RESTRICTION_VIOLATED = "parameter_restriction_violated";
public final static String FILE_NOT_PUBLIC = "file_not_public";
public final static String FILE_NOT_ACTIVE = "file_not_active";
public final static String HYPERLINK_NOT_ACTIVE = "hyperlink_not_active";
public final static String ENCRYPTED_FILE = "encrypted_file";
}
33 changes: 18 additions & 15 deletions core/src/main/java/org/phoenixctms/ctsms/domain/FileDaoImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,19 @@ private final static void applyModuleIdCriterions(org.hibernate.Criteria criteri
}
}

private final static void applyActiveCriterion(org.hibernate.Criteria criteria, Boolean active) {
if (active != null) {
User user = CoreUtil.getUser();
if (user != null) {
criteria.add(Restrictions.or(
Restrictions.eq("active", active.booleanValue()),
Restrictions.eq("modifiedUser.id", user.getId().longValue())));
} else {
criteria.add(Restrictions.eq("active", active.booleanValue()));
}
}
}

private final static void applySubTreeCriterion(org.hibernate.Criteria criteria, boolean subTree, String logicalPath) {
if (logicalPath != null && logicalPath.length() > 0) {
logicalPath = CommonUtil.fixLogicalPathFolderName(logicalPath);
Expand Down Expand Up @@ -733,9 +746,7 @@ protected Collection<String> handleFindFileFolders(FileModule module,
if (useParentPath) {
fileCriteria.add(Restrictions.like("logicalPath", parentLogicalFolder, MatchMode.START));
}
if (active != null) {
fileCriteria.add(Restrictions.eq("active", active.booleanValue()));
}
applyActiveCriterion(fileCriteria, active);
if (publicFile != null) {
fileCriteria.add(Restrictions.eq("publicFile", publicFile.booleanValue()));
}
Expand Down Expand Up @@ -767,9 +778,7 @@ protected Collection<File> handleFindFiles(FileModule module, Long id, String lo
SubCriteriaMap criteriaMap = new SubCriteriaMap(File.class, fileCriteria);
applyModuleIdCriterions(fileCriteria, module, id);
applySubTreeCriterion(fileCriteria, subTree, logicalPath);
if (active != null) {
fileCriteria.add(Restrictions.eq("active", active.booleanValue()));
}
applyActiveCriterion(fileCriteria, active);
if (publicFile != null) {
fileCriteria.add(Restrictions.eq("publicFile", publicFile.booleanValue()));
}
Expand All @@ -784,9 +793,7 @@ protected long handleGetCount(FileModule module, Long id, String logicalPath, bo
org.hibernate.Criteria fileCriteria = createFileCriteria();
applyModuleIdCriterions(fileCriteria, module, id);
applySubTreeCriterion(fileCriteria, subTree, logicalPath);
if (active != null) {
fileCriteria.add(Restrictions.eq("active", active.booleanValue()));
}
applyActiveCriterion(fileCriteria, active);
if (publicFile != null) {
fileCriteria.add(Restrictions.eq("publicFile", publicFile.booleanValue()));
}
Expand All @@ -800,9 +807,7 @@ protected String handleGetCountSafe(FileModule module, Long id, String logicalPa
org.hibernate.Criteria fileCriteria = createFileCriteria();
applyModuleIdCriterions(fileCriteria, module, id);
applySubTreeCriterion(fileCriteria, subTree, logicalPath);
if (active != null) {
fileCriteria.add(Restrictions.eq("active", active.booleanValue()));
}
applyActiveCriterion(fileCriteria, active);
if (publicFile != null) {
fileCriteria.add(Restrictions.eq("publicFile", publicFile.booleanValue()));
}
Expand All @@ -826,9 +831,7 @@ protected long handleGetFileSizeSum(FileModule module, Long id, String logicalPa
org.hibernate.Criteria fileCriteria = createFileCriteria();
applyModuleIdCriterions(fileCriteria, module, id);
applySubTreeCriterion(fileCriteria, subTree, logicalPath);
if (active != null) {
fileCriteria.add(Restrictions.eq("active", active.booleanValue()));
}
applyActiveCriterion(fileCriteria, active);
if (publicFile != null) {
fileCriteria.add(Restrictions.eq("publicFile", publicFile.booleanValue()));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import org.phoenixctms.ctsms.enumeration.HyperlinkModule;
import org.phoenixctms.ctsms.query.CriteriaUtil;
import org.phoenixctms.ctsms.query.SubCriteriaMap;
import org.phoenixctms.ctsms.util.CoreUtil;
import org.phoenixctms.ctsms.vo.CourseOutVO;
import org.phoenixctms.ctsms.vo.HyperlinkCategoryVO;
import org.phoenixctms.ctsms.vo.HyperlinkInVO;
Expand Down Expand Up @@ -63,6 +64,19 @@ private static void applyModuleIdCriterions(SubCriteriaMap criteriaMap, Hyperlin
}
}

private final static void applyActiveCriterion(org.hibernate.Criteria criteria, Boolean active) {
if (active != null) {
User user = CoreUtil.getUser();
if (user != null) {
criteria.add(Restrictions.or(
Restrictions.eq("active", active.booleanValue()),
Restrictions.eq("modifiedUser.id", user.getId().longValue())));
} else {
criteria.add(Restrictions.eq("active", active.booleanValue()));
}
}
}

private org.hibernate.Criteria createHyperLinkCriteria() {
org.hibernate.Criteria hyperlinkCriteria = this.getSession().createCriteria(Hyperlink.class);
return hyperlinkCriteria;
Expand All @@ -73,9 +87,7 @@ protected Collection<Hyperlink> handleFindHyperlinks(
HyperlinkModule module, Long id, Boolean active, PSFVO psf) throws Exception {
org.hibernate.Criteria hyperlinkCriteria = createHyperLinkCriteria();
SubCriteriaMap criteriaMap = new SubCriteriaMap(Hyperlink.class, hyperlinkCriteria);
if (active != null) {
hyperlinkCriteria.add(Restrictions.eq("active", active.booleanValue()));
}
applyActiveCriterion(hyperlinkCriteria, active);
applyModuleIdCriterions(criteriaMap, module, id);
CriteriaUtil.applyPSFVO(criteriaMap, psf);
return hyperlinkCriteria.list();
Expand All @@ -85,9 +97,7 @@ protected Collection<Hyperlink> handleFindHyperlinks(
protected long handleGetCount(
HyperlinkModule module, Long id, Boolean active) throws Exception {
org.hibernate.Criteria hyperlinkCriteria = createHyperLinkCriteria();
if (active != null) {
hyperlinkCriteria.add(Restrictions.eq("active", active.booleanValue()));
}
applyActiveCriterion(hyperlinkCriteria, active);
applyModuleIdCriterions(hyperlinkCriteria, module, id);
return (Long) hyperlinkCriteria.setProjection(Projections.rowCount()).uniqueResult();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,17 @@
import org.phoenixctms.ctsms.domain.Trial;
import org.phoenixctms.ctsms.domain.TrialDao;
import org.phoenixctms.ctsms.domain.User;
import org.phoenixctms.ctsms.domain.UserPermissionProfileDao;
import org.phoenixctms.ctsms.enumeration.FileModule;
import org.phoenixctms.ctsms.enumeration.PermissionProfile;
import org.phoenixctms.ctsms.enumeration.PermissionProfileGroup;
import org.phoenixctms.ctsms.exception.AuthorisationException;
import org.phoenixctms.ctsms.exception.ServiceException;
import org.phoenixctms.ctsms.pdf.PDFMerger;
import org.phoenixctms.ctsms.security.CipherStream;
import org.phoenixctms.ctsms.security.CipherText;
import org.phoenixctms.ctsms.security.CryptoUtil;
import org.phoenixctms.ctsms.util.AuthorisationExceptionCodes;
import org.phoenixctms.ctsms.util.CheckIDUtil;
import org.phoenixctms.ctsms.util.CommonUtil;
import org.phoenixctms.ctsms.util.CoreUtil;
Expand Down Expand Up @@ -300,6 +305,69 @@ private void checkMimeType(String mimeType, FileModule module) throws ServiceExc
}
}

private void checkActivePermission(File file) throws AuthorisationException {
if (!file.isActive()) {
User user = CoreUtil.getUser();
if (!user.equals(file.getModifiedUser())) {
UserPermissionProfileDao userPermissionProfileDao = this.getUserPermissionProfileDao();
switch (file.getModule()) {
case INVENTORY_DOCUMENT:
if (!ServiceUtil.hasInheritedPermissionProfile(user, PermissionProfileGroup.INVENTORY, userPermissionProfileDao,
PermissionProfile.INVENTORY_MASTER_ALL_DEPARTMENTS,
PermissionProfile.INVENTORY_DETAIL_ALL_DEPARTMENTS,
PermissionProfile.INVENTORY_VIEW_ALL_DEPARTMENTS)) {
throw L10nUtil.initAuthorisationException(AuthorisationExceptionCodes.FILE_NOT_ACTIVE, file.getId().toString());
}
break;
case STAFF_DOCUMENT:
if (!ServiceUtil.hasInheritedPermissionProfile(user, PermissionProfileGroup.STAFF, userPermissionProfileDao,
PermissionProfile.STAFF_MASTER_ALL_DEPARTMENTS,
PermissionProfile.STAFF_DETAIL_ALL_DEPARTMENTS,
PermissionProfile.STAFF_VIEW_ALL_DEPARTMENTS)) {
throw L10nUtil.initAuthorisationException(AuthorisationExceptionCodes.FILE_NOT_ACTIVE, file.getId().toString());
}
break;
case COURSE_DOCUMENT:
if (!ServiceUtil.hasInheritedPermissionProfile(user, PermissionProfileGroup.COURSE, userPermissionProfileDao,
PermissionProfile.COURSE_MASTER_ALL_DEPARTMENTS,
PermissionProfile.COURSE_DETAIL_ALL_DEPARTMENTS,
PermissionProfile.COURSE_VIEW_ALL_DEPARTMENTS)) {
throw L10nUtil.initAuthorisationException(AuthorisationExceptionCodes.FILE_NOT_ACTIVE, file.getId().toString());
}
break;
case TRIAL_DOCUMENT:
if (!ServiceUtil.hasInheritedPermissionProfile(user, PermissionProfileGroup.TRIAL, userPermissionProfileDao,
PermissionProfile.TRIAL_MASTER_ALL_DEPARTMENTS,
PermissionProfile.TRIAL_DETAIL_ALL_DEPARTMENTS,
PermissionProfile.TRIAL_VIEW_ALL_DEPARTMENTS)) {
throw L10nUtil.initAuthorisationException(AuthorisationExceptionCodes.FILE_NOT_ACTIVE, file.getId().toString());
}
break;
case PROBAND_DOCUMENT:
if (!ServiceUtil.hasInheritedPermissionProfile(user, PermissionProfileGroup.PROBAND, userPermissionProfileDao,
PermissionProfile.PROBAND_MASTER_ALL_DEPARTMENTS,
PermissionProfile.PROBAND_DETAIL_ALL_DEPARTMENTS,
PermissionProfile.PROBAND_VIEW_ALL_DEPARTMENTS)) {
throw L10nUtil.initAuthorisationException(AuthorisationExceptionCodes.FILE_NOT_ACTIVE, file.getId().toString());
}
break;
case MASS_MAIL_DOCUMENT:
if (!ServiceUtil.hasInheritedPermissionProfile(user, PermissionProfileGroup.MASS_MAIL, userPermissionProfileDao,
PermissionProfile.MASS_MAIL_MASTER_ALL_DEPARTMENTS,
PermissionProfile.MASS_MAIL_DETAIL_ALL_DEPARTMENTS,
PermissionProfile.MASS_MAIL_VIEW_ALL_DEPARTMENTS)) {
throw L10nUtil.initAuthorisationException(AuthorisationExceptionCodes.FILE_NOT_ACTIVE, file.getId().toString());
}
break;
default:
// not supported for now...
throw new IllegalArgumentException(L10nUtil.getMessage(MessageCodes.UNSUPPORTED_FILE_MODULE, DefaultMessages.UNSUPPORTED_FILE_MODULE, new Object[] { file
.getModule().toString() }));
}
}
}
}

private FileOutVO createFile(File file, Timestamp now, User user) throws Exception {
FileDao fileDao = this.getFileDao();
JournalEntryDao journalEntryDao = this.getJournalEntryDao();
Expand Down Expand Up @@ -335,6 +403,7 @@ private FileOutVO createFile(File file, Timestamp now, User user) throws Excepti
private FileOutVO deleteFileHelper(Long fileId, Timestamp now, User user) throws Exception {
FileDao fileDao = this.getFileDao();
File file = CheckIDUtil.checkFileId(fileId, fileDao, LockMode.UPGRADE_NOWAIT);
checkActivePermission(file);
FileOutVO result = fileDao.toFileOutVO(file);
if (!result.isDecrypted()) {
throw L10nUtil.initServiceException(ServiceExceptionCodes.CANNOT_DECRYPT_FILE);
Expand Down Expand Up @@ -524,6 +593,7 @@ protected FileOutVO handleGetFile(AuthenticationVO auth, Long fileId)
throws Exception {
FileDao fileDao = this.getFileDao();
File file = CheckIDUtil.checkFileId(fileId, fileDao);
checkActivePermission(file);
FileOutVO result = fileDao.toFileOutVO(file);
return result;
}
Expand All @@ -536,6 +606,7 @@ protected FileContentOutVO handleGetFileContent(AuthenticationVO auth, Long file
throws Exception {
FileDao fileDao = this.getFileDao();
File file = CheckIDUtil.checkFileId(fileId, fileDao);
checkActivePermission(file);
checkContentSize(file);
FileContentOutVO result = fileDao.toFileContentOutVO(file);
return result;
Expand Down Expand Up @@ -584,6 +655,7 @@ protected FileStreamOutVO handleGetFileStream(AuthenticationVO auth, Long fileId
throws Exception {
FileDao fileDao = this.getFileDao();
File file = CheckIDUtil.checkFileId(fileId, fileDao);
checkActivePermission(file);
FileStreamOutVO result = fileDao.toFileStreamOutVO(file);
return result;
}
Expand All @@ -603,6 +675,7 @@ protected FileOutVO handleUpdateFile(AuthenticationVO auth, FileInVO modifiedFil
throws Exception {
FileDao fileDao = this.getFileDao();
File originalFile = CheckIDUtil.checkFileId(modifiedFile.getId(), fileDao, LockMode.UPGRADE_NOWAIT);
checkActivePermission(originalFile);
checkFileInput(modifiedFile);
if (!fileDao.toFileOutVO(originalFile).isDecrypted()) {
throw L10nUtil.initServiceException(ServiceExceptionCodes.CANNOT_DECRYPT_FILE);
Expand All @@ -625,6 +698,7 @@ protected FileOutVO handleUpdateFile(AuthenticationVO auth, FileInVO modifiedFil
throws Exception {
FileDao fileDao = this.getFileDao();
File originalFile = CheckIDUtil.checkFileId(modifiedFile.getId(), fileDao, LockMode.UPGRADE_NOWAIT);
checkActivePermission(originalFile);
checkFileInput(modifiedFile);
if (!fileDao.toFileOutVO(originalFile).isDecrypted()) {
throw L10nUtil.initServiceException(ServiceExceptionCodes.CANNOT_DECRYPT_FILE);
Expand All @@ -648,6 +722,7 @@ protected FileOutVO handleUpdateFile(AuthenticationVO auth, FileInVO modifiedFil
throws Exception {
FileDao fileDao = this.getFileDao();
File originalFile = CheckIDUtil.checkFileId(modifiedFile.getId(), fileDao, LockMode.UPGRADE_NOWAIT);
checkActivePermission(originalFile);
checkFileInput(modifiedFile);
if (!fileDao.toFileOutVO(originalFile).isDecrypted()) {
throw L10nUtil.initServiceException(ServiceExceptionCodes.CANNOT_DECRYPT_FILE);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,14 @@
import org.phoenixctms.ctsms.domain.Staff;
import org.phoenixctms.ctsms.domain.Trial;
import org.phoenixctms.ctsms.domain.User;
import org.phoenixctms.ctsms.domain.UserPermissionProfileDao;
import org.phoenixctms.ctsms.enumeration.HyperlinkModule;
import org.phoenixctms.ctsms.enumeration.JournalModule;
import org.phoenixctms.ctsms.enumeration.PermissionProfile;
import org.phoenixctms.ctsms.enumeration.PermissionProfileGroup;
import org.phoenixctms.ctsms.exception.AuthorisationException;
import org.phoenixctms.ctsms.exception.ServiceException;
import org.phoenixctms.ctsms.util.AuthorisationExceptionCodes;
import org.phoenixctms.ctsms.util.CheckIDUtil;
import org.phoenixctms.ctsms.util.CommonUtil;
import org.phoenixctms.ctsms.util.CoreUtil;
Expand Down Expand Up @@ -139,6 +144,53 @@ private void checkHyperlinkModuleId(HyperlinkModule module, Long id) throws Serv
}
}

private void checkActivePermission(Hyperlink hyperlink) throws AuthorisationException {
if (!hyperlink.isActive()) {
User user = CoreUtil.getUser();
if (!user.equals(hyperlink.getModifiedUser())) {
UserPermissionProfileDao userPermissionProfileDao = this.getUserPermissionProfileDao();
switch (hyperlink.getCategory().getModule()) {
case INVENTORY_HYPERLINK:
if (!ServiceUtil.hasInheritedPermissionProfile(user, PermissionProfileGroup.INVENTORY, userPermissionProfileDao,
PermissionProfile.INVENTORY_MASTER_ALL_DEPARTMENTS,
PermissionProfile.INVENTORY_DETAIL_ALL_DEPARTMENTS,
PermissionProfile.INVENTORY_VIEW_ALL_DEPARTMENTS)) {
throw L10nUtil.initAuthorisationException(AuthorisationExceptionCodes.HYPERLINK_NOT_ACTIVE, hyperlink.getId().toString());
}
break;
case STAFF_HYPERLINK:
if (!ServiceUtil.hasInheritedPermissionProfile(user, PermissionProfileGroup.STAFF, userPermissionProfileDao,
PermissionProfile.STAFF_MASTER_ALL_DEPARTMENTS,
PermissionProfile.STAFF_DETAIL_ALL_DEPARTMENTS,
PermissionProfile.STAFF_VIEW_ALL_DEPARTMENTS)) {
throw L10nUtil.initAuthorisationException(AuthorisationExceptionCodes.HYPERLINK_NOT_ACTIVE, hyperlink.getId().toString());
}
break;
case COURSE_HYPERLINK:
if (!ServiceUtil.hasInheritedPermissionProfile(user, PermissionProfileGroup.COURSE, userPermissionProfileDao,
PermissionProfile.COURSE_MASTER_ALL_DEPARTMENTS,
PermissionProfile.COURSE_DETAIL_ALL_DEPARTMENTS,
PermissionProfile.COURSE_VIEW_ALL_DEPARTMENTS)) {
throw L10nUtil.initAuthorisationException(AuthorisationExceptionCodes.HYPERLINK_NOT_ACTIVE, hyperlink.getId().toString());
}
break;
case TRIAL_HYPERLINK:
if (!ServiceUtil.hasInheritedPermissionProfile(user, PermissionProfileGroup.TRIAL, userPermissionProfileDao,
PermissionProfile.TRIAL_MASTER_ALL_DEPARTMENTS,
PermissionProfile.TRIAL_DETAIL_ALL_DEPARTMENTS,
PermissionProfile.TRIAL_VIEW_ALL_DEPARTMENTS)) {
throw L10nUtil.initAuthorisationException(AuthorisationExceptionCodes.HYPERLINK_NOT_ACTIVE, hyperlink.getId().toString());
}
break;
default:
// not supported for now...
throw new IllegalArgumentException(L10nUtil.getMessage(MessageCodes.UNSUPPORTED_HYPERLINK_MODULE, DefaultMessages.UNSUPPORTED_HYPERLINK_MODULE,
new Object[] { hyperlink.getCategory().getModule().toString() }));
}
}
}
}

/**
* @see org.phoenixctms.ctsms.service.shared.HyperlinkService#addHyperlink(HyperlinkInVO)
*/
Expand Down Expand Up @@ -183,6 +235,7 @@ protected HyperlinkOutVO handleDeleteHyperlink(AuthenticationVO auth, Long hyper
throws Exception {
HyperlinkDao hyperlinkDao = this.getHyperlinkDao();
Hyperlink hyperlink = CheckIDUtil.checkHyperlinkId(hyperlinkId, hyperlinkDao);
checkActivePermission(hyperlink);
HyperlinkOutVO result = hyperlinkDao.toHyperlinkOutVO(hyperlink);
Timestamp now = new Timestamp(System.currentTimeMillis());
User user = CoreUtil.getUser();
Expand Down Expand Up @@ -233,6 +286,7 @@ protected HyperlinkOutVO handleGetHyperlink(AuthenticationVO auth, Long hyperlin
throws Exception {
HyperlinkDao hyperlinkDao = this.getHyperlinkDao();
Hyperlink hyperlink = CheckIDUtil.checkHyperlinkId(hyperlinkId, hyperlinkDao);
checkActivePermission(hyperlink);
HyperlinkOutVO result = hyperlinkDao.toHyperlinkOutVO(hyperlink);
return result;
}
Expand Down Expand Up @@ -265,6 +319,7 @@ protected HyperlinkOutVO handleUpdateHyperlink(AuthenticationVO auth, HyperlinkI
throws Exception {
HyperlinkDao hyperlinkDao = this.getHyperlinkDao();
Hyperlink originalHyperlink = CheckIDUtil.checkHyperlinkId(modifiedHyperlink.getId(), hyperlinkDao);
checkActivePermission(originalHyperlink);
checkHyperlinkInput(modifiedHyperlink);
HyperlinkOutVO original = hyperlinkDao.toHyperlinkOutVO(originalHyperlink);
hyperlinkDao.evict(originalHyperlink);
Expand Down
15 changes: 15 additions & 0 deletions core/src/main/java/org/phoenixctms/ctsms/util/ServiceUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import java.text.DateFormat;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
Expand Down Expand Up @@ -3364,6 +3365,20 @@ public static boolean hasProbandAllDepartmentsAccount(Staff staff, UserPermissio
return false;
}

public static boolean hasInheritedPermissionProfile(User user, PermissionProfileGroup profileGroup,
UserPermissionProfileDao userPermissionProfileDao, PermissionProfile... profiles) {
HashMap<Long, HashSet<PermissionProfileGroup>> inheritPermissionProfileGroupMap = new HashMap<Long, HashSet<PermissionProfileGroup>>();
Iterator<UserPermissionProfile> userPermissionProfilesIt = ServiceUtil.getInheritedUserPermissionProfiles(CoreUtil.getUser(), profileGroup,
true, inheritPermissionProfileGroupMap, userPermissionProfileDao).iterator();
HashSet<PermissionProfile> profilesSet = new HashSet<PermissionProfile>(Arrays.asList(profiles));
while (userPermissionProfilesIt.hasNext()) {
if (profilesSet.contains(userPermissionProfilesIt.next().getProfile())) {
return true;
}
}
return false;
}

public static Collection<UserPermissionProfile> getInheritedUserPermissionProfiles(User user, PermissionProfileGroup profileGroup, Boolean active,
HashMap<Long, HashSet<PermissionProfileGroup>> inheritPermissionProfileGroupMap, UserPermissionProfileDao userPermissionProfileDao) {
if (isPermissionProfileGroupInherited(user, profileGroup, inheritPermissionProfileGroupMap)) {
Expand Down
Loading

0 comments on commit 21803b8

Please sign in to comment.