diff --git a/README.md b/README.md index 210489f..282644d 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,13 @@ Default: ``1`` Default user name prefix. Default: `vdjviz` +- ``userManagementSystem`` +Enable user management (admin) panel for users with admin rights +Default: `true` + +- ``userManagementSystemAccounts`` +Specifies the list of user accounts with predefined emails and passwords that will be granted admin rights. The ``email`` and ``password`` properties in ``application.conf`` should be used to specify their credentials. + - ``db.default.url`` Points to the path that will be used to store H2 database file. Default value: ``~/vdjviz/h2.db`` diff --git a/app/Global.java b/app/Global.java index 4685b5c..727815a 100644 --- a/app/Global.java +++ b/app/Global.java @@ -28,6 +28,8 @@ import java.io.File; import java.lang.reflect.Method; +import java.util.List; +import java.util.Map; import java.util.concurrent.TimeUnit; @@ -123,6 +125,40 @@ public void onStart(Application app) { } } + if (Configuration.isUserManagementSystemEnabled()) { + UserService userService = new UserService(app); + String username = "administrator"; + try { + List> userManagementSystemAccounts = Configuration.getUserManagementSystemAccounts(); + for (Map userManagementSystemAccount : userManagementSystemAccounts) { + String email = userManagementSystemAccount.get("email"); + String password = userManagementSystemAccount.get("password"); + LocalUser localUser = LocalUser.find.byId(email); + if (localUser != null) { + localUser.account.setPrivelegies(true); + localUser.account.update(); + localUser.update(); + } else { + Option bcrypt = Registry.hashers().get("bcrypt"); + SocialUser socialUser = new SocialUser(new IdentityId(email, "userpass"), + username, username, String.format("%s %s", username, username), + Option.apply(email), null, AuthenticationMethod.UserPassword(), + null, null, Some.apply(new PasswordInfo("bcrypt", BCrypt.hashpw(password, BCrypt.gensalt()), null)) + ); + userService.doSave(socialUser); + LocalUser localUser1 = LocalUser.find.byId(email); + localUser1.account.setPrivelegies(true); + localUser1.account.update(); + localUser1.update(); + } + Logger.info("Administrator user: " + email + " created."); + } + } catch (RuntimeException e) { + Logger.error("Error while creating admin users"); + e.printStackTrace(); + } + } + //Deleting empty files for (Account account: Account.findAll()) { for (UserFile userFile: account.getUserfiles()) { diff --git a/app/controllers/AccountAPI.java b/app/controllers/AccountAPI.java index 8383c57..58c2c85 100644 --- a/app/controllers/AccountAPI.java +++ b/app/controllers/AccountAPI.java @@ -84,7 +84,7 @@ public Result apply() throws Throwable { if (file == null) { return ok(Json.toJson(new ServerResponse("error", "You should upload the file first"))); } - if (account.getMaxFilesSize() > 0) { + if (account.getMaxFilesSize() != 0) { Long sizeMB = file.getFile().length() / 1024; if (sizeMB > account.getMaxFilesSize()) { return ok(Json.toJson(new ServerResponse("error", "File is too large"))); @@ -616,23 +616,23 @@ private static Result rarefaction(Account account, Boolean needToCreateNew) thro return ok(Json.toJson(new CacheServerResponse("success", rarefactionChart.create(needToCreateNew)))); } - public static class TagRequest { + public static class TagRequestClient { public String description; public String color; public String tagName; public long id; - public TagRequest() {} + public TagRequestClient() {} } public static Result createTag() { Account account = getCurrentAccount(); JsonNode request = request().body().asJson(); - TagRequest tagRequest; + TagRequestClient tagRequest; try { ObjectMapper objectMapper = new ObjectMapper(); - tagRequest = objectMapper.convertValue(request, TagRequest.class); + tagRequest = objectMapper.convertValue(request, TagRequestClient.class); } catch (Exception e) { e.printStackTrace(); return badRequest(Json.toJson(new ServerResponse("error", "Invalid request"))); @@ -646,17 +646,17 @@ public static Result editTag() { Account account = getCurrentAccount(); JsonNode request = request().body().asJson(); - TagRequest tagRequest; + TagRequestClient tagRequest; try { ObjectMapper objectMapper = new ObjectMapper(); - tagRequest = objectMapper.convertValue(request, TagRequest.class); + tagRequest = objectMapper.convertValue(request, TagRequestClient.class); } catch (Exception e) { e.printStackTrace(); return badRequest(Json.toJson(new ServerResponse("error", "Invalid request"))); } Tag tag = Tag.findById(tagRequest.id, account); if (tag == null) return badRequest(Json.toJson(new ServerResponse("error", "Invalid tag"))); - tag.updateTag(tagRequest); + tag.updateTag(tagRequest.description, tagRequest.color, tagRequest.tagName); return ok(Json.toJson(new Tag.TagInformation(tag))); } @@ -734,7 +734,7 @@ public void invoke(JsonNode event) { sample = sampleFileConnection.getSample(); file.setSampleCount(sample.getDiversity(), sample.getCount()); - if (account.getMaxClonotypesCount() > 0) { + if (account.getMaxClonotypesCount() != 0) { if (file.getClonotypesCount() > account.getMaxClonotypesCount()) { UserFile.deleteFile(file); out.write(Json.toJson(new WSResponse("error", "render", fileName, "Number of clonotypes in a sample should be less than " + account.getMaxClonotypesCount()))); diff --git a/app/controllers/AdministratorAPI.java b/app/controllers/AdministratorAPI.java new file mode 100644 index 0000000..c920062 --- /dev/null +++ b/app/controllers/AdministratorAPI.java @@ -0,0 +1,144 @@ +package controllers; + +import com.fasterxml.jackson.databind.JsonNode; +import models.Account; +import models.LocalUser; +import org.mindrot.jbcrypt.BCrypt; +import play.Logger; +import play.Play; +import play.libs.Json; +import play.mvc.Controller; +import play.mvc.Result; +import scala.Option; +import scala.Some; +import securesocial.core.*; +import securesocial.core.java.SecureSocial; +import securesocial.core.providers.utils.PasswordHasher; +import utils.UserService; +import utils.server.Configuration; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Created by bvdmitri on 06.04.16. + */ +@SecureSocial.SecuredAction +public class AdministratorAPI extends Controller { + + private static Account getCurrentAccount() { + Identity user = (Identity) ctx().args.get(SecureSocial.USER_KEY); + LocalUser localUser = LocalUser.find.byId(user.identityId().userId()); + return localUser.account; + } + + private static String getAdministratorName() { + Identity user = (Identity) ctx().args.get(SecureSocial.USER_KEY); + return user.identityId().userId(); + } + + private static Boolean isAccessDenied() { + return !Configuration.isUserManagementSystemEnabled() || !getCurrentAccount().isPrivilege(); + } + + public static Result index() { + if (isAccessDenied()) { + return redirect("/"); + } + return ok(views.html.administrator.index.render(getAdministratorName())); + } + + public static Result getAllAccounts() { + if (isAccessDenied()) return badRequest(); + List accounts = new ArrayList<>(); + for (Account account : Account.findAll()) { + accounts.add(Account.getAccountInformation(account)); + } + return ok(Json.toJson(accounts)); + } + + public static Result deleteUser() { + if (isAccessDenied()) return badRequest("Access denied"); + JsonNode request = request().body().asJson(); + if (!request.has("userName")) { + return badRequest(Json.toJson("Empty user name")); + } + try { + Account account = Account.findByUserName(request.get("userName").asText()); + if (account == null) return badRequest("Invalid user name"); + account.deleteAccount(); + return ok("Successfully deleted"); + } catch (Exception e) { + e.printStackTrace(); + return badRequest("Error while deleting"); + } + } + + public static Result createUser() { + if (isAccessDenied()) return badRequest("Access denied"); + JsonNode request = request().body().asJson(); + if (!request.has("userName") || !request.has("password")) return badRequest("Invalid request"); + String userName = request.get("userName").asText(); + String password = request.get("password").asText(); + Integer maxFileSize = request.has("maxFileSize") ? request.get("maxFileSize").asInt() : Configuration.getMaxFileSize(); + Integer maxFilesCount = request.has("maxFilesCount") ? request.get("maxFilesCount").asInt() : Configuration.getMaxFilesCount(); + Integer maxClonotypesCount = request.has("maxClonotypesCount") ? request.get("maxClonotypesCount").asInt() : Configuration.getMaxClonotypesCount(); + Integer maxSharedGroups = request.has("maxSharedFiles") ? request.get("maxSharedFiles").asInt() : Configuration.getMaxSharedGroups(); + Boolean privelegies = request.has("privelegies") && request.get("privelegies").asBoolean(); + + UserService userService = new UserService(Play.application()); + //Integer minimalPasswordLength = Play.application().configuration().getInt("securesocial.userpass.minimumPasswordLength"); + + //if (password.length() < minimalPasswordLength) return badRequest("Minimal password length: " + minimalPasswordLength); + + try { + LocalUser localUser = LocalUser.find.byId(userName); + if (localUser != null) return badRequest("User " + userName + " already created"); + Option bcrypt = Registry.hashers().get("bcrypt"); + SocialUser socialUser = new SocialUser(new IdentityId(userName, "userpass"), + userName, userName, String.format("%s %s", userName, userName), + Option.apply(userName), null, AuthenticationMethod.UserPassword(), + null, null, Some.apply(new PasswordInfo("bcrypt", BCrypt.hashpw(password, BCrypt.gensalt()), null)) + ); + userService.doSave(socialUser); + Account createdAccount = Account.findByUserName(userName); + createdAccount.setPrivelegies(privelegies); + createdAccount.setMaxClonotypesCount(maxClonotypesCount); + createdAccount.setMaxFilesCount(maxFilesCount); + createdAccount.setMaxFilesSize(maxFileSize); + createdAccount.setMaxSharedFiles(maxSharedGroups); + createdAccount.update(); + createdAccount.save(); + return ok("Successfully created"); + } catch (RuntimeException e) { + Logger.error("Error while creating default users"); + e.printStackTrace(); + } + return ok(); + } + + public static Result editUser() { + if (isAccessDenied()) return badRequest("Access denied"); + JsonNode request = request().body().asJson(); + if (!request.has("userName")) return badRequest("Invalid request"); + String oldUserName = request.get("userName").asText(); + Account account = Account.findByUserName(oldUserName); + if (account == null) return badRequest("Invalid request"); + Integer maxFileSize = request.has("maxFileSize") ? request.get("maxFileSize").asInt() : account.getMaxFilesSize(); + Integer maxFilesCount = request.has("maxFilesCount") ? request.get("maxFilesCount").asInt() : account.getMaxFilesCount(); + Integer maxClonotypesCount = request.has("maxClonotypesCount") ? request.get("maxClonotypesCount").asInt() : account.getMaxClonotypesCount(); + Integer maxSharedGroups = request.has("maxSharedFiles") ? request.get("maxSharedFiles").asInt() : account.getMaxSharedFiles(); + Boolean privelegies = request.has("privelegies") && request.get("privelegies").asBoolean(); + + account.setPrivelegies(privelegies); + account.setMaxClonotypesCount(maxClonotypesCount); + account.setMaxFilesCount(maxFilesCount); + account.setMaxFilesSize(maxFileSize); + account.setMaxSharedFiles(maxSharedGroups); + account.update(); + account.save(); + return ok("Successfully edited"); + } + +} diff --git a/app/models/Account.java b/app/models/Account.java index 1dfc557..a8a2616 100644 --- a/app/models/Account.java +++ b/app/models/Account.java @@ -54,12 +54,16 @@ public static class AccountInformation { public String userName; public Boolean privelegies; public Integer filesCount; + public Integer sharedGroupsCount; + public Integer maxSharedGroupsCount; public FilesInformation filesInformation; public List tags; public AccountInformation(Account account) { this.userName = account.userName; this.filesCount = account.getFilesCount(); + this.maxSharedGroupsCount = account.getMaxSharedFiles(); + this.sharedGroupsCount = SharedGroup.findByAccount(account).size(); this.filesInformation = getFilesInformation(account); this.privelegies = account.isPrivilege(); this.tags = new ArrayList<>(); @@ -134,7 +138,7 @@ public Integer getMaxSharedFiles() { } public Boolean isMaxSharedGroupsCountExceeded() { - if (getMaxSharedFiles() > 0) { + if (getMaxSharedFiles() != 0) { List byAccount = SharedGroup.findByAccount(this); if (byAccount.size() >= getMaxSharedFiles()) return true; } @@ -142,7 +146,7 @@ public Boolean isMaxSharedGroupsCountExceeded() { } public Boolean isMaxFilesCountExceeded() { - if (getMaxFilesCount() > 0) { + if (getMaxFilesCount() != 0) { if (getFilesCount() > getMaxFilesCount()) { return true; } @@ -214,13 +218,39 @@ public List getRenderedUserFiles() { } public long getMaxSampleCount() { - long max = 0l; + long max = 0L; for (UserFile userFile : getRenderedUserFiles()) { max = userFile.getSampleCount() > max ? userFile.getSampleCount() : max; } return max; } + + + public void setUserName(String userName) { + this.userName = userName; + } + + public void setMaxFilesSize(Integer maxFilesSize) { + this.maxFilesSize = maxFilesSize; + } + + public void setMaxFilesCount(Integer maxFilesCount) { + this.maxFilesCount = maxFilesCount; + } + + public void setMaxClonotypesCount(Integer maxClonotypesCount) { + this.maxClonotypesCount = maxClonotypesCount; + } + + public void setMaxSharedFiles(Integer maxSharedFiles) { + this.maxSharedFiles = maxSharedFiles; + } + + public void setPrivelegies(Boolean privelegies) { + this.privelegies = privelegies; + } + public String getUserName() { return userName; } @@ -245,6 +275,24 @@ public String toString() { return String.format("%s", userName); } + public void deleteAccount() { + for (UserFile userfile : userfiles) { + UserFile.deleteFile(userfile); + } + for (Tag tag : Tag.findByAccount(this)) { + tag.deleteTag(); + } + for (SharedGroup sharedGroup : SharedGroup.findByAccount(this)) { + sharedGroup.deleteGroup(); + } + File userDirectory = new File(userDirPath); + userDirectory.delete(); + user.account = null; + user.update(); + this.delete(); + user.delete(); + } + public static List findAll() { return find().all(); } diff --git a/app/models/Tag.java b/app/models/Tag.java index 3075dda..8552d7a 100644 --- a/app/models/Tag.java +++ b/app/models/Tag.java @@ -66,10 +66,10 @@ public void tagFiles(String[] selectedFiles) { } } - public void updateTag(AccountAPI.TagRequest tagRequest) { - this.color = tagRequest.color; - this.description = tagRequest.description; - this.tagName = tagRequest.tagName; + public void updateTag(String description, String color, String tagName) { + this.color = color; + this.description = description; + this.tagName = tagName; this.update(); } diff --git a/app/utils/server/Configuration.java b/app/utils/server/Configuration.java index c65f8dd..3e6ab6b 100644 --- a/app/utils/server/Configuration.java +++ b/app/utils/server/Configuration.java @@ -1,8 +1,13 @@ package utils.server; import play.Play; +import securesocial.core.java.SecureSocial; import java.io.File; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; public class Configuration { private static final Integer maxClonotypesCount = Play.application().configuration().getInt("maxClonotypesCount"); @@ -18,7 +23,8 @@ public class Configuration { private static final Boolean allowChangePasswords = Play.application().configuration().getBoolean("allowChangePasswords"); private static final Boolean allowSharing = Play.application().configuration().getBoolean("allowSharing"); private static final Boolean applyNewLimitsToOldUsers = Play.application().configuration().getBoolean("applyNewLimitsToOldUsers"); - + private static final Boolean allowUserManagmentSystem = Play.application().configuration().getBoolean("userManagementSystem"); + private static final List userManagmentSystemAccounts = Play.application().configuration().getConfigList("userManagementSystemAccounts"); public static Integer getMaxClonotypesCount() { @@ -62,6 +68,33 @@ public static String getUploadPath() { } } + public static List> getUserManagementSystemAccounts() { + List> accounts = new ArrayList<>(); + //Integer minimalPasswordLength = Play.application().configuration().getConfig("securesocial").getConfig("userpass").getInt("minimumPasswordLength"); + for (play.Configuration userManagmentSystemAccount : userManagmentSystemAccounts) { + Map account = new HashMap<>(); + if (!userManagmentSystemAccount.keys().contains("email") || !userManagmentSystemAccount.keys().contains("password")) { + LogAggregator.logWarning("Configuration warning: wrong user management account entry, " + + "please specify valid email and password, skipping.."); + continue; + } + String accountEmail = userManagmentSystemAccount.getString("email"); + String password = userManagmentSystemAccount.getString("password"); + /*if (password.length() < minimalPasswordLength) { + //TODO? + LogAggregator.logWarning("Configuration warning: invalid password for account " + accountEmail + + ", you can specify minimal password length in securesocial.conf file, skipping.."); + continue; + }*/ + account.put("email", accountEmail); + account.put("password", password); + accounts.add(account); + } + return accounts; + } + + public static Boolean isUserManagementSystemEnabled() { return allowUserManagmentSystem; } + public static Boolean isRegistrationEnabled() { return allowRegistration; } diff --git a/app/utils/server/LogAggregator.java b/app/utils/server/LogAggregator.java index 61d8ee8..64b5cae 100644 --- a/app/utils/server/LogAggregator.java +++ b/app/utils/server/LogAggregator.java @@ -36,8 +36,12 @@ public static void logClientSharedContentError(String message, Account account) log(message, LogType.CLIENT_SHARED, account); } - public static void logError(String message) { + public static void logWarning(String message) { Logger.warn(message); } + public static void logError(String message) { + Logger.error(message); + } + } diff --git a/app/views/Secure/Registration/startSignUp.scala.html b/app/views/Secure/Registration/startSignUp.scala.html index 5364eaf..db121de 100644 --- a/app/views/Secure/Registration/startSignUp.scala.html +++ b/app/views/Secure/Registration/startSignUp.scala.html @@ -23,7 +23,7 @@ >
- +
diff --git a/app/views/Secure/provider.scala.html b/app/views/Secure/provider.scala.html index 99409ee..ab58952 100644 --- a/app/views/Secure/provider.scala.html +++ b/app/views/Secure/provider.scala.html @@ -20,7 +20,7 @@
- +
diff --git a/app/views/account/account.scala.js b/app/views/account/account.scala.js index efc6209..fce6f24 100644 --- a/app/views/account/account.scala.js +++ b/app/views/account/account.scala.js @@ -803,6 +803,11 @@ var shared = false; $scope.setSharingState = setSharingState; $scope.showCompareModal = showCompareModal; $scope.openTags = openTags; + $scope.administratorPanel = administratorPanel; + + function administratorPanel() { + window.location.replace('/account/administrator') + } function showCompareModal() { $('#comparingAddButton').click(); diff --git a/app/views/account/accountMainPage.scala.html b/app/views/account/accountMainPage.scala.html index d38f045..1067505 100644 --- a/app/views/account/accountMainPage.scala.html +++ b/app/views/account/accountMainPage.scala.html @@ -13,7 +13,7 @@ @views.html.account.notifications() - @views.html.account.filesSidebar(shared) + @views.html.account.filesSidebar(shared, accountInfo) @if(!shared) { @views.html.account.fileUpload() diff --git a/app/views/account/filesSidebar.scala.html b/app/views/account/filesSidebar.scala.html index b2f6672..5553846 100644 --- a/app/views/account/filesSidebar.scala.html +++ b/app/views/account/filesSidebar.scala.html @@ -1,4 +1,4 @@ -@(shared: Boolean) +@(shared: Boolean, accountInfo: models.Account.AccountInformation) @import utils.server.Configuration;