From 2475784d52c3175f3bfdbcf41cb5d9b9ab71bc31 Mon Sep 17 00:00:00 2001 From: F43nd1r Date: Mon, 22 May 2017 18:46:51 +0200 Subject: [PATCH] Implemented user management --- backend/build.gradle | 29 +++- .../com/faendir/acra/data/AppManager.java | 38 ----- .../com/faendir/acra/data/AppRepository.java | 10 -- .../faendir/acra/data/AttachmentManager.java | 49 ------- .../com/faendir/acra/data/MappingManager.java | 34 ----- .../faendir/acra/data/MappingRepository.java | 10 -- .../com/faendir/acra/data/ReportManager.java | 66 --------- .../faendir/acra/data/ReportRepository.java | 10 -- .../{data => mongod}/MongoConfiguration.java | 2 +- .../acra/mongod/data/AppRepository.java | 11 ++ .../faendir/acra/mongod/data/DataManager.java | 138 ++++++++++++++++++ .../acra/mongod/data/MappingRepository.java | 11 ++ .../acra/mongod/data/ReportRepository.java | 11 ++ .../acra/{ => mongod}/data/ReportUtils.java | 9 +- .../acra/{data => mongod/model}/App.java | 2 +- .../acra/{data => mongod/model}/Bug.java | 2 +- .../faendir/acra/mongod/model/Permission.java | 60 ++++++++ .../model}/ProguardMapping.java | 6 +- .../acra/{data => mongod/model}/Report.java | 8 +- .../com/faendir/acra/mongod/model/User.java | 88 +++++++++++ .../faendir/acra/mongod/user/UserManager.java | 113 ++++++++++++++ .../acra/mongod/user/UserRepository.java | 11 ++ .../acra/security/SecurityInitializer.java | 24 --- .../faendir/acra/security/SecurityUtils.java | 14 ++ .../acra/security/WebSecurityConfig.java | 105 +++++++------ .../faendir/acra/service/ReportService.java | 12 +- .../java/com/faendir/acra/ui/BackendUI.java | 45 +++++- .../faendir/acra/ui/NavigationManager.java | 13 +- .../com/faendir/acra/ui/view/AppView.java | 37 +++-- .../com/faendir/acra/ui/view/ErrorView.java | 25 ++++ .../com/faendir/acra/ui/view/Overview.java | 45 +++--- .../faendir/acra/ui/view/PropertiesTab.java | 24 --- .../com/faendir/acra/ui/view/ReportList.java | 37 ----- .../com/faendir/acra/ui/view/ReportView.java | 23 ++- .../acra/ui/view/{ => base}/MyGrid.java | 6 +- .../acra/ui/view/{ => base}/NamedView.java | 9 +- .../faendir/acra/ui/view/base/ReportList.java | 43 ++++++ .../acra/ui/view/{ => tabs}/BugTab.java | 28 ++-- .../ui/view/{ => tabs}/DeObfuscationTab.java | 42 ++++-- .../acra/ui/view/tabs/PropertiesTab.java | 52 +++++++ .../acra/ui/view/user/ChangePasswordView.java | 72 +++++++++ .../acra/ui/view/user/PermissionEditor.java | 86 +++++++++++ .../acra/ui/view/user/UserManagerView.java | 106 ++++++++++++++ .../fileupload/Rfc1341ServletFileUpload.java | 2 - .../src/main/resources/AppWidgetset.gwt.xml | 13 ++ build.gradle | 3 + 46 files changed, 1124 insertions(+), 460 deletions(-) delete mode 100644 backend/src/main/java/com/faendir/acra/data/AppManager.java delete mode 100644 backend/src/main/java/com/faendir/acra/data/AppRepository.java delete mode 100644 backend/src/main/java/com/faendir/acra/data/AttachmentManager.java delete mode 100644 backend/src/main/java/com/faendir/acra/data/MappingManager.java delete mode 100644 backend/src/main/java/com/faendir/acra/data/MappingRepository.java delete mode 100644 backend/src/main/java/com/faendir/acra/data/ReportManager.java delete mode 100644 backend/src/main/java/com/faendir/acra/data/ReportRepository.java rename backend/src/main/java/com/faendir/acra/{data => mongod}/MongoConfiguration.java (97%) create mode 100644 backend/src/main/java/com/faendir/acra/mongod/data/AppRepository.java create mode 100644 backend/src/main/java/com/faendir/acra/mongod/data/DataManager.java create mode 100644 backend/src/main/java/com/faendir/acra/mongod/data/MappingRepository.java create mode 100644 backend/src/main/java/com/faendir/acra/mongod/data/ReportRepository.java rename backend/src/main/java/com/faendir/acra/{ => mongod}/data/ReportUtils.java (82%) rename backend/src/main/java/com/faendir/acra/{data => mongod/model}/App.java (93%) rename backend/src/main/java/com/faendir/acra/{data => mongod/model}/Bug.java (95%) create mode 100644 backend/src/main/java/com/faendir/acra/mongod/model/Permission.java rename backend/src/main/java/com/faendir/acra/{data => mongod/model}/ProguardMapping.java (83%) rename backend/src/main/java/com/faendir/acra/{data => mongod/model}/Report.java (89%) create mode 100644 backend/src/main/java/com/faendir/acra/mongod/model/User.java create mode 100644 backend/src/main/java/com/faendir/acra/mongod/user/UserManager.java create mode 100644 backend/src/main/java/com/faendir/acra/mongod/user/UserRepository.java delete mode 100644 backend/src/main/java/com/faendir/acra/security/SecurityInitializer.java create mode 100644 backend/src/main/java/com/faendir/acra/ui/view/ErrorView.java delete mode 100644 backend/src/main/java/com/faendir/acra/ui/view/PropertiesTab.java delete mode 100644 backend/src/main/java/com/faendir/acra/ui/view/ReportList.java rename backend/src/main/java/com/faendir/acra/ui/view/{ => base}/MyGrid.java (83%) rename backend/src/main/java/com/faendir/acra/ui/view/{ => base}/NamedView.java (78%) create mode 100644 backend/src/main/java/com/faendir/acra/ui/view/base/ReportList.java rename backend/src/main/java/com/faendir/acra/ui/view/{ => tabs}/BugTab.java (65%) rename backend/src/main/java/com/faendir/acra/ui/view/{ => tabs}/DeObfuscationTab.java (60%) create mode 100644 backend/src/main/java/com/faendir/acra/ui/view/tabs/PropertiesTab.java create mode 100644 backend/src/main/java/com/faendir/acra/ui/view/user/ChangePasswordView.java create mode 100644 backend/src/main/java/com/faendir/acra/ui/view/user/PermissionEditor.java create mode 100644 backend/src/main/java/com/faendir/acra/ui/view/user/UserManagerView.java create mode 100644 backend/src/main/resources/AppWidgetset.gwt.xml diff --git a/backend/build.gradle b/backend/build.gradle index 26edcf4..b825261 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -12,6 +12,7 @@ buildscript { plugins { id "net.ltgt.apt" version "0.10" + id 'fi.jasoft.plugin.vaadin' version '1.1.10' } apply plugin: 'java' apply plugin: 'eclipse' @@ -20,7 +21,9 @@ apply plugin: 'org.springframework.boot' group 'com.faendir' version = '0.1.0-SNAPSHOT' sourceCompatibility = 1.8 -ext.debug = true; +ext { + debug = true +} dependencies { compile 'org.springframework.boot:spring-boot-starter-data-mongodb' @@ -31,17 +34,39 @@ dependencies { compile 'org.ocpsoft.prettytime:prettytime:3.2.7.Final' compile 'commons-fileupload:commons-fileupload:1.3.2' compile 'net.sf.proguard:proguard-retrace:5.3.3' + compile 'org.vaadin.addons:popupbutton:3.0.0' compileOnly project(':annotation') apt project(':annotationprocessor') } +configurations { + 'vaadin-client' { + resolutionStrategy { + dependencySubstitution { + substitute module('javax.validation:validation-api') with module('javax.validation:validation-api:1.0.0.GA') + } + //force 'javax.validation:validation-api:1.0.0.GA' + } + } +} + dependencyManagement { imports { mavenBom "com.vaadin:vaadin-bom:8.0.6" } } -if(!ext.debug){ +bootRun { + // Needed by Vaadin plugin (https://github.com/johndevs/gradle-vaadin-plugin/issues/325) + classpath += configurations['vaadin-server'] + classpath += configurations['vaadin-client'] + + dependsOn 'vaadinCompile', 'vaadinThemeCompile' +} + +project.convention.getPlugin(WarPluginConvention).webAppDirName = "src/main/resources" + +if (!ext.debug) { apply from: 'releasepackaging.gradle' } \ No newline at end of file diff --git a/backend/src/main/java/com/faendir/acra/data/AppManager.java b/backend/src/main/java/com/faendir/acra/data/AppManager.java deleted file mode 100644 index 9d780d7..0000000 --- a/backend/src/main/java/com/faendir/acra/data/AppManager.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.faendir.acra.data; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; -import org.springframework.util.Base64Utils; - -import java.security.SecureRandom; -import java.util.List; - -/** - * @author Lukas - * @since 22.03.2017 - */ -@Component -public class AppManager { - private final SecureRandom secureRandom; - private final AppRepository appRepository; - - @Autowired - public AppManager(SecureRandom secureRandom, AppRepository appRepository) { - this.secureRandom = secureRandom; - this.appRepository = appRepository; - } - - public App createNewApp(String name){ - byte[] bytes = new byte[12]; - secureRandom.nextBytes(bytes); - return appRepository.save(new App(name, Base64Utils.encodeToString(bytes))); - } - - public List getApps(){ - return appRepository.findAll(); - } - - public App getApp(String id){ - return appRepository.findOne(id); - } -} diff --git a/backend/src/main/java/com/faendir/acra/data/AppRepository.java b/backend/src/main/java/com/faendir/acra/data/AppRepository.java deleted file mode 100644 index e103ffe..0000000 --- a/backend/src/main/java/com/faendir/acra/data/AppRepository.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.faendir.acra.data; - -import org.springframework.data.mongodb.repository.MongoRepository; - -/** - * @author Lukas - * @since 22.03.2017 - */ -public interface AppRepository extends MongoRepository { -} diff --git a/backend/src/main/java/com/faendir/acra/data/AttachmentManager.java b/backend/src/main/java/com/faendir/acra/data/AttachmentManager.java deleted file mode 100644 index 9791333..0000000 --- a/backend/src/main/java/com/faendir/acra/data/AttachmentManager.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.faendir.acra.data; - -import com.mongodb.BasicDBObjectBuilder; -import com.mongodb.gridfs.GridFSDBFile; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.mongodb.core.query.Criteria; -import org.springframework.data.mongodb.core.query.Query; -import org.springframework.data.mongodb.gridfs.GridFsTemplate; -import org.springframework.stereotype.Component; -import org.springframework.web.multipart.MultipartFile; - -import java.io.IOException; -import java.util.List; - -/** - * @author Lukas - * @since 19.05.2017 - */ -@Component -public class AttachmentManager { - private final GridFsTemplate gridFsTemplate; - private final Logger logger; - - @Autowired - public AttachmentManager(GridFsTemplate gridFsTemplate) { - this.gridFsTemplate = gridFsTemplate; - logger = LoggerFactory.getLogger(AttachmentManager.class); - } - - public void saveAttachments(String report, List attachments) { - for (MultipartFile a : attachments) { - try { - gridFsTemplate.store(a.getInputStream(), a.getOriginalFilename(), a.getContentType(), new BasicDBObjectBuilder().add("reportId", report).get()); - } catch (IOException e) { - logger.warn("Failed to load attachment", e); - } - } - } - - public List getAttachments(String report) { - return gridFsTemplate.find(new Query(Criteria.where("metadata.reportId").is(report))); - } - - public void removeAttachments(String report) { - gridFsTemplate.delete(new Query(Criteria.where("metadata.reportId").is(report))); - } -} diff --git a/backend/src/main/java/com/faendir/acra/data/MappingManager.java b/backend/src/main/java/com/faendir/acra/data/MappingManager.java deleted file mode 100644 index 3f0fb0b..0000000 --- a/backend/src/main/java/com/faendir/acra/data/MappingManager.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.faendir.acra.data; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.domain.Example; -import org.springframework.data.domain.ExampleMatcher; -import org.springframework.stereotype.Component; - -import java.util.List; - -/** - * @author Lukas - * @since 19.05.2017 - */ -@Component -public class MappingManager { - private final MappingRepository mappingRepository; - - @Autowired - public MappingManager(MappingRepository mappingRepository) { - this.mappingRepository = mappingRepository; - } - - public void addMapping(String app, int version, String mappings) { - mappingRepository.save(new ProguardMapping(app, version, mappings)); - } - - public ProguardMapping getMapping(String app, int version) { - return mappingRepository.findOne(new ProguardMapping.MetaData(app, version)); - } - - public List getMappings(String app) { - return mappingRepository.findAll(Example.of(new ProguardMapping(app, -1, null), ExampleMatcher.matchingAny())); - } -} diff --git a/backend/src/main/java/com/faendir/acra/data/MappingRepository.java b/backend/src/main/java/com/faendir/acra/data/MappingRepository.java deleted file mode 100644 index c0fa8f4..0000000 --- a/backend/src/main/java/com/faendir/acra/data/MappingRepository.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.faendir.acra.data; - -import org.springframework.data.mongodb.repository.MongoRepository; - -/** - * @author Lukas - * @since 19.05.2017 - */ -public interface MappingRepository extends MongoRepository { -} diff --git a/backend/src/main/java/com/faendir/acra/data/ReportManager.java b/backend/src/main/java/com/faendir/acra/data/ReportManager.java deleted file mode 100644 index ea2c332..0000000 --- a/backend/src/main/java/com/faendir/acra/data/ReportManager.java +++ /dev/null @@ -1,66 +0,0 @@ -package com.faendir.acra.data; - -import org.json.JSONObject; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.domain.Example; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.stereotype.Component; -import org.springframework.web.multipart.MultipartFile; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -/** - * @author Lukas - * @since 22.03.2017 - */ -@Component -public class ReportManager { - private final ReportRepository reportRepository; - private final AttachmentManager attachmentManager; - private final List listeners; - - @Autowired - public ReportManager(ReportRepository reportRepository, AttachmentManager attachmentManager) { - this.reportRepository = reportRepository; - this.attachmentManager = attachmentManager; - listeners = new ArrayList<>(); - } - - public void newReport(JSONObject content) { - newReport(content, Collections.emptyList()); - } - - public void newReport(JSONObject content, List attachments) { - Report report = reportRepository.save(new Report(content, SecurityContextHolder.getContext().getAuthentication().getName())); - attachmentManager.saveAttachments(report.getId(), attachments); - listeners.forEach(ChangeListener::onChange); - } - - public List getReports(String app) { - return reportRepository.findAll(Example.of(new Report(null, app))); - } - - public Report getReport(String id) { - return reportRepository.findOne(id); - } - - public void remove(Report report){ - reportRepository.delete(report); - attachmentManager.removeAttachments(report.getId()); - listeners.forEach(ChangeListener::onChange); - } - - public boolean addListener(ChangeListener changeListener) { - return listeners.add(changeListener); - } - - public boolean removeListener(ChangeListener changeListener) { - return listeners.remove(changeListener); - } - - public interface ChangeListener { - void onChange(); - } -} diff --git a/backend/src/main/java/com/faendir/acra/data/ReportRepository.java b/backend/src/main/java/com/faendir/acra/data/ReportRepository.java deleted file mode 100644 index e4f5fc0..0000000 --- a/backend/src/main/java/com/faendir/acra/data/ReportRepository.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.faendir.acra.data; - -import org.springframework.data.mongodb.repository.MongoRepository; - -/** - * @author Lukas - * @since 22.03.2017 - */ -public interface ReportRepository extends MongoRepository { -} diff --git a/backend/src/main/java/com/faendir/acra/data/MongoConfiguration.java b/backend/src/main/java/com/faendir/acra/mongod/MongoConfiguration.java similarity index 97% rename from backend/src/main/java/com/faendir/acra/data/MongoConfiguration.java rename to backend/src/main/java/com/faendir/acra/mongod/MongoConfiguration.java index cc7b2fc..e6a674b 100644 --- a/backend/src/main/java/com/faendir/acra/data/MongoConfiguration.java +++ b/backend/src/main/java/com/faendir/acra/mongod/MongoConfiguration.java @@ -1,4 +1,4 @@ -package com.faendir.acra.data; +package com.faendir.acra.mongod; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; diff --git a/backend/src/main/java/com/faendir/acra/mongod/data/AppRepository.java b/backend/src/main/java/com/faendir/acra/mongod/data/AppRepository.java new file mode 100644 index 0000000..e8c4be8 --- /dev/null +++ b/backend/src/main/java/com/faendir/acra/mongod/data/AppRepository.java @@ -0,0 +1,11 @@ +package com.faendir.acra.mongod.data; + +import com.faendir.acra.mongod.model.App; +import org.springframework.data.mongodb.repository.MongoRepository; + +/** + * @author Lukas + * @since 22.03.2017 + */ +interface AppRepository extends MongoRepository { +} diff --git a/backend/src/main/java/com/faendir/acra/mongod/data/DataManager.java b/backend/src/main/java/com/faendir/acra/mongod/data/DataManager.java new file mode 100644 index 0000000..df60a04 --- /dev/null +++ b/backend/src/main/java/com/faendir/acra/mongod/data/DataManager.java @@ -0,0 +1,138 @@ +package com.faendir.acra.mongod.data; + +import com.faendir.acra.mongod.model.App; +import com.faendir.acra.mongod.model.ProguardMapping; +import com.faendir.acra.mongod.model.Report; +import com.mongodb.BasicDBObjectBuilder; +import com.mongodb.gridfs.GridFSDBFile; +import org.json.JSONObject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Example; +import org.springframework.data.domain.ExampleMatcher; +import org.springframework.data.mongodb.core.query.Criteria; +import org.springframework.data.mongodb.core.query.Query; +import org.springframework.data.mongodb.gridfs.GridFsTemplate; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.util.Base64Utils; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * @author Lukas + * @since 21.05.2017 + */ +@Component +public class DataManager { + private final MappingRepository mappingRepository; + private final ReportRepository reportRepository; + private final List listeners; + private final GridFsTemplate gridFsTemplate; + private final Logger logger; + private final SecureRandom secureRandom; + private final AppRepository appRepository; + + @Autowired + public DataManager(SecureRandom secureRandom, AppRepository appRepository, GridFsTemplate gridFsTemplate, MappingRepository mappingRepository, ReportRepository reportRepository) { + this.secureRandom = secureRandom; + this.appRepository = appRepository; + logger = LoggerFactory.getLogger(DataManager.class); + this.gridFsTemplate = gridFsTemplate; + this.mappingRepository = mappingRepository; + this.reportRepository = reportRepository; + this.listeners = new ArrayList<>(); + } + + public void createNewApp(String name){ + byte[] bytes = new byte[12]; + secureRandom.nextBytes(bytes); + appRepository.save(new App(name, Base64Utils.encodeToString(bytes))); + } + + public List getApps(){ + return appRepository.findAll(); + } + + public App getApp(String id){ + return appRepository.findOne(id); + } + + public void deleteApp(String id){ + appRepository.delete(id); + getReports(id).forEach(this::remove); + mappingRepository.delete(getMappings(id)); + } + + public void saveAttachments(String report, List attachments) { + for (MultipartFile a : attachments) { + try { + gridFsTemplate.store(a.getInputStream(), a.getOriginalFilename(), a.getContentType(), new BasicDBObjectBuilder().add("reportId", report).get()); + } catch (IOException e) { + logger.warn("Failed to load attachment", e); + } + } + } + + public List getAttachments(String report) { + return gridFsTemplate.find(new Query(Criteria.where("metadata.reportId").is(report))); + } + + public void removeAttachments(String report) { + gridFsTemplate.delete(new Query(Criteria.where("metadata.reportId").is(report))); + } + + public void addMapping(String app, int version, String mappings) { + mappingRepository.save(new ProguardMapping(app, version, mappings)); + } + + public ProguardMapping getMapping(String app, int version) { + return mappingRepository.findOne(new ProguardMapping.MetaData(app, version)); + } + + public List getMappings(String app) { + return mappingRepository.findAll(Example.of(new ProguardMapping(app, -1, null), ExampleMatcher.matchingAny())); + } + + public void newReport(JSONObject content) { + newReport(content, Collections.emptyList()); + } + + public void newReport(JSONObject content, List attachments) { + Report report = reportRepository.save(new Report(content, SecurityContextHolder.getContext().getAuthentication().getName())); + saveAttachments(report.getId(), attachments); + listeners.forEach(ReportChangeListener::onChange); + } + + public List getReports(String app) { + return reportRepository.findAll(Example.of(new Report(null, app))); + } + + public Report getReport(String id) { + return reportRepository.findOne(id); + } + + public void remove(Report report){ + reportRepository.delete(report); + removeAttachments(report.getId()); + listeners.forEach(ReportChangeListener::onChange); + } + + public boolean addListener(ReportChangeListener reportChangeListener) { + return listeners.add(reportChangeListener); + } + + public boolean removeListener(ReportChangeListener reportChangeListener) { + return listeners.remove(reportChangeListener); + } + + public interface ReportChangeListener { + void onChange(); + } +} diff --git a/backend/src/main/java/com/faendir/acra/mongod/data/MappingRepository.java b/backend/src/main/java/com/faendir/acra/mongod/data/MappingRepository.java new file mode 100644 index 0000000..92ed790 --- /dev/null +++ b/backend/src/main/java/com/faendir/acra/mongod/data/MappingRepository.java @@ -0,0 +1,11 @@ +package com.faendir.acra.mongod.data; + +import com.faendir.acra.mongod.model.ProguardMapping; +import org.springframework.data.mongodb.repository.MongoRepository; + +/** + * @author Lukas + * @since 19.05.2017 + */ +interface MappingRepository extends MongoRepository { +} diff --git a/backend/src/main/java/com/faendir/acra/mongod/data/ReportRepository.java b/backend/src/main/java/com/faendir/acra/mongod/data/ReportRepository.java new file mode 100644 index 0000000..43ea3fe --- /dev/null +++ b/backend/src/main/java/com/faendir/acra/mongod/data/ReportRepository.java @@ -0,0 +1,11 @@ +package com.faendir.acra.mongod.data; + +import com.faendir.acra.mongod.model.Report; +import org.springframework.data.mongodb.repository.MongoRepository; + +/** + * @author Lukas + * @since 22.03.2017 + */ +interface ReportRepository extends MongoRepository { +} diff --git a/backend/src/main/java/com/faendir/acra/data/ReportUtils.java b/backend/src/main/java/com/faendir/acra/mongod/data/ReportUtils.java similarity index 82% rename from backend/src/main/java/com/faendir/acra/data/ReportUtils.java rename to backend/src/main/java/com/faendir/acra/mongod/data/ReportUtils.java index 2cb0114..5f8b93f 100644 --- a/backend/src/main/java/com/faendir/acra/data/ReportUtils.java +++ b/backend/src/main/java/com/faendir/acra/mongod/data/ReportUtils.java @@ -1,5 +1,8 @@ -package com.faendir.acra.data; +package com.faendir.acra.mongod.data; +import com.faendir.acra.mongod.model.Bug; +import com.faendir.acra.mongod.model.ProguardMapping; +import com.faendir.acra.mongod.model.Report; import org.apache.commons.io.FileUtils; import proguard.retrace.ReTrace; @@ -35,7 +38,7 @@ public final class ReportUtils { return bugs; } - static Date getDateFromString(String s) { + public static Date getDateFromString(String s) { try { return dateFormat.parse(s); } catch (ParseException e) { @@ -43,7 +46,7 @@ public final class ReportUtils { } } - static String retrace(String stacktrace, ProguardMapping mapping) throws IOException { + public static String retrace(String stacktrace, ProguardMapping mapping) throws IOException { File file = File.createTempFile("mapping", ".txt"); FileUtils.writeStringToFile(file, mapping.getMappings()); StringWriter writer = new StringWriter(); diff --git a/backend/src/main/java/com/faendir/acra/data/App.java b/backend/src/main/java/com/faendir/acra/mongod/model/App.java similarity index 93% rename from backend/src/main/java/com/faendir/acra/data/App.java rename to backend/src/main/java/com/faendir/acra/mongod/model/App.java index f4880b3..339b048 100644 --- a/backend/src/main/java/com/faendir/acra/data/App.java +++ b/backend/src/main/java/com/faendir/acra/mongod/model/App.java @@ -1,4 +1,4 @@ -package com.faendir.acra.data; +package com.faendir.acra.mongod.model; import org.springframework.data.mongodb.core.mapping.Document; diff --git a/backend/src/main/java/com/faendir/acra/data/Bug.java b/backend/src/main/java/com/faendir/acra/mongod/model/Bug.java similarity index 95% rename from backend/src/main/java/com/faendir/acra/data/Bug.java rename to backend/src/main/java/com/faendir/acra/mongod/model/Bug.java index b00397a..0771485 100644 --- a/backend/src/main/java/com/faendir/acra/data/Bug.java +++ b/backend/src/main/java/com/faendir/acra/mongod/model/Bug.java @@ -1,4 +1,4 @@ -package com.faendir.acra.data; +package com.faendir.acra.mongod.model; import java.util.ArrayList; import java.util.Date; diff --git a/backend/src/main/java/com/faendir/acra/mongod/model/Permission.java b/backend/src/main/java/com/faendir/acra/mongod/model/Permission.java new file mode 100644 index 0000000..2e7c0a1 --- /dev/null +++ b/backend/src/main/java/com/faendir/acra/mongod/model/Permission.java @@ -0,0 +1,60 @@ +package com.faendir.acra.mongod.model; + +import org.springframework.security.core.GrantedAuthority; + +/** + * @author Lukas + * @since 20.05.2017 + */ +public class Permission implements GrantedAuthority { + + public enum Level { + NONE, + VIEW, + EDIT, + ADMIN + } + + private Level level; + private String app; + + public Permission() { + } + + public Permission(String app, Level level) { + this.level = level; + this.app = app; + } + + public Level getLevel() { + return level; + } + + public void setLevel(Level level) { + this.level = level; + } + + public String getApp() { + return app; + } + + @Override + public String getAuthority() { + return "PERMISSION_" + level.name() + "_" + app; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Permission that = (Permission) o; + + return app.equals(that.app); + } + + @Override + public int hashCode() { + return app.hashCode(); + } +} diff --git a/backend/src/main/java/com/faendir/acra/data/ProguardMapping.java b/backend/src/main/java/com/faendir/acra/mongod/model/ProguardMapping.java similarity index 83% rename from backend/src/main/java/com/faendir/acra/data/ProguardMapping.java rename to backend/src/main/java/com/faendir/acra/mongod/model/ProguardMapping.java index c0fff97..324eac9 100644 --- a/backend/src/main/java/com/faendir/acra/data/ProguardMapping.java +++ b/backend/src/main/java/com/faendir/acra/mongod/model/ProguardMapping.java @@ -1,4 +1,4 @@ -package com.faendir.acra.data; +package com.faendir.acra.mongod.model; import org.springframework.data.mongodb.core.mapping.Document; @@ -33,11 +33,11 @@ public class ProguardMapping { return mappings; } - static class MetaData implements Serializable { + public static class MetaData implements Serializable { private String app; private int version; - MetaData(String app, int version) { + public MetaData(String app, int version) { this.app = app; this.version = version; } diff --git a/backend/src/main/java/com/faendir/acra/data/Report.java b/backend/src/main/java/com/faendir/acra/mongod/model/Report.java similarity index 89% rename from backend/src/main/java/com/faendir/acra/data/Report.java rename to backend/src/main/java/com/faendir/acra/mongod/model/Report.java index fcf15b2..2879cad 100644 --- a/backend/src/main/java/com/faendir/acra/data/Report.java +++ b/backend/src/main/java/com/faendir/acra/mongod/model/Report.java @@ -1,5 +1,7 @@ -package com.faendir.acra.data; +package com.faendir.acra.mongod.model; +import com.faendir.acra.mongod.data.DataManager; +import com.faendir.acra.mongod.data.ReportUtils; import org.json.JSONException; import org.json.JSONObject; import org.springframework.data.mongodb.core.index.Indexed; @@ -47,11 +49,11 @@ public class Report { return getValueSafe("STACK_TRACE", content::getString, ""); } - public String getDeObfuscatedStacktrace(MappingManager mappingManager){ + public String getDeObfuscatedStacktrace(DataManager dataManager){ if (deObfuscatedTrace != null) { return deObfuscatedTrace; } - ProguardMapping mapping = mappingManager.getMapping(app, getVersionCode()); + ProguardMapping mapping = dataManager.getMapping(app, getVersionCode()); if (mapping != null) { try { deObfuscatedTrace = ReportUtils.retrace(getStacktrace(), mapping); diff --git a/backend/src/main/java/com/faendir/acra/mongod/model/User.java b/backend/src/main/java/com/faendir/acra/mongod/model/User.java new file mode 100644 index 0000000..6524382 --- /dev/null +++ b/backend/src/main/java/com/faendir/acra/mongod/model/User.java @@ -0,0 +1,88 @@ +package com.faendir.acra.mongod.model; + +import org.springframework.data.annotation.Id; +import org.springframework.data.mongodb.core.mapping.Document; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * @author Lukas + * @since 20.05.2017 + */ +@Document +public class User implements UserDetails { + @Id + private String username; + private String password; + private Set roles; + private Set permissions; + + public User() { + permissions = new HashSet<>(); + roles = new HashSet<>(); + } + + public User(String username, String password, Collection roles) { + this(); + this.username = username; + this.password = password; + this.roles = new HashSet<>(roles); + } + + public Set getPermissions() { + return permissions; + } + + public Set getRoles() { + return roles; + } + + public void setPassword(String password) { + this.password = password; + } + + @Override + public Collection getAuthorities() { + List authorities = new ArrayList<>(permissions); + authorities.addAll(roles.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList())); + return authorities; + } + + @Override + public String getPassword() { + return password; + } + + @Override + public String getUsername() { + return username; + } + + @Override + public boolean isAccountNonExpired() { + return true; + } + + @Override + public boolean isAccountNonLocked() { + return true; + } + + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + public boolean isEnabled() { + return true; + } +} diff --git a/backend/src/main/java/com/faendir/acra/mongod/user/UserManager.java b/backend/src/main/java/com/faendir/acra/mongod/user/UserManager.java new file mode 100644 index 0000000..d22cef9 --- /dev/null +++ b/backend/src/main/java/com/faendir/acra/mongod/user/UserManager.java @@ -0,0 +1,113 @@ +package com.faendir.acra.mongod.user; + +import com.faendir.acra.mongod.data.DataManager; +import com.faendir.acra.mongod.model.Permission; +import com.faendir.acra.mongod.model.User; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Component; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +/** + * @author Lukas + * @since 20.05.2017 + */ +@Component +public class UserManager { + public static final String ROLE_ADMIN = "ROLE_ADMIN"; + public static final String ROLE_USER = "ROLE_USER"; + + private final UserRepository userRepository; + private final DataManager dataManager; + private final String defaultUser; + private final String defaultPassword; + private final PasswordEncoder passwordEncoder; + + @Autowired + public UserManager(UserRepository userRepository, DataManager dataManager, @Value("${security.user.name}") String defaultUser, @Value("${security.user.password}") String defaultPassword) { + this.userRepository = userRepository; + this.dataManager = dataManager; + this.defaultUser = defaultUser; + this.defaultPassword = defaultPassword; + passwordEncoder = new BCryptPasswordEncoder(); + } + + private void ensureValidPermissions(User user) { + user.getPermissions().removeIf(permission -> dataManager.getApp(permission.getApp()) == null); + dataManager.getApps().stream().filter(app -> user.getPermissions().stream().noneMatch(permission -> permission.getApp().equals(app.getId()))) + .forEach(app -> user.getPermissions().add(new Permission(app.getId(), user.getRoles().contains(ROLE_ADMIN) ? Permission.Level.ADMIN : Permission.Level.NONE))); + } + + public User getUser(String username) { + User user = userRepository.findOne(username); + if (user == null && defaultUser.equals(username)) { + return getDefaultUser(); + } + ensureValidPermissions(user); + return user; + } + + public boolean createUser(String username, String password) { + if (userRepository.exists(username)) { + return false; + } + User user = new User(username, passwordEncoder.encode(password), Collections.singleton(ROLE_USER)); + ensureValidPermissions(user); + userRepository.save(user); + return true; + } + + public boolean checkPassword(User user, String password) { + return user != null && passwordEncoder.matches(password, user.getPassword()); + } + + public boolean changePassword(User user, String oldPassword, String newPassword) { + if (checkPassword(user, oldPassword)) { + user.setPassword(newPassword); + userRepository.save(user); + return true; + } + return false; + } + + public void setAdmin(User user, boolean admin) { + if (admin) user.getRoles().add(ROLE_ADMIN); + else user.getRoles().remove(ROLE_ADMIN); + userRepository.save(user); + } + + public User setPermission(User user, String app, Permission.Level level) { + Optional permission = user.getPermissions().stream().filter(p -> p.getApp().equals(app)).findAny(); + if (permission.isPresent()) { + permission.get().setLevel(level); + } else { + user.getPermissions().add(new Permission(app, level)); + } + return userRepository.save(user); + } + + public boolean hasPermission(User user, String app, Permission.Level level) { + ensureValidPermissions(user); + Optional optional = user.getPermissions().stream().filter(permission -> permission.getApp().equals(app)).findAny(); + return optional.isPresent() && optional.get().getLevel().ordinal() >= level.ordinal(); + } + + public List getUsers() { + List users = userRepository.findAll(); + if (users.stream().noneMatch(user -> user.getUsername().equals(defaultUser))) { + users.add(getDefaultUser()); + } + users.forEach(this::ensureValidPermissions); + return users; + } + + private User getDefaultUser() { + return new User(defaultUser, passwordEncoder.encode(defaultPassword), Arrays.asList(ROLE_USER, ROLE_ADMIN)); + } +} diff --git a/backend/src/main/java/com/faendir/acra/mongod/user/UserRepository.java b/backend/src/main/java/com/faendir/acra/mongod/user/UserRepository.java new file mode 100644 index 0000000..05206d5 --- /dev/null +++ b/backend/src/main/java/com/faendir/acra/mongod/user/UserRepository.java @@ -0,0 +1,11 @@ +package com.faendir.acra.mongod.user; + +import com.faendir.acra.mongod.model.User; +import org.springframework.data.mongodb.repository.MongoRepository; + +/** + * @author Lukas + * @since 20.05.2017 + */ +interface UserRepository extends MongoRepository { +} diff --git a/backend/src/main/java/com/faendir/acra/security/SecurityInitializer.java b/backend/src/main/java/com/faendir/acra/security/SecurityInitializer.java deleted file mode 100644 index 26b549a..0000000 --- a/backend/src/main/java/com/faendir/acra/security/SecurityInitializer.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.faendir.acra.security; - -import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer; - -/** - * @author Lukas - * @since 13.05.2017 - */ -public class SecurityInitializer extends AbstractAnnotationConfigDispatcherServletInitializer { - @Override - protected Class[] getRootConfigClasses() { - return new Class[0]; - } - - @Override - protected Class[] getServletConfigClasses() { - return new Class[0]; - } - - @Override - protected String[] getServletMappings() { - return new String[0]; - } -} diff --git a/backend/src/main/java/com/faendir/acra/security/SecurityUtils.java b/backend/src/main/java/com/faendir/acra/security/SecurityUtils.java index 005d2a3..96dcbc7 100644 --- a/backend/src/main/java/com/faendir/acra/security/SecurityUtils.java +++ b/backend/src/main/java/com/faendir/acra/security/SecurityUtils.java @@ -1,5 +1,7 @@ package com.faendir.acra.security; +import com.faendir.acra.mongod.model.Permission; +import com.faendir.acra.mongod.user.UserManager; import org.springframework.security.core.Authentication; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; @@ -18,4 +20,16 @@ public final class SecurityUtils { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); return authentication != null && authentication.getAuthorities().contains(new SimpleGrantedAuthority(role)); } + + public static String getUsername() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + return authentication != null ? authentication.getName() : ""; + } + + public static boolean hasPermission(String app, Permission.Level level) { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + return authentication != null && authentication.getAuthorities().stream().filter(authority -> authority instanceof Permission) + .map(Permission.class::cast).filter(permission -> permission.getApp().equals(app)).findAny().map(Permission::getLevel) + .orElseGet(() -> hasRole(UserManager.ROLE_ADMIN) ? Permission.Level.ADMIN : Permission.Level.NONE).ordinal() >= level.ordinal(); + } } diff --git a/backend/src/main/java/com/faendir/acra/security/WebSecurityConfig.java b/backend/src/main/java/com/faendir/acra/security/WebSecurityConfig.java index 4321b2a..45b93f5 100644 --- a/backend/src/main/java/com/faendir/acra/security/WebSecurityConfig.java +++ b/backend/src/main/java/com/faendir/acra/security/WebSecurityConfig.java @@ -1,11 +1,12 @@ package com.faendir.acra.security; -import com.faendir.acra.data.App; -import com.faendir.acra.data.AppManager; +import com.faendir.acra.mongod.data.DataManager; +import com.faendir.acra.mongod.model.App; +import com.faendir.acra.mongod.model.User; +import com.faendir.acra.mongod.user.UserManager; import com.vaadin.server.VaadinService; import com.vaadin.server.VaadinSession; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; @@ -20,12 +21,15 @@ import org.springframework.security.config.annotation.web.configuration.EnableWe import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; +import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextImpl; import org.springframework.security.core.userdetails.UsernameNotFoundException; +import java.util.Collection; + /** * @author Lukas * @since 22.03.2017 @@ -39,57 +43,62 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter { SecurityContextHolder.setStrategyName(VaadinSessionSecurityContextHolderStrategy.class.getName()); } - private final String user; - private final String password; - private final AppManager appManager; - private final AuthenticationProvider authenticationProvider = new AuthenticationProvider() { - @Override - public Authentication authenticate(Authentication authentication) throws AuthenticationException { - if (authentication instanceof UsernamePasswordAuthenticationToken) { - if (user.equals(authentication.getName())) { - if (password.equals(authentication.getCredentials())) { - return new UsernamePasswordAuthenticationToken(authentication.getName(), authentication.getCredentials(), AuthorityUtils.createAuthorityList("ROLE_ADMIN")); - } else { - throw new BadCredentialsException("Password mismatch for user " + authentication.getName()); - } - } - App app = appManager.getApp(authentication.getName()); - if (app != null) { - if (app.getPassword().equals(authentication.getCredentials())) { - Authentication auth = new UsernamePasswordAuthenticationToken(authentication.getName(), authentication.getCredentials(), AuthorityUtils.createAuthorityList("ROLE_REPORTER")); - VaadinSession session = VaadinSession.getCurrent(); - if (session == null) session = new VaadinSession(VaadinService.getCurrent()); - VaadinSession.setCurrent(session); - SecurityContext securityContext = new SecurityContextImpl(); - securityContext.setAuthentication(auth); - session.setAttribute(SecurityContext.class, securityContext); - return auth; - } else { - throw new BadCredentialsException("Password mismatch for user " + authentication.getName()); - } - } else { - throw new UsernameNotFoundException("Username " + authentication.getName() + " not found"); - } - } - return null; - } - - @Override - public boolean supports(Class authentication) { - return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication); - } - }; + private final DataManager dataManager; + private final UserManager userManager; @Autowired - public WebSecurityConfig(@Value("${security.user.name}") String user, @Value("${security.user.password}") String password, AppManager appManager) { - this.user = user; - this.password = password; - this.appManager = appManager; + public WebSecurityConfig(DataManager dataManager, UserManager userManager) { + this.dataManager = dataManager; + this.userManager = userManager; } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { - auth.authenticationProvider(authenticationProvider); + auth.authenticationProvider(new AuthenticationProvider() { + @Override + public Authentication authenticate(Authentication authentication) throws AuthenticationException { + if (authentication instanceof UsernamePasswordAuthenticationToken) { + App app = dataManager.getApp(authentication.getName()); + User user; + if (app != null) { + if (app.getPassword().equals(authentication.getCredentials())) { + return getGrantedToken(app.getId(), app.getPassword(), AuthorityUtils.createAuthorityList("ROLE_REPORTER")); + } else { + throwBadCredentials(app.getId()); + } + } else if ((user = userManager.getUser(authentication.getName())) != null) { + if (userManager.checkPassword(user, (String) authentication.getCredentials())) { + return getGrantedToken(user.getUsername(), user.getPassword(), user.getAuthorities()); + } else { + throwBadCredentials(user.getUsername()); + } + } else { + throw new UsernameNotFoundException("Username " + authentication.getName() + " not found"); + } + } + return null; + } + + @Override + public boolean supports(Class authentication) { + return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication); + } + }); + } + + private void throwBadCredentials(String username) { + throw new BadCredentialsException("Password mismatch for user " + username); + } + + private Authentication getGrantedToken(String username, String password, Collection authorities) { + Authentication auth = new UsernamePasswordAuthenticationToken(username, password, authorities); + VaadinSession session = VaadinSession.getCurrent(); + if (session == null) session = new VaadinSession(VaadinService.getCurrent()); + VaadinSession.setCurrent(session); + SecurityContext securityContext = new SecurityContextImpl(); + securityContext.setAuthentication(auth); + session.setAttribute(SecurityContext.class, securityContext); + return auth; } @Override diff --git a/backend/src/main/java/com/faendir/acra/service/ReportService.java b/backend/src/main/java/com/faendir/acra/service/ReportService.java index f574f4c..96fde04 100644 --- a/backend/src/main/java/com/faendir/acra/service/ReportService.java +++ b/backend/src/main/java/com/faendir/acra/service/ReportService.java @@ -1,6 +1,6 @@ package com.faendir.acra.service; -import com.faendir.acra.data.ReportManager; +import com.faendir.acra.mongod.data.DataManager; import org.json.JSONObject; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.MediaType; @@ -26,18 +26,18 @@ import java.util.List; */ @RestController public class ReportService { - private final ReportManager reportManager; + private final DataManager dataManager; @Autowired - public ReportService(ReportManager reportManager) { - this.reportManager = reportManager; + public ReportService(DataManager dataManager) { + this.dataManager = dataManager; } @PreAuthorize("hasRole('REPORTER')") @RequestMapping(value = "/report", consumes = MediaType.APPLICATION_JSON_VALUE) public void report(@RequestBody String content) throws IOException { JSONObject jsonObject = new JSONObject(content); - reportManager.newReport(jsonObject); + dataManager.newReport(jsonObject); } @PreAuthorize("hasRole('REPORTER')") @@ -56,7 +56,7 @@ public class ReportService { } } if(jsonObject != null) { - reportManager.newReport(jsonObject, attachments); + dataManager.newReport(jsonObject, attachments); return ResponseEntity.ok().build(); }else { return ResponseEntity.badRequest().build(); diff --git a/backend/src/main/java/com/faendir/acra/ui/BackendUI.java b/backend/src/main/java/com/faendir/acra/ui/BackendUI.java index c5a9228..117e20f 100644 --- a/backend/src/main/java/com/faendir/acra/ui/BackendUI.java +++ b/backend/src/main/java/com/faendir/acra/ui/BackendUI.java @@ -1,12 +1,16 @@ package com.faendir.acra.ui; +import com.faendir.acra.mongod.user.UserManager; import com.faendir.acra.security.SecurityUtils; +import com.faendir.acra.ui.view.user.ChangePasswordView; +import com.faendir.acra.ui.view.user.UserManagerView; import com.faendir.acra.util.Style; import com.vaadin.annotations.Theme; import com.vaadin.server.VaadinRequest; import com.vaadin.server.VaadinService; import com.vaadin.spring.annotation.SpringUI; import com.vaadin.spring.annotation.UIScope; +import com.vaadin.ui.Alignment; import com.vaadin.ui.Button; import com.vaadin.ui.HorizontalLayout; import com.vaadin.ui.LoginForm; @@ -46,9 +50,7 @@ public class BackendUI extends UI { if (SecurityUtils.isLoggedIn()) { showMain(); } else { - LoginForm loginForm = new LoginForm(); - loginForm.addLoginListener(event -> login(event.getLoginParameter("username"), event.getLoginParameter("password"))); - setContent(loginForm); + showLogin(); } } @@ -64,10 +66,43 @@ public class BackendUI extends UI { } } + public void logout() { + SecurityContextHolder.clearContext(); + getPage().reload(); + getSession().close(); + } + + private void showLogin() { + LoginForm loginForm = new LoginForm(); + loginForm.addLoginListener(event -> login(event.getLoginParameter("username"), event.getLoginParameter("password"))); + VerticalLayout layout = new VerticalLayout(loginForm); + layout.setSizeFull(); + layout.setComponentAlignment(loginForm, Alignment.MIDDLE_CENTER); + setContent(layout); + } + private void showMain() { NavigationManager navigationManager = applicationContext.getBean(NavigationManager.class); - HorizontalLayout header = new HorizontalLayout(new Button("Up", e -> navigationManager.navigateBack())); - Style.apply(header, Style.MARGIN_TOP, Style.MARGIN_LEFT, Style.MARGIN_RIGHT); + Button up = new Button("Up", e -> navigationManager.navigateBack()); + HorizontalLayout header = new HorizontalLayout(up); + header.setExpandRatio(up, 1); + header.setWidth(100, Unit.PERCENTAGE); + + if(SecurityUtils.hasRole(UserManager.ROLE_ADMIN)){ + Button userManager = new Button("User Manager", e -> navigationManager.navigateTo(UserManagerView.class, "")); + header.addComponent(userManager); + header.setComponentAlignment(userManager, Alignment.MIDDLE_RIGHT); + } + + Button changePassword = new Button("Change Password", e-> navigationManager.navigateTo(ChangePasswordView.class, "")); + header.addComponent(changePassword); + header.setComponentAlignment(changePassword, Alignment.MIDDLE_RIGHT); + + Button logout = new Button("Logout", e -> logout()); + header.addComponent(logout); + header.setComponentAlignment(logout, Alignment.MIDDLE_RIGHT); + + Style.apply(header, Style.PADDING_TOP, Style.PADDING_LEFT, Style.PADDING_RIGHT); VerticalLayout root = new VerticalLayout(header, content); root.setExpandRatio(content, 1); root.setSizeFull(); diff --git a/backend/src/main/java/com/faendir/acra/ui/NavigationManager.java b/backend/src/main/java/com/faendir/acra/ui/NavigationManager.java index d529467..d2ad344 100644 --- a/backend/src/main/java/com/faendir/acra/ui/NavigationManager.java +++ b/backend/src/main/java/com/faendir/acra/ui/NavigationManager.java @@ -1,7 +1,9 @@ package com.faendir.acra.ui; import com.faendir.acra.gen.ViewDefinition; -import com.faendir.acra.ui.view.NamedView; +import com.faendir.acra.security.SecurityUtils; +import com.faendir.acra.ui.view.ErrorView; +import com.faendir.acra.ui.view.base.NamedView; import com.vaadin.navigator.Navigator; import com.vaadin.navigator.View; import com.vaadin.navigator.ViewProvider; @@ -37,7 +39,7 @@ public class NavigationManager { @Override public String getViewName(String viewAndParameters) { String name = viewAndParameters.split("/", 2)[0]; - if (views.stream().map(applicationContext::getBean).map(NamedView.class::cast).map(NamedView::getName).anyMatch(name::equals)) + if (views.stream().map(applicationContext::getBean).map(NamedView.class::cast).filter(view -> SecurityUtils.hasRole(view.requiredRole())).map(NamedView::getName).anyMatch(name::equals)) return name; return null; } @@ -47,6 +49,7 @@ public class NavigationManager { return views.stream().map(applicationContext::getBean).map(NamedView.class::cast).filter(view -> view.getName().equals(viewName)).findAny().orElse(null); } }); + navigator.setErrorView(ErrorView.class); views = ViewDefinition.getViewClasses(); String target = Optional.ofNullable(ui.getPage().getLocation().getFragment()).orElse("").replace("!", ""); backStack.add(target); @@ -56,8 +59,10 @@ public class NavigationManager { public void navigateTo(Class namedView, String contentId) { NamedView view = applicationContext.getBean(namedView); String target = view.getName() + (contentId == null ? "" : "/" + contentId) + view.fragmentSuffix(); - backStack.add(0, target); - navigator.navigateTo(target); + if (!backStack.get(0).equals(target)) { + backStack.add(0, target); + navigator.navigateTo(target); + } } public void navigateBack() { diff --git a/backend/src/main/java/com/faendir/acra/ui/view/AppView.java b/backend/src/main/java/com/faendir/acra/ui/view/AppView.java index 41da036..4a28ca1 100644 --- a/backend/src/main/java/com/faendir/acra/ui/view/AppView.java +++ b/backend/src/main/java/com/faendir/acra/ui/view/AppView.java @@ -1,9 +1,14 @@ package com.faendir.acra.ui.view; -import com.faendir.acra.data.App; -import com.faendir.acra.data.AppManager; -import com.faendir.acra.data.MappingManager; -import com.faendir.acra.data.ReportManager; +import com.faendir.acra.mongod.data.DataManager; +import com.faendir.acra.mongod.model.App; +import com.faendir.acra.mongod.model.Permission; +import com.faendir.acra.security.SecurityUtils; +import com.faendir.acra.ui.view.base.NamedView; +import com.faendir.acra.ui.view.base.ReportList; +import com.faendir.acra.ui.view.tabs.BugTab; +import com.faendir.acra.ui.view.tabs.DeObfuscationTab; +import com.faendir.acra.ui.view.tabs.PropertiesTab; import com.faendir.acra.util.Style; import com.vaadin.navigator.ViewChangeListener; import com.vaadin.spring.annotation.UIScope; @@ -21,15 +26,11 @@ import org.springframework.stereotype.Component; @Component public class AppView extends NamedView { - private final AppManager appManager; - private final ReportManager reportManager; - private final MappingManager mappingManager; + private final DataManager dataManager; @Autowired - public AppView(AppManager appManager, ReportManager reportManager, MappingManager mappingManager) { - this.appManager = appManager; - this.reportManager = reportManager; - this.mappingManager = mappingManager; + public AppView(DataManager dataManager) { + this.dataManager = dataManager; } @Override @@ -45,13 +46,16 @@ public class AppView extends NamedView { @Override public void enter(ViewChangeListener.ViewChangeEvent event) { String[] parameters = event.getParameters().split("/"); - App app = appManager.getApp(parameters[0]); + App app = dataManager.getApp(parameters[0]); VerticalLayout statistics = new VerticalLayout(new Label("Coming soon")); statistics.setCaption("Statistics"); statistics.setSizeFull(); - TabSheet tabSheet = new TabSheet(new BugTab(app.getId(), getNavigationManager(), reportManager), - new ReportList(app.getId(), getNavigationManager(), reportManager), - statistics, new DeObfuscationTab(app.getId(), mappingManager), new PropertiesTab(app)); + TabSheet tabSheet = new TabSheet(new BugTab(app.getId(), getNavigationManager(), dataManager), + new ReportList(app.getId(), getNavigationManager(), dataManager), + statistics, new DeObfuscationTab(app.getId(), dataManager)); + if(SecurityUtils.hasPermission(app.getId(), Permission.Level.ADMIN)){ + tabSheet.addComponent(new PropertiesTab(app, dataManager, getNavigationManager())); + } tabSheet.setSizeFull(); VerticalLayout content = new VerticalLayout(tabSheet); content.setSizeFull(); @@ -66,6 +70,7 @@ public class AppView extends NamedView { } } } - tabSheet.addSelectedTabChangeListener(e -> getUI().getPage().setUriFragment(getName() + "/" + app.getId() + "/" + tabSheet.getSelectedTab().getCaption(), false)); + tabSheet.addSelectedTabChangeListener(e -> getUI().getPage() + .setUriFragment(getName() + "/" + app.getId() + "/" + tabSheet.getSelectedTab().getCaption(), false)); } } diff --git a/backend/src/main/java/com/faendir/acra/ui/view/ErrorView.java b/backend/src/main/java/com/faendir/acra/ui/view/ErrorView.java new file mode 100644 index 0000000..030f331 --- /dev/null +++ b/backend/src/main/java/com/faendir/acra/ui/view/ErrorView.java @@ -0,0 +1,25 @@ +package com.faendir.acra.ui.view; + + +import com.vaadin.navigator.View; +import com.vaadin.navigator.ViewChangeListener; +import com.vaadin.ui.Alignment; +import com.vaadin.ui.CustomComponent; +import com.vaadin.ui.Label; +import com.vaadin.ui.VerticalLayout; + +/** + * @author Lukas + * @since 21.05.2017 + */ +public class ErrorView extends CustomComponent implements View { + @Override + public void enter(ViewChangeListener.ViewChangeEvent event) { + Label label = new Label("This page does not exist or you do not have the permission to view it."); + VerticalLayout layout = new VerticalLayout(label); + layout.setComponentAlignment(label, Alignment.MIDDLE_CENTER); + layout.setSizeFull(); + setSizeFull(); + setCompositionRoot(layout); + } +} diff --git a/backend/src/main/java/com/faendir/acra/ui/view/Overview.java b/backend/src/main/java/com/faendir/acra/ui/view/Overview.java index b7473d7..2ac2224 100644 --- a/backend/src/main/java/com/faendir/acra/ui/view/Overview.java +++ b/backend/src/main/java/com/faendir/acra/ui/view/Overview.java @@ -1,8 +1,12 @@ package com.faendir.acra.ui.view; -import com.faendir.acra.data.App; -import com.faendir.acra.data.AppManager; -import com.faendir.acra.data.ReportManager; +import com.faendir.acra.mongod.data.DataManager; +import com.faendir.acra.mongod.model.App; +import com.faendir.acra.mongod.model.Permission; +import com.faendir.acra.mongod.user.UserManager; +import com.faendir.acra.security.SecurityUtils; +import com.faendir.acra.ui.view.base.MyGrid; +import com.faendir.acra.ui.view.base.NamedView; import com.faendir.acra.util.Style; import com.vaadin.navigator.ViewChangeListener; import com.vaadin.spring.annotation.UIScope; @@ -12,10 +16,12 @@ import com.vaadin.ui.TextField; import com.vaadin.ui.UI; import com.vaadin.ui.VerticalLayout; import com.vaadin.ui.Window; -import com.vaadin.ui.components.grid.FooterCell; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; +import java.util.List; +import java.util.stream.Collectors; + /** * @author Lukas * @since 23.03.2017 @@ -24,14 +30,12 @@ import org.springframework.stereotype.Component; @Component public class Overview extends NamedView { - private final AppManager appManager; - private final ReportManager reportManager; + private final DataManager dataManager; private MyGrid grid; @Autowired - public Overview(AppManager appManager, ReportManager reportManager) { - this.appManager = appManager; - this.reportManager = reportManager; + public Overview(DataManager dataManager) { + this.dataManager = dataManager; } private void addApp() { @@ -39,9 +43,9 @@ public class Overview extends NamedView { TextField name = new TextField("Name"); Button create = new Button("Create"); create.addClickListener(e -> { - appManager.createNewApp(name.getValue()); + dataManager.createNewApp(name.getValue()); window.close(); - grid.setItems(appManager.getApps()); + grid.setItems(getApps()); }); VerticalLayout layout = new VerticalLayout(name, create); @@ -50,17 +54,22 @@ public class Overview extends NamedView { UI.getCurrent().addWindow(window); } + private List getApps(){ + return dataManager.getApps().stream().filter(app -> SecurityUtils.hasPermission(app.getId(), Permission.Level.VIEW)).collect(Collectors.toList()); + } + @Override public void enter(ViewChangeListener.ViewChangeEvent event) { - grid = new MyGrid<>("Apps", appManager.getApps()); + grid = new MyGrid<>("Apps", getApps()); grid.setSizeFull(); - Grid.Column column = grid.addColumn(App::getName, "Name"); - grid.addColumn(app -> String.valueOf(reportManager.getReports(app.getId()).size()), "Reports"); - FooterCell footerCell = grid.appendFooterRow().getCell(column.getId()); - Button add = new Button("New App", e -> addApp()); - add.setSizeFull(); - footerCell.setComponent(add); + grid.setSelectionMode(Grid.SelectionMode.NONE); + grid.addColumn(App::getName, "Name"); + grid.addColumn(app -> String.valueOf(dataManager.getReports(app.getId()).size()), "Reports"); VerticalLayout layout = new VerticalLayout(grid); + if(SecurityUtils.hasRole(UserManager.ROLE_ADMIN)){ + Button add = new Button("New App", e -> addApp()); + layout.addComponent(add); + } Style.apply(layout, Style.NO_PADDING, Style.PADDING_LEFT, Style.PADDING_RIGHT, Style.PADDING_BOTTOM); setCompositionRoot(layout); grid.addItemClickListener(e -> getNavigationManager().navigateTo(AppView.class, e.getItem().getId())); diff --git a/backend/src/main/java/com/faendir/acra/ui/view/PropertiesTab.java b/backend/src/main/java/com/faendir/acra/ui/view/PropertiesTab.java deleted file mode 100644 index 48689a4..0000000 --- a/backend/src/main/java/com/faendir/acra/ui/view/PropertiesTab.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.faendir.acra.ui.view; - -import com.faendir.acra.data.App; -import com.vaadin.shared.ui.ContentMode; -import com.vaadin.ui.Label; -import com.vaadin.ui.UI; -import com.vaadin.ui.VerticalLayout; - -/** - * @author Lukas - * @since 19.05.2017 - */ -public class PropertiesTab extends VerticalLayout { - public PropertiesTab(App app) { - String location = UI.getCurrent().getPage().getLocation().toASCIIString(); - location = location.substring(0, location.indexOf('#')); - addComponent(new Label(String.format("Required ACRA configuration:
formUri = \"%sreport\",
" + - "formUriBasicAuthLogin = \"%s\",
formUriBasicAuthPassword = \"%s\",
" + - "httpMethod = HttpSender.Method.POST,
reportType = HttpSender.Type.JSON
", - location, app.getId(), app.getPassword()), ContentMode.HTML)); - setCaption("Properties"); - setSizeFull(); - } -} diff --git a/backend/src/main/java/com/faendir/acra/ui/view/ReportList.java b/backend/src/main/java/com/faendir/acra/ui/view/ReportList.java deleted file mode 100644 index eaf76e0..0000000 --- a/backend/src/main/java/com/faendir/acra/ui/view/ReportList.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.faendir.acra.ui.view; - -import com.faendir.acra.data.Report; -import com.faendir.acra.data.ReportManager; -import com.faendir.acra.ui.NavigationManager; -import com.faendir.acra.util.StringUtils; -import com.vaadin.ui.renderers.ButtonRenderer; - -/** - * @author Lukas - * @since 14.05.2017 - */ -public class ReportList extends MyGrid implements ReportManager.ChangeListener { - private final String app; - private final ReportManager reportManager; - - public ReportList(String app, NavigationManager navigationManager, ReportManager reportManager) { - super("Reports", reportManager.getReports(app)); - this.app = app; - this.reportManager = reportManager; - setSizeFull(); - addColumn(report -> StringUtils.distanceFromNowAsString(report.getDate()), "Date"); - addColumn(report -> String.valueOf(report.getVersionCode()), "App Version"); - addColumn(Report::getAndroidVersion, "Android Version"); - addColumn(Report::getPhoneModel, "Device"); - addColumn(report -> report.getStacktrace().split("\n", 2)[0], "Stacktrace").setExpandRatio(1); - addColumn(report -> "Delete", new ButtonRenderer<>(e -> reportManager.remove(e.getItem()))); - addItemClickListener(e -> navigationManager.navigateTo(ReportView.class, e.getItem().getId())); - addAttachListener(e -> reportManager.addListener(this)); - addDetachListener(e -> reportManager.removeListener(this)); - } - - @Override - public void onChange() { - setItems(reportManager.getReports(app)); - } -} diff --git a/backend/src/main/java/com/faendir/acra/ui/view/ReportView.java b/backend/src/main/java/com/faendir/acra/ui/view/ReportView.java index f5d3880..179b06d 100644 --- a/backend/src/main/java/com/faendir/acra/ui/view/ReportView.java +++ b/backend/src/main/java/com/faendir/acra/ui/view/ReportView.java @@ -1,9 +1,8 @@ package com.faendir.acra.ui.view; -import com.faendir.acra.data.AttachmentManager; -import com.faendir.acra.data.MappingManager; -import com.faendir.acra.data.Report; -import com.faendir.acra.data.ReportManager; +import com.faendir.acra.mongod.data.DataManager; +import com.faendir.acra.mongod.model.Report; +import com.faendir.acra.ui.view.base.NamedView; import com.faendir.acra.util.Style; import com.mongodb.gridfs.GridFSDBFile; import com.vaadin.navigator.ViewChangeListener; @@ -35,15 +34,11 @@ import java.util.stream.Stream; @org.springframework.stereotype.Component public class ReportView extends NamedView { - private final ReportManager reportManager; - private final AttachmentManager attachmentManager; - private final MappingManager mappingManager; + private final DataManager dataManager; @Autowired - public ReportView(ReportManager reportManager, AttachmentManager attachmentManager, MappingManager mappingManager) { - this.reportManager = reportManager; - this.attachmentManager = attachmentManager; - this.mappingManager = mappingManager; + public ReportView(DataManager dataManager) { + this.dataManager = dataManager; } private Stream getLayoutForEntry(String key, Object value) { @@ -81,8 +76,8 @@ public class ReportView extends NamedView { @Override public void enter(ViewChangeListener.ViewChangeEvent event) { - Report report = reportManager.getReport(event.getParameters()); - List attachmentList = attachmentManager.getAttachments(report.getId()); + Report report = dataManager.getReport(event.getParameters()); + List attachmentList = dataManager.getAttachments(report.getId()); HorizontalLayout attachments = new HorizontalLayout(attachmentList.stream().map(file -> { Button button = new Button(file.getFilename()); new FileDownloader(new StreamResource(file::getInputStream, file.getFilename())).extend(button); @@ -92,7 +87,7 @@ public class ReportView extends NamedView { summaryGrid.addComponents(new Label("Version", ContentMode.PREFORMATTED), new Label(report.getVersionName(), ContentMode.PREFORMATTED)); summaryGrid.addComponents(new Label("Email", ContentMode.PREFORMATTED), new Label(report.getUserEmail(), ContentMode.PREFORMATTED)); summaryGrid.addComponents(new Label("Comment", ContentMode.PREFORMATTED), new Label(report.getUserComment(), ContentMode.PREFORMATTED)); - summaryGrid.addComponents(new Label("De-obfuscated Stacktrace", ContentMode.PREFORMATTED), new Label(report.getDeObfuscatedStacktrace(mappingManager), ContentMode.PREFORMATTED)); + summaryGrid.addComponents(new Label("De-obfuscated Stacktrace", ContentMode.PREFORMATTED), new Label(report.getDeObfuscatedStacktrace(dataManager), ContentMode.PREFORMATTED)); summaryGrid.addComponents(new Label("Attachments", ContentMode.PREFORMATTED), attachments); summaryGrid.setDefaultComponentAlignment(Alignment.MIDDLE_LEFT); summaryGrid.setSizeFull(); diff --git a/backend/src/main/java/com/faendir/acra/ui/view/MyGrid.java b/backend/src/main/java/com/faendir/acra/ui/view/base/MyGrid.java similarity index 83% rename from backend/src/main/java/com/faendir/acra/ui/view/MyGrid.java rename to backend/src/main/java/com/faendir/acra/ui/view/base/MyGrid.java index dc1ad6f..32ee7a9 100644 --- a/backend/src/main/java/com/faendir/acra/ui/view/MyGrid.java +++ b/backend/src/main/java/com/faendir/acra/ui/view/base/MyGrid.java @@ -1,4 +1,4 @@ -package com.faendir.acra.ui.view; +package com.faendir.acra.ui.view.base; import com.vaadin.data.ValueProvider; import com.vaadin.ui.Grid; @@ -10,6 +10,10 @@ import java.util.Collection; * @since 14.05.2017 */ public class MyGrid extends Grid { + public MyGrid(String caption) { + super(caption); + } + public MyGrid(String caption, Collection items) { super(caption, items); } diff --git a/backend/src/main/java/com/faendir/acra/ui/view/NamedView.java b/backend/src/main/java/com/faendir/acra/ui/view/base/NamedView.java similarity index 78% rename from backend/src/main/java/com/faendir/acra/ui/view/NamedView.java rename to backend/src/main/java/com/faendir/acra/ui/view/base/NamedView.java index b11363b..5a4a881 100644 --- a/backend/src/main/java/com/faendir/acra/ui/view/NamedView.java +++ b/backend/src/main/java/com/faendir/acra/ui/view/base/NamedView.java @@ -1,6 +1,7 @@ -package com.faendir.acra.ui.view; +package com.faendir.acra.ui.view.base; import com.faendir.acra.annotation.AutoDiscoverView; +import com.faendir.acra.mongod.user.UserManager; import com.faendir.acra.ui.NavigationManager; import com.vaadin.navigator.View; import com.vaadin.ui.CustomComponent; @@ -23,7 +24,11 @@ public abstract class NamedView extends CustomComponent implements View { return ""; } - NavigationManager getNavigationManager() { + public String requiredRole(){ + return UserManager.ROLE_USER; + } + + protected NavigationManager getNavigationManager() { return navigationManager; } diff --git a/backend/src/main/java/com/faendir/acra/ui/view/base/ReportList.java b/backend/src/main/java/com/faendir/acra/ui/view/base/ReportList.java new file mode 100644 index 0000000..ea2ba0a --- /dev/null +++ b/backend/src/main/java/com/faendir/acra/ui/view/base/ReportList.java @@ -0,0 +1,43 @@ +package com.faendir.acra.ui.view.base; + +import com.faendir.acra.mongod.data.DataManager; +import com.faendir.acra.mongod.model.Permission; +import com.faendir.acra.mongod.model.Report; +import com.faendir.acra.security.SecurityUtils; +import com.faendir.acra.ui.NavigationManager; +import com.faendir.acra.ui.view.ReportView; +import com.faendir.acra.util.StringUtils; +import com.vaadin.ui.renderers.ButtonRenderer; + +/** + * @author Lukas + * @since 14.05.2017 + */ +public class ReportList extends MyGrid implements DataManager.ReportChangeListener { + private final String app; + private final DataManager dataManager; + + public ReportList(String app, NavigationManager navigationManager, DataManager dataManager) { + super("Reports", dataManager.getReports(app)); + this.app = app; + this.dataManager = dataManager; + setSizeFull(); + setSelectionMode(SelectionMode.NONE); + addColumn(report -> StringUtils.distanceFromNowAsString(report.getDate()), "Date"); + addColumn(report -> String.valueOf(report.getVersionCode()), "App Version"); + addColumn(Report::getAndroidVersion, "Android Version"); + addColumn(Report::getPhoneModel, "Device"); + addColumn(report -> report.getStacktrace().split("\n", 2)[0], "Stacktrace").setExpandRatio(1); + if(SecurityUtils.hasPermission(app, Permission.Level.EDIT)) { + addColumn(report -> "Delete", new ButtonRenderer<>(e -> dataManager.remove(e.getItem()))); + } + addItemClickListener(e -> navigationManager.navigateTo(ReportView.class, e.getItem().getId())); + addAttachListener(e -> dataManager.addListener(this)); + addDetachListener(e -> dataManager.removeListener(this)); + } + + @Override + public void onChange() { + setItems(dataManager.getReports(app)); + } +} diff --git a/backend/src/main/java/com/faendir/acra/ui/view/BugTab.java b/backend/src/main/java/com/faendir/acra/ui/view/tabs/BugTab.java similarity index 65% rename from backend/src/main/java/com/faendir/acra/ui/view/BugTab.java rename to backend/src/main/java/com/faendir/acra/ui/view/tabs/BugTab.java index b1da601..344eeb0 100644 --- a/backend/src/main/java/com/faendir/acra/ui/view/BugTab.java +++ b/backend/src/main/java/com/faendir/acra/ui/view/tabs/BugTab.java @@ -1,9 +1,11 @@ -package com.faendir.acra.ui.view; +package com.faendir.acra.ui.view.tabs; -import com.faendir.acra.data.Bug; -import com.faendir.acra.data.ReportManager; -import com.faendir.acra.data.ReportUtils; +import com.faendir.acra.mongod.data.DataManager; +import com.faendir.acra.mongod.model.Bug; +import com.faendir.acra.mongod.data.ReportUtils; import com.faendir.acra.ui.NavigationManager; +import com.faendir.acra.ui.view.base.MyGrid; +import com.faendir.acra.ui.view.base.ReportList; import com.faendir.acra.util.StringUtils; import com.faendir.acra.util.Style; import com.vaadin.event.selection.SelectionEvent; @@ -14,19 +16,19 @@ import com.vaadin.ui.VerticalLayout; * @author Lukas * @since 17.05.2017 */ -public class BugTab extends CustomComponent implements ReportManager.ChangeListener { +public class BugTab extends CustomComponent implements DataManager.ReportChangeListener { public static final String CAPTION = "Bugs"; private final VerticalLayout root; private final String app; private final NavigationManager navigationManager; - private final ReportManager reportManager; + private final DataManager dataManager; private final MyGrid bugs; - public BugTab(String app, NavigationManager navigationManager, ReportManager reportManager) { + public BugTab(String app, NavigationManager navigationManager, DataManager dataManager) { this.app = app; this.navigationManager = navigationManager; - this.reportManager = reportManager; - bugs = new MyGrid<>(null, ReportUtils.getBugs(reportManager.getReports(app))); + this.dataManager = dataManager; + bugs = new MyGrid<>(null, ReportUtils.getBugs(dataManager.getReports(app))); bugs.setSizeFull(); bugs.addColumn(bug -> String.valueOf(bug.getReports().size()), "Reports"); bugs.addColumn(bug -> StringUtils.distanceFromNowAsString(bug.getLastDate()), "Latest Report"); @@ -39,19 +41,19 @@ public class BugTab extends CustomComponent implements ReportManager.ChangeListe setCompositionRoot(root); setSizeFull(); setCaption(CAPTION); - addAttachListener(e -> reportManager.addListener(this)); - addDetachListener(e -> reportManager.removeListener(this)); + addAttachListener(e -> dataManager.addListener(this)); + addDetachListener(e -> dataManager.removeListener(this)); } private void handleBugSelection(SelectionEvent e) { if(root.getComponentCount() == 2){ root.removeComponent(root.getComponent(1)); } - e.getFirstSelectedItem().ifPresent(bug -> root.addComponent(new ReportList(app, navigationManager, reportManager))); + e.getFirstSelectedItem().ifPresent(bug -> root.addComponent(new ReportList(app, navigationManager, dataManager))); } @Override public void onChange() { - bugs.setItems(ReportUtils.getBugs(reportManager.getReports(app))); + bugs.setItems(ReportUtils.getBugs(dataManager.getReports(app))); } } diff --git a/backend/src/main/java/com/faendir/acra/ui/view/DeObfuscationTab.java b/backend/src/main/java/com/faendir/acra/ui/view/tabs/DeObfuscationTab.java similarity index 60% rename from backend/src/main/java/com/faendir/acra/ui/view/DeObfuscationTab.java rename to backend/src/main/java/com/faendir/acra/ui/view/tabs/DeObfuscationTab.java index a15cbf4..dcf3b5b 100644 --- a/backend/src/main/java/com/faendir/acra/ui/view/DeObfuscationTab.java +++ b/backend/src/main/java/com/faendir/acra/ui/view/tabs/DeObfuscationTab.java @@ -1,8 +1,13 @@ -package com.faendir.acra.ui.view; +package com.faendir.acra.ui.view.tabs; -import com.faendir.acra.data.MappingManager; -import com.faendir.acra.data.ProguardMapping; +import com.faendir.acra.mongod.data.DataManager; +import com.faendir.acra.mongod.model.Permission; +import com.faendir.acra.mongod.model.ProguardMapping; +import com.faendir.acra.security.SecurityUtils; +import com.faendir.acra.ui.view.base.MyGrid; +import com.faendir.acra.util.Style; import com.vaadin.ui.Button; +import com.vaadin.ui.CustomComponent; import com.vaadin.ui.ProgressBar; import com.vaadin.ui.TextField; import com.vaadin.ui.UI; @@ -17,21 +22,28 @@ import java.io.ByteArrayOutputStream; * @author Lukas * @since 19.05.2017 */ -public class DeObfuscationTab extends MyGrid { +public class DeObfuscationTab extends CustomComponent { private final String app; - private final MappingManager mappingManager; + private final DataManager dataManager; + private final MyGrid grid; private boolean validNumber; private boolean validFile; - public DeObfuscationTab(String app, MappingManager mappingManager) { - super("De-Obfuscation", mappingManager.getMappings(app)); + public DeObfuscationTab(String app, DataManager dataManager) { + setCaption("De-Obfuscation"); + grid = new MyGrid<>(null, dataManager.getMappings(app)); this.app = app; - this.mappingManager = mappingManager; - Column column = addColumn(mapping -> String.valueOf(mapping.getVersion()), "Version"); - setSizeFull(); - Button add = new Button("Add File", e -> addFile()); - add.setSizeFull(); - appendFooterRow().getCell(column).setComponent(add); + this.dataManager = dataManager; + grid.addColumn(mapping -> String.valueOf(mapping.getVersion()), "Version"); + grid.setSizeFull(); + VerticalLayout layout = new VerticalLayout(grid); + layout.setSizeFull(); + Style.NO_PADDING.apply(layout); + setCompositionRoot(layout); + if (SecurityUtils.hasPermission(app, Permission.Level.EDIT)) { + layout.addComponent(new Button("Add File", e -> addFile())); + } + } private void addFile() { @@ -64,8 +76,8 @@ public class DeObfuscationTab extends MyGrid { }); upload.setSizeFull(); confirm.addClickListener(e -> { - mappingManager.addMapping(app, Integer.parseInt(version.getValue()), out.toString()); - setItems(mappingManager.getMappings(app)); + dataManager.addMapping(app, Integer.parseInt(version.getValue()), out.toString()); + grid.setItems(dataManager.getMappings(app)); window.close(); }); confirm.setSizeFull(); diff --git a/backend/src/main/java/com/faendir/acra/ui/view/tabs/PropertiesTab.java b/backend/src/main/java/com/faendir/acra/ui/view/tabs/PropertiesTab.java new file mode 100644 index 0000000..e642046 --- /dev/null +++ b/backend/src/main/java/com/faendir/acra/ui/view/tabs/PropertiesTab.java @@ -0,0 +1,52 @@ +package com.faendir.acra.ui.view.tabs; + +import com.faendir.acra.mongod.data.DataManager; +import com.faendir.acra.mongod.model.App; +import com.faendir.acra.ui.NavigationManager; +import com.vaadin.shared.ui.ContentMode; +import com.vaadin.ui.Button; +import com.vaadin.ui.HorizontalLayout; +import com.vaadin.ui.Label; +import com.vaadin.ui.UI; +import com.vaadin.ui.VerticalLayout; +import com.vaadin.ui.Window; + +/** + * @author Lukas + * @since 19.05.2017 + */ +public class PropertiesTab extends VerticalLayout { + private final App app; + private final DataManager dataManager; + private final NavigationManager navigationManager; + + public PropertiesTab(App app, DataManager dataManager, NavigationManager navigationManager) { + this.app = app; + this.dataManager = dataManager; + this.navigationManager = navigationManager; + String location = UI.getCurrent().getPage().getLocation().toASCIIString(); + location = location.substring(0, location.indexOf('#')); + addComponent(new Label(String.format("Required ACRA configuration:
formUri = \"%sreport\",
" + + "formUriBasicAuthLogin = \"%s\",
formUriBasicAuthPassword = \"%s\",
" + + "httpMethod = HttpSender.Method.POST,
reportType = HttpSender.Type.JSON
", + location, app.getId(), app.getPassword()), ContentMode.HTML)); + addComponent(new Button("Delete App", e -> deleteApp())); + setCaption("Properties"); + setSizeUndefined(); + } + + private void deleteApp() { + Window window = new Window("Confirm"); + Label label = new Label("Are you sure you want to delete this app and all its reports and mappings?"); + Button yes = new Button("Yes", e -> { + dataManager.deleteApp(app.getId()); + window.close(); + navigationManager.navigateBack(); + }); + Button no = new Button("No", e -> window.close()); + VerticalLayout layout = new VerticalLayout(label, new HorizontalLayout(yes, no)); + window.setContent(layout); + window.center(); + UI.getCurrent().addWindow(window); + } +} diff --git a/backend/src/main/java/com/faendir/acra/ui/view/user/ChangePasswordView.java b/backend/src/main/java/com/faendir/acra/ui/view/user/ChangePasswordView.java new file mode 100644 index 0000000..d9c21b5 --- /dev/null +++ b/backend/src/main/java/com/faendir/acra/ui/view/user/ChangePasswordView.java @@ -0,0 +1,72 @@ +package com.faendir.acra.ui.view.user; + +import com.faendir.acra.mongod.model.User; +import com.faendir.acra.mongod.user.UserManager; +import com.faendir.acra.security.SecurityUtils; +import com.faendir.acra.ui.BackendUI; +import com.faendir.acra.ui.view.base.NamedView; +import com.vaadin.navigator.ViewChangeListener; +import com.vaadin.server.UserError; +import com.vaadin.spring.annotation.UIScope; +import com.vaadin.ui.Alignment; +import com.vaadin.ui.Button; +import com.vaadin.ui.Notification; +import com.vaadin.ui.PasswordField; +import com.vaadin.ui.VerticalLayout; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +/** + * @author Lukas + * @since 21.05.2017 + */ +@UIScope +@Component +public class ChangePasswordView extends NamedView { + + private final UserManager userManager; + private final BackendUI backendUI; + + @Autowired + public ChangePasswordView(UserManager userManager, BackendUI backendUI) { + + this.userManager = userManager; + this.backendUI = backendUI; + } + + @Override + public String getName() { + return "password-editor"; + } + + @Override + public void enter(ViewChangeListener.ViewChangeEvent event) { + PasswordField oldPassword = new PasswordField("Old Password"); + PasswordField newPassword = new PasswordField("New Password"); + PasswordField repeatPassword = new PasswordField("Repeat Password"); + Button confirm = new Button("Confirm", e -> { + User user = userManager.getUser(SecurityUtils.getUsername()); + if(userManager.checkPassword(user, oldPassword.getValue())) { + if (newPassword.getValue().equals(repeatPassword.getValue())) { + user.setPassword(newPassword.getValue()); + Notification.show("Successful!"); + getNavigationManager().navigateBack(); + backendUI.logout(); + } else { + repeatPassword.setComponentError(new UserError("Passwords do not match")); + } + }else { + oldPassword.setComponentError(new UserError("Incorrect password")); + } + + }); + confirm.setSizeFull(); + VerticalLayout layout = new VerticalLayout(oldPassword, newPassword, repeatPassword, confirm); + layout.setSizeUndefined(); + VerticalLayout root = new VerticalLayout(layout); + root.setSizeFull(); + root.setComponentAlignment(layout, Alignment.MIDDLE_CENTER); + setSizeFull(); + setCompositionRoot(root); + } +} diff --git a/backend/src/main/java/com/faendir/acra/ui/view/user/PermissionEditor.java b/backend/src/main/java/com/faendir/acra/ui/view/user/PermissionEditor.java new file mode 100644 index 0000000..d284a1e --- /dev/null +++ b/backend/src/main/java/com/faendir/acra/ui/view/user/PermissionEditor.java @@ -0,0 +1,86 @@ +package com.faendir.acra.ui.view.user; + +import com.faendir.acra.mongod.data.DataManager; +import com.faendir.acra.mongod.model.Permission; +import com.faendir.acra.ui.view.base.MyGrid; +import com.vaadin.data.HasValue; +import com.vaadin.shared.Registration; +import com.vaadin.ui.ComboBox; +import com.vaadin.ui.Grid; +import org.vaadin.hene.popupbutton.PopupButton; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.stream.Collectors; + +/** + * @author Lukas + * @since 20.05.2017 + */ +public class PermissionEditor extends PopupButton implements HasValue> { + + private Collection items; + private MyGrid grid; + + public PermissionEditor(DataManager dataManager) { + super("Editor"); + items = new ArrayList<>(); + grid = new MyGrid<>(null, items); + grid.addColumn(permission -> dataManager.getApp(permission.getApp()).getName(), "App"); + ComboBox levelComboBox = new ComboBox<>("", Arrays.asList(Permission.Level.values())); + grid.addColumn(permission -> permission.getLevel().name(), "Level") + .setEditorBinding(grid.getEditor().getBinder().bind(levelComboBox, Permission::getLevel, (permission, level) -> { + Collection newValue = items.stream().map(p -> new Permission(p.getApp(), p.getLevel())).collect(Collectors.toList()); + newValue.stream().filter(p -> p.getApp().equals(permission.getApp())).findAny().ifPresent(p -> p.setLevel(level)); + setValue(newValue, true); + })); + grid.getEditor().setEnabled(true).setBuffered(false); + grid.setSelectionMode(Grid.SelectionMode.NONE); + setContent(grid); + setPopupVisible(true); + } + + @Override + public void setValue(Collection value) { + setValue(value, false); + } + + private void setValue(Collection value, boolean userOriginated) { + Collection oldValue = items; + items = value; + grid.setItems(items); + grid.setHeightByRows(items.size()); + fireEvent(new ValueChangeEvent<>(this, oldValue, userOriginated)); + } + + @Override + public Collection getValue() { + return items; + } + + @Override + public void setRequiredIndicatorVisible(boolean requiredIndicatorVisible) { + } + + @Override + public boolean isRequiredIndicatorVisible() { + return false; + } + + @Override + public Registration addValueChangeListener(ValueChangeListener> listener) { + return addListener(ValueChangeEvent.class, listener, + ValueChangeListener.VALUE_CHANGE_METHOD); + } + + @Override + public void setReadOnly(boolean readOnly) { + + } + + @Override + public boolean isReadOnly() { + return false; + } +} diff --git a/backend/src/main/java/com/faendir/acra/ui/view/user/UserManagerView.java b/backend/src/main/java/com/faendir/acra/ui/view/user/UserManagerView.java new file mode 100644 index 0000000..17f92bf --- /dev/null +++ b/backend/src/main/java/com/faendir/acra/ui/view/user/UserManagerView.java @@ -0,0 +1,106 @@ +package com.faendir.acra.ui.view.user; + +import com.faendir.acra.mongod.data.DataManager; +import com.faendir.acra.mongod.model.User; +import com.faendir.acra.mongod.user.UserManager; +import com.faendir.acra.security.SecurityUtils; +import com.faendir.acra.ui.view.base.MyGrid; +import com.faendir.acra.ui.view.base.NamedView; +import com.faendir.acra.util.Style; +import com.vaadin.data.Binder; +import com.vaadin.navigator.ViewChangeListener; +import com.vaadin.server.UserError; +import com.vaadin.spring.annotation.UIScope; +import com.vaadin.ui.Button; +import com.vaadin.ui.CheckBox; +import com.vaadin.ui.Grid; +import com.vaadin.ui.PasswordField; +import com.vaadin.ui.TextField; +import com.vaadin.ui.UI; +import com.vaadin.ui.VerticalLayout; +import com.vaadin.ui.Window; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.util.stream.Collectors; + +/** + * @author Lukas + * @since 20.05.2017 + */ +@UIScope +@Component +public class UserManagerView extends NamedView { + private final UserManager userManager; + private final DataManager dataManager; + private MyGrid userGrid; + + @Autowired + public UserManagerView(UserManager userManager, DataManager dataManager) { + this.userManager = userManager; + this.dataManager = dataManager; + } + + @Override + public String getName() { + return "user-manager"; + } + + @Override + public String requiredRole() { + return UserManager.ROLE_ADMIN; + } + + @Override + public void enter(ViewChangeListener.ViewChangeEvent event) { + userGrid = new MyGrid<>("Users", userManager.getUsers()); + userGrid.getEditor().setEnabled(true).setBuffered(false); + userGrid.setSelectionMode(Grid.SelectionMode.NONE); + userGrid.addColumn(User::getUsername, "Username"); + Binder binder = userGrid.getEditor().getBinder(); + userGrid.addColumn(user -> user.getRoles().contains(UserManager.ROLE_ADMIN) ? "Yes" : "No").setCaption("Admin") + .setEditorBinding(binder.forField(new CheckBox()) + .withValidator(bool -> bool || !binder.getBean().getUsername().equals(SecurityUtils.getUsername()), "Cannot revoke own admin privileges") + .bind(user -> user.getRoles().contains(UserManager.ROLE_ADMIN), userManager::setAdmin)); + userGrid.addColumn(this::getPermissionString, "App Permissions") + .setEditorBinding(userGrid.getEditor().getBinder().bind(new PermissionEditor(dataManager), User::getPermissions, + ((user, permissions) -> permissions.forEach(p -> userManager.setPermission(user, p.getApp(), p.getLevel()))))); + Button newUser = new Button("New User", e -> newUser()); + VerticalLayout layout = new VerticalLayout(userGrid, newUser); + layout.setSizeFull(); + Style.NO_PADDING.apply(layout); + setCompositionRoot(layout); + userGrid.setSizeFull(); + setSizeFull(); + Style.apply(this, Style.PADDING_LEFT, Style.PADDING_RIGHT, Style.PADDING_BOTTOM); + } + + private String getPermissionString(User user) { + return user.getPermissions().stream() + .map(permission -> dataManager.getApp(permission.getApp()).getName() + ": " + permission.getLevel().name()) + .collect(Collectors.joining(", ")); + } + + private void newUser() { + Window window = new Window("New User"); + TextField name = new TextField("Username"); + PasswordField password = new PasswordField("Password"); + PasswordField repeatPassword = new PasswordField("Repeat Password"); + Button create = new Button("Create"); + create.addClickListener(e -> { + if (password.getValue().equals(repeatPassword.getValue())) { + userManager.createUser(name.getValue(), password.getValue()); + userGrid.setItems(userManager.getUsers()); + window.close(); + } else { + repeatPassword.setComponentError(new UserError("Passwords do not match")); + } + + }); + VerticalLayout layout = new VerticalLayout(name, password, repeatPassword, create); + window.setContent(layout); + window.center(); + UI.getCurrent().addWindow(window); + } + +} diff --git a/backend/src/main/java/org/apache/commons/fileupload/Rfc1341ServletFileUpload.java b/backend/src/main/java/org/apache/commons/fileupload/Rfc1341ServletFileUpload.java index e885bf7..206fbfb 100644 --- a/backend/src/main/java/org/apache/commons/fileupload/Rfc1341ServletFileUpload.java +++ b/backend/src/main/java/org/apache/commons/fileupload/Rfc1341ServletFileUpload.java @@ -30,8 +30,6 @@ public class Rfc1341ServletFileUpload extends ServletFileUpload { /** * Modified copy (with appropriate cast) of {@link FileUploadBase#parseRequest(RequestContext)} - * - * @inheritDoc */ @Override public List parseRequest(RequestContext ctx) throws FileUploadException { diff --git a/backend/src/main/resources/AppWidgetset.gwt.xml b/backend/src/main/resources/AppWidgetset.gwt.xml new file mode 100644 index 0000000..308fc4e --- /dev/null +++ b/backend/src/main/resources/AppWidgetset.gwt.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/build.gradle b/build.gradle index 06b7a9d..8033eb8 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,9 @@ allprojects{ repositories { mavenCentral() + maven { + url = "http://maven.vaadin.com/vaadin-addons" + } } }