From c1fe54ecf3a9b9f7264945305af4b99f22128c80 Mon Sep 17 00:00:00 2001 From: Khakers <22665282+khakers@users.noreply.github.com> Date: Wed, 16 Aug 2023 18:06:50 -0700 Subject: [PATCH 01/21] refactor: refactor Admin page classes for dependency injection --- .../com/github/khakers/modmailviewer/Main.java | 9 ++++++--- .../modmailviewer/auditlog/AuditEventDAO.java | 15 +++++++++++++++ .../auditlog/MongoAuditEventLogger.java | 5 ++++- .../modmailviewer/page/admin/AdminController.java | 12 ++++++++---- .../modmailviewer/page/admin/AdminPage.java | 5 ++--- 5 files changed, 35 insertions(+), 11 deletions(-) create mode 100644 src/main/java/com/github/khakers/modmailviewer/auditlog/AuditEventDAO.java diff --git a/src/main/java/com/github/khakers/modmailviewer/Main.java b/src/main/java/com/github/khakers/modmailviewer/Main.java index 65aa4355..0408b185 100644 --- a/src/main/java/com/github/khakers/modmailviewer/Main.java +++ b/src/main/java/com/github/khakers/modmailviewer/Main.java @@ -101,11 +101,11 @@ public class Main { public static final ModMailLogDB MOD_MAIL_LOG_CLIENT = new ModMailLogDB(MODMAIL_DATABASE); // We will always need an audit logger for searching, even if pushing to an audit logger is disabled - public static MongoAuditEventLogger AuditLogClient = new MongoAuditEventLogger(mongoClient, Config.MONGODB_URI, "modmail_bot", "audit_log"); + public static AuditEventDAO AuditLogClient = new MongoAuditEventLogger(mongoClient, Config.MONGODB_URI, "modmail_bot", "audit_log"); public static final OutboundAuditEventLogger auditLogger = Config.isAuthEnabled - ? AuditLogClient - : new NoopAuditEventLogger(); + ? (OutboundAuditEventLogger) AuditLogClient + : new NoopAuditEventLogger(); static final AuthHandler authHandler = Config.isAuthEnabled ? @@ -132,6 +132,8 @@ public static void main(String[] args) { registerValidators(); + var adminController = new AdminController(AuditLogClient); + JavalinJte.init(templateEngine); var app = Javalin.create(Main::configure) .get("/hello", ctx -> ctx.status(200).result("hello"), RoleUtils.anyone()) @@ -167,6 +169,7 @@ public static void main(String[] args) { }) .get("/admin", AdminController.serveAdminPage, RoleUtils.atLeastAdministrator()) .get("/audit/{id}", AuditController.serveAuditPage, RoleUtils.atLeastAdministrator()) + .get("/admin", adminController.serveAdminPage, RoleUtils.atLeastAdministrator()) .after("/api/*", ctx -> { if (Config.isApiAuditingEnabled) { if (ctx.statusCode() == HttpStatus.FORBIDDEN.getCode()) { diff --git a/src/main/java/com/github/khakers/modmailviewer/auditlog/AuditEventDAO.java b/src/main/java/com/github/khakers/modmailviewer/auditlog/AuditEventDAO.java new file mode 100644 index 00000000..9ad9b222 --- /dev/null +++ b/src/main/java/com/github/khakers/modmailviewer/auditlog/AuditEventDAO.java @@ -0,0 +1,15 @@ +package com.github.khakers.modmailviewer.auditlog; + +import com.github.khakers.modmailviewer.auditlog.event.AuditEvent; + +import java.time.Instant; +import java.util.List; +import java.util.Optional; + +public interface AuditEventDAO { + List getAuditEvents(); + + Optional getAuditEvent(String id); + + List searchAuditEvents(Instant rangeStart, Instant rangeEnd, List userIds, List actions); +} diff --git a/src/main/java/com/github/khakers/modmailviewer/auditlog/MongoAuditEventLogger.java b/src/main/java/com/github/khakers/modmailviewer/auditlog/MongoAuditEventLogger.java index 1a1e83e5..7f26e439 100644 --- a/src/main/java/com/github/khakers/modmailviewer/auditlog/MongoAuditEventLogger.java +++ b/src/main/java/com/github/khakers/modmailviewer/auditlog/MongoAuditEventLogger.java @@ -28,7 +28,7 @@ import java.util.Optional; import java.util.stream.Collectors; -public class MongoAuditEventLogger implements OutboundAuditEventLogger { +public class MongoAuditEventLogger implements OutboundAuditEventLogger, AuditEventDAO { private static final Logger logger = LogManager.getLogger(); @@ -66,15 +66,18 @@ public MongoAuditEventLogger(MongoClient mongoClient, String connectionString, S } + @Override public List getAuditEvents() { return this.auditCollection.find().into(new ArrayList<>()); } + @Override public Optional getAuditEvent(String id) { logger.debug("Getting audit event with id: {}", id); return Optional.ofNullable(this.auditCollection.find(Filters.eq("_id", new ObjectId(id))).first()); } + @Override public List searchAuditEvents(Instant rangeStart, Instant rangeEnd, List userIds, List actions) { var timeFilter = Filters.and( Filters.gte("timestamp", rangeStart), diff --git a/src/main/java/com/github/khakers/modmailviewer/page/admin/AdminController.java b/src/main/java/com/github/khakers/modmailviewer/page/admin/AdminController.java index a7e0d891..960a8f26 100644 --- a/src/main/java/com/github/khakers/modmailviewer/page/admin/AdminController.java +++ b/src/main/java/com/github/khakers/modmailviewer/page/admin/AdminController.java @@ -1,5 +1,6 @@ package com.github.khakers.modmailviewer.page.admin; +import com.github.khakers.modmailviewer.auditlog.AuditEventDAO; import io.javalin.http.Handler; import java.time.LocalDate; @@ -7,6 +8,7 @@ import java.time.ZoneId; import java.time.ZoneOffset; import java.util.List; +import java.util.Objects; import java.util.regex.MatchResult; import java.util.regex.Pattern; @@ -14,7 +16,8 @@ public class AdminController { static final Pattern userPattern = Pattern.compile("(\\d{16,20})[,\\s]*?"); static final Pattern actionPattern = Pattern.compile("([\\w.]+)[,\\s]*?"); - public static Handler serveAdminPage = ctx -> { + private AuditEventDAO auditLogClient; + public Handler serveAdminPage = ctx -> { var tz = ctx.queryParamAsClass("tz", ZoneId.class).getOrDefault(ZoneOffset.UTC); @@ -54,12 +57,13 @@ public class AdminController { actions = List.of(); } - var page = new AdminPage(ctx, rangeStart, rangeEnd, users, actions, tz); + var page = new AdminPage(ctx, rangeStart, rangeEnd, users, actions, tz, this.auditLogClient.searchAuditEvents(rangeStart, rangeEnd, users, actions)); page.render(); }; - public AdminController() { - + public AdminController(AuditEventDAO auditLogClient) { + Objects.requireNonNull(auditLogClient, "auditLogClient cannot be null"); + this.auditLogClient = auditLogClient; } } diff --git a/src/main/java/com/github/khakers/modmailviewer/page/admin/AdminPage.java b/src/main/java/com/github/khakers/modmailviewer/page/admin/AdminPage.java index 009effe1..07beba72 100644 --- a/src/main/java/com/github/khakers/modmailviewer/page/admin/AdminPage.java +++ b/src/main/java/com/github/khakers/modmailviewer/page/admin/AdminPage.java @@ -1,6 +1,5 @@ package com.github.khakers.modmailviewer.page.admin; -import com.github.khakers.modmailviewer.Main; import com.github.khakers.modmailviewer.Page; import com.github.khakers.modmailviewer.auditlog.event.AuditEvent; import io.javalin.http.Context; @@ -24,14 +23,14 @@ public class AdminPage extends Page { public final List auditEvents; - AdminPage(Context ctx, Instant rangeStartTime, Instant rangeEndTime, List users, List actions, ZoneId tz) { + AdminPage(Context ctx, Instant rangeStartTime, Instant rangeEndTime, List users, List actions, ZoneId tz, List auditEvents) { super(ctx); this.startTime = rangeStartTime; this.endTime = rangeEndTime; this.users = users; this.actions = actions; this.tz = tz; - auditEvents = Main.AuditLogClient.searchAuditEvents(rangeStartTime, rangeEndTime, users, actions); + this.auditEvents = auditEvents; } @Override From 0c28772ccad6601d1b999464803c84f42a51822d Mon Sep 17 00:00:00 2001 From: Khakers <22665282+khakers@users.noreply.github.com> Date: Wed, 16 Aug 2023 18:09:34 -0700 Subject: [PATCH 02/21] refactor: use dependency injection on AuditController --- .../java/com/github/khakers/modmailviewer/Main.java | 4 ++-- .../modmailviewer/page/audit/AuditController.java | 12 +++++++++--- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/github/khakers/modmailviewer/Main.java b/src/main/java/com/github/khakers/modmailviewer/Main.java index 0408b185..500b6277 100644 --- a/src/main/java/com/github/khakers/modmailviewer/Main.java +++ b/src/main/java/com/github/khakers/modmailviewer/Main.java @@ -133,6 +133,7 @@ public static void main(String[] args) { registerValidators(); var adminController = new AdminController(AuditLogClient); + var auditController = new AuditController(AuditLogClient); JavalinJte.init(templateEngine); var app = Javalin.create(Main::configure) @@ -167,9 +168,8 @@ public static void main(String[] args) { auditLogger.pushAuditEventWithContext(ctx, "log.accessed", String.format("accessed log id %s", ctx.pathParam("id"))); } }) - .get("/admin", AdminController.serveAdminPage, RoleUtils.atLeastAdministrator()) - .get("/audit/{id}", AuditController.serveAuditPage, RoleUtils.atLeastAdministrator()) .get("/admin", adminController.serveAdminPage, RoleUtils.atLeastAdministrator()) + .get("/audit/{id}", auditController.serveAuditPage, RoleUtils.atLeastAdministrator()) .after("/api/*", ctx -> { if (Config.isApiAuditingEnabled) { if (ctx.statusCode() == HttpStatus.FORBIDDEN.getCode()) { diff --git a/src/main/java/com/github/khakers/modmailviewer/page/audit/AuditController.java b/src/main/java/com/github/khakers/modmailviewer/page/audit/AuditController.java index ddc097ae..45fd920f 100644 --- a/src/main/java/com/github/khakers/modmailviewer/page/audit/AuditController.java +++ b/src/main/java/com/github/khakers/modmailviewer/page/audit/AuditController.java @@ -1,13 +1,19 @@ package com.github.khakers.modmailviewer.page.audit; -import com.github.khakers.modmailviewer.Main; +import com.github.khakers.modmailviewer.auditlog.AuditEventDAO; import io.javalin.http.Handler; public class AuditController { - public static Handler serveAuditPage = ctx -> { + private AuditEventDAO auditEventDAO; + + public AuditController(AuditEventDAO auditEventDAO) { + this.auditEventDAO = auditEventDAO; + } + + public Handler serveAuditPage = ctx -> { var id = ctx.pathParam("id"); - var event = Main.AuditLogClient.getAuditEvent(id); + var event = this.auditEventDAO.getAuditEvent(id); if (event.isEmpty()) { ctx.status(404); return; From 09edd56ec30fff69e5274ab861a4f32187bb381d Mon Sep 17 00:00:00 2001 From: Khakers <22665282+khakers@users.noreply.github.com> Date: Wed, 16 Aug 2023 18:57:30 -0700 Subject: [PATCH 03/21] refactor: add builder to AuditEvent --- .../auditlog/event/AuditEvent.java | 115 ++++++++++++++++-- 1 file changed, 106 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/github/khakers/modmailviewer/auditlog/event/AuditEvent.java b/src/main/java/com/github/khakers/modmailviewer/auditlog/event/AuditEvent.java index 9400ed15..1145378f 100644 --- a/src/main/java/com/github/khakers/modmailviewer/auditlog/event/AuditEvent.java +++ b/src/main/java/com/github/khakers/modmailviewer/auditlog/event/AuditEvent.java @@ -2,20 +2,117 @@ import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonProperty; +import com.github.khakers.modmailviewer.auth.Role; +import io.javalin.http.Context; import org.bson.codecs.pojo.annotations.BsonProperty; import org.mongojack.ObjectId; import java.time.Instant; +import java.util.Objects; public record AuditEvent( - @JsonProperty("_id") - @BsonProperty("_id") - @ObjectId - org.bson.types.ObjectId id, - String action, - @JsonFormat(shape = JsonFormat.Shape.NUMBER) - Instant timestamp, - String description, - AuditEventSource actor + @JsonProperty("_id") + @BsonProperty("_id") + @ObjectId + org.bson.types.ObjectId id, + String action, + @JsonFormat(shape = JsonFormat.Shape.NUMBER) + Instant timestamp, + String description, + AuditEventSource actor ) { + public static final class Builder { + private String action; + private Instant timestamp = Instant.now(); + private String description; + private AuditEventSource actor = null; + + // also add parameters for AuditEventSource + + private long userId; + private String username; + private String ip; + private String country; + private String userAgent; + private Role role = Role.ANYONE; + private String source = "modmail-viewer"; + + + public Builder(String action) { + this.action = action; + } + + public Builder withAction(String action) { + this.action = action; + return this; + } + + public Builder withTimestamp(Instant timestamp) { + this.timestamp = timestamp; + return this; + } + + public Builder withDescription(String description) { + this.description = description; + return this; + } + + /** + * Override the actor of the event + * When set, the userId, username, ip, country, userAgent, and role parameters are ignored + * + * @param actor The actor of the event + * @return The builder + */ + public Builder withActor(AuditEventSource actor) { + this.actor = actor; + return this; + } + + public Builder fromCtx(Context ctx) { + this.ip = ctx.ip(); + this.userAgent = ctx.userAgent(); + + return this; + } + + public Builder withUserId(long userId) { + this.userId = userId; + return this; + } + + public Builder withUsername(String username) { + this.username = username; + return this; + } + + public Builder withIp(String ip) { + this.ip = ip; + return this; + } + + public Builder withCountry(String country) { + this.country = country; + return this; + } + + public Builder withUserAgent(String userAgent) { + this.userAgent = userAgent; + return this; + } + + public Builder withRole(Role role) { + this.role = role; + return this; + } + + public Builder withSource(String source) { + this.source = source; + return this; + } + + public AuditEvent build() { + return new AuditEvent(null, action, timestamp, description, Objects.isNull(actor) ? new AuditEventSource(userId, username, ip, country, userAgent, role, source) : actor); + } + } } From 8b71a2615283b2b19400549408eb490f7e955911 Mon Sep 17 00:00:00 2001 From: Khakers <22665282+khakers@users.noreply.github.com> Date: Wed, 16 Aug 2023 18:59:41 -0700 Subject: [PATCH 04/21] refactor: use AuditEvent builder in AuthHandler#handleCallback and migrate OutBoundAuditLogger to class parameter --- .../modmailviewer/auth/AuthHandler.java | 33 ++++++++----------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/src/main/java/com/github/khakers/modmailviewer/auth/AuthHandler.java b/src/main/java/com/github/khakers/modmailviewer/auth/AuthHandler.java index f0b80b82..88c991c7 100644 --- a/src/main/java/com/github/khakers/modmailviewer/auth/AuthHandler.java +++ b/src/main/java/com/github/khakers/modmailviewer/auth/AuthHandler.java @@ -3,12 +3,13 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import com.github.khakers.modmailviewer.Config; -import com.github.khakers.modmailviewer.Main; import com.github.khakers.modmailviewer.ModMailLogDB; import com.github.khakers.modmailviewer.ModmailViewer; +import com.github.khakers.modmailviewer.auditlog.OutboundAuditEventLogger; import com.github.khakers.modmailviewer.auditlog.event.AuditEvent; import com.github.khakers.modmailviewer.auditlog.event.AuditEventSource; import com.github.khakers.modmailviewer.auth.discord.GuildMember; +import com.github.khakers.modmailviewer.util.DiscordUtils; import com.github.scribejava.apis.DiscordApi; import com.github.scribejava.core.builder.ServiceBuilder; import com.github.scribejava.core.exceptions.OAuthException; @@ -23,7 +24,6 @@ import okhttp3.OkHttpClient; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.bson.types.ObjectId; import org.jetbrains.annotations.NotNull; import java.io.IOException; @@ -49,8 +49,11 @@ public class AuthHandler { private final Map ouathState = new Hashtable<>(); private final SecureRandom secureRandom = new SecureRandom(); + private final OutboundAuditEventLogger auditLogger; - public AuthHandler(String callback, String clientId, String clientSecret, String jwtSecret, ModMailLogDB modMailLogDB) { + + public AuthHandler(String callback, String clientId, String clientSecret, String jwtSecret, ModMailLogDB modMailLogDB, OutboundAuditEventLogger auditLogger) { + this.auditLogger = auditLogger; this.service = new ServiceBuilder(clientId) .apiSecret(clientSecret) .defaultScope("identify guilds.members.read") @@ -217,22 +220,14 @@ public void handleCallback(Context ctx) throws IOException, ExecutionException, return; } -// Main.auditLogger.pushAuditEventWithContext(ctx, ); - Main.auditLogger.pushEvent( - new AuditEvent( - new ObjectId(), - "viewer.login", - Instant.now(), - "User logged in through discord", - new AuditEventSource( - user.getId(), - user.getUsername(), - ctx.ip(), - null, - ctx.userAgent(), - role, - "modmail-viewer"))); - + this.auditLogger.pushEvent( + new AuditEvent.Builder("viewer.login") + .fromCtx(ctx) + .withDescription("User logged in through discord") + .withUserId(user.getId()) + .withUsername(user.getUsername() + DiscordUtils.getDiscriminatorString(user)) + .withRole(role) + .build()); ctx.result(userResponse.getBody()); handleGenerateJWT(ctx, user, guild.roles()); From 1a6f4f20d7ba2bdfebc8c8c016299fe285a0b544 Mon Sep 17 00:00:00 2001 From: Khakers <22665282+khakers@users.noreply.github.com> Date: Wed, 16 Aug 2023 19:02:22 -0700 Subject: [PATCH 05/21] chore: add gestalt dependency --- build.gradle | 2 ++ gradle/libs.versions.toml | 3 +++ 2 files changed, 5 insertions(+) diff --git a/build.gradle b/build.gradle index 1c17b414..12af290f 100644 --- a/build.gradle +++ b/build.gradle @@ -70,6 +70,8 @@ dependencies { implementation platform(libs.okhttp.bom) implementation libs.okhttp + implementation(libs.gestalt) + testImplementation libs.junit.jupiter.api testRuntimeOnly libs.junit.jupiter.engine testImplementation libs.junit.jupiter.params diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 77472f07..dcde1d49 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -22,6 +22,7 @@ okhttp = "4.10.0" mongodriver = "4.8.2" mongojack = "4.8.0" owasp-encoder = "1.2.3" +gestalt = "0.22.0" bootstrap = "5.3.0" bootstrap-icons = "1.10.3" @@ -67,6 +68,8 @@ okhttp = { module = "com.squareup.okhttp3:okhttp" } owasp-encoder = { module = "org.owasp.encoder:encoder", version.ref = "owasp-encoder" } +gestalt = { module = "com.github.gestalt-config:gestalt-core", version.ref = "gestalt" } + webjar-bootstrap = { module = "org.webjars.npm:bootstrap", version.ref = "bootstrap" } webjar-bootstrap-icons = { module = "org.webjars.npm:bootstrap-icons", version.ref = "bootstrap-icons" } webjar-highlightjs = { module = "org.webjars.npm:highlightjs__cdn-assets", version.ref = "highlightjs" } From df03cadc387b077c915942d12117b48a95ab69ce Mon Sep 17 00:00:00 2001 From: Khakers <22665282+khakers@users.noreply.github.com> Date: Thu, 17 Aug 2023 17:37:00 -0700 Subject: [PATCH 06/21] fix: add default id to AuditEvent builder --- .../khakers/modmailviewer/auditlog/event/AuditEvent.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/github/khakers/modmailviewer/auditlog/event/AuditEvent.java b/src/main/java/com/github/khakers/modmailviewer/auditlog/event/AuditEvent.java index 1145378f..909ec838 100644 --- a/src/main/java/com/github/khakers/modmailviewer/auditlog/event/AuditEvent.java +++ b/src/main/java/com/github/khakers/modmailviewer/auditlog/event/AuditEvent.java @@ -22,6 +22,7 @@ public record AuditEvent( AuditEventSource actor ) { public static final class Builder { + private org.bson.types.ObjectId id = new org.bson.types.ObjectId(); private String action; private Instant timestamp = Instant.now(); private String description; @@ -112,7 +113,7 @@ public Builder withSource(String source) { } public AuditEvent build() { - return new AuditEvent(null, action, timestamp, description, Objects.isNull(actor) ? new AuditEventSource(userId, username, ip, country, userAgent, role, source) : actor); + return new AuditEvent(id, action, timestamp, description, Objects.isNull(actor) ? new AuditEventSource(userId, username, ip, country, userAgent, role, source) : actor); } } } From d2899dec6f466ca50156a52d9aebd4dc349f00fe Mon Sep 17 00:00:00 2001 From: Khakers <22665282+khakers@users.noreply.github.com> Date: Thu, 17 Aug 2023 17:56:26 -0700 Subject: [PATCH 07/21] refactor: add AuditEvent.Builder#withUser & use builder in MongoAuditEventLogger#pushAuditEventWithContext --- .../auditlog/MongoAuditEventLogger.java | 27 ++++----------- .../auditlog/event/AuditEvent.java | 34 +++++++++++++++++++ .../modmailviewer/auth/AuthHandler.java | 1 - 3 files changed, 40 insertions(+), 22 deletions(-) diff --git a/src/main/java/com/github/khakers/modmailviewer/auditlog/MongoAuditEventLogger.java b/src/main/java/com/github/khakers/modmailviewer/auditlog/MongoAuditEventLogger.java index 7f26e439..db796761 100644 --- a/src/main/java/com/github/khakers/modmailviewer/auditlog/MongoAuditEventLogger.java +++ b/src/main/java/com/github/khakers/modmailviewer/auditlog/MongoAuditEventLogger.java @@ -4,9 +4,7 @@ import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.json.JsonMapper; import com.github.khakers.modmailviewer.auditlog.event.AuditEvent; -import com.github.khakers.modmailviewer.auditlog.event.AuditEventSource; import com.github.khakers.modmailviewer.auth.AuthHandler; -import com.github.khakers.modmailviewer.util.DiscordUtils; import com.mongodb.ConnectionString; import com.mongodb.client.MongoClient; import com.mongodb.client.MongoCollection; @@ -117,25 +115,12 @@ public void pushEvent(AuditEvent event) { @Override public void pushAuditEventWithContext(Context ctx, String event, String description) throws Exception { var user = AuthHandler.getUser(ctx); - ObjectId.get(); - var auditEvent = new AuditEvent( - new ObjectId(), - event, - Instant.now(), - description, - new AuditEventSource( - user.getId(), - user.getUsername() + (DiscordUtils.isMigratedUserName(user) ? "" : "#" + user.getDiscriminator()), - ctx.ip(), - null, - ctx.userAgent(), - AuthHandler.getUserRole(ctx), - "modmail-viewer" - ) - - ); - - this.pushEvent(auditEvent); + this.pushEvent(new AuditEvent.Builder(event) + .fromCtx(ctx) + .withDescription(description) + .withRole(AuthHandler.getUserRole(ctx)) + .withUser(user) + .build()); } } diff --git a/src/main/java/com/github/khakers/modmailviewer/auditlog/event/AuditEvent.java b/src/main/java/com/github/khakers/modmailviewer/auditlog/event/AuditEvent.java index 909ec838..01c16a2d 100644 --- a/src/main/java/com/github/khakers/modmailviewer/auditlog/event/AuditEvent.java +++ b/src/main/java/com/github/khakers/modmailviewer/auditlog/event/AuditEvent.java @@ -3,6 +3,9 @@ import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonProperty; import com.github.khakers.modmailviewer.auth.Role; +import com.github.khakers.modmailviewer.auth.UserToken; +import com.github.khakers.modmailviewer.data.User; +import com.github.khakers.modmailviewer.util.DiscordUtils; import io.javalin.http.Context; import org.bson.codecs.pojo.annotations.BsonProperty; import org.mongojack.ObjectId; @@ -70,6 +73,12 @@ public Builder withActor(AuditEventSource actor) { return this; } + /** + * Sets the ip and user agent from the given context + * + * @param ctx The context to set the ip and user agent from + * @return The builder object + */ public Builder fromCtx(Context ctx) { this.ip = ctx.ip(); this.userAgent = ctx.userAgent(); @@ -82,6 +91,31 @@ public Builder withUserId(long userId) { return this; } + /** + * Sets the user ID and username from the given user + * + * @param user The user to set the ID and username from + * @return The builder object + */ + public Builder withUser(UserToken user) { + this.userId = user.getId(); + this.username = DiscordUtils.getDiscriminatorString(user); + + return this; + } + + /** + * Sets the user ID and username from the given user + * + * @param user The user to set the ID and username from + * @return The builder object + */ + public Builder withUser(User user) { + this.userId = Long.parseLong(user.id()); + this.username = DiscordUtils.getDiscriminatorString(user); + return this; + } + public Builder withUsername(String username) { this.username = username; return this; diff --git a/src/main/java/com/github/khakers/modmailviewer/auth/AuthHandler.java b/src/main/java/com/github/khakers/modmailviewer/auth/AuthHandler.java index 88c991c7..0bb69e74 100644 --- a/src/main/java/com/github/khakers/modmailviewer/auth/AuthHandler.java +++ b/src/main/java/com/github/khakers/modmailviewer/auth/AuthHandler.java @@ -7,7 +7,6 @@ import com.github.khakers.modmailviewer.ModmailViewer; import com.github.khakers.modmailviewer.auditlog.OutboundAuditEventLogger; import com.github.khakers.modmailviewer.auditlog.event.AuditEvent; -import com.github.khakers.modmailviewer.auditlog.event.AuditEventSource; import com.github.khakers.modmailviewer.auth.discord.GuildMember; import com.github.khakers.modmailviewer.util.DiscordUtils; import com.github.scribejava.apis.DiscordApi; From c2091da06371677222b5057b0d1a61cdda05f49b Mon Sep 17 00:00:00 2001 From: Khakers <22665282+khakers@users.noreply.github.com> Date: Fri, 18 Aug 2023 17:50:54 -0700 Subject: [PATCH 08/21] refactor: Migrate configuration to Gestalt Configuration used in JTE templates has not yet been migrated. --- CHANGELOG.md | 22 ++ UPGRADING.md | 31 ++ gradle/libs.versions.toml | 2 +- .../github/khakers/modmailviewer/Config.java | 15 +- .../github/khakers/modmailviewer/Main.java | 327 +++++++++++------- .../khakers/modmailviewer/ModMailLogDB.java | 17 +- .../modmailviewer/auth/AuthHandler.java | 15 +- .../khakers/modmailviewer/auth/UserToken.java | 6 +- .../configuration/AppConfig.java | 31 ++ .../configuration/AuditLogConfig.java | 17 + .../configuration/AuthConfig.java | 15 + .../configuration/CSPConfig.java | 12 + .../configuration/DiscordClient.java | 10 + .../configuration/SSLConfig.java | 18 + .../khakers/modmailviewer/log/LogPage.java | 2 +- .../khakers/modmailviewer/log/LogsPage.java | 9 +- .../page/dashboard/MetricsAccessor.java | 2 +- src/main/jte/pages/DashboardView.jte | 6 +- src/main/resources/default.properties | 25 ++ 19 files changed, 428 insertions(+), 154 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 UPGRADING.md create mode 100644 src/main/java/com/github/khakers/modmailviewer/configuration/AppConfig.java create mode 100644 src/main/java/com/github/khakers/modmailviewer/configuration/AuditLogConfig.java create mode 100644 src/main/java/com/github/khakers/modmailviewer/configuration/AuthConfig.java create mode 100644 src/main/java/com/github/khakers/modmailviewer/configuration/CSPConfig.java create mode 100644 src/main/java/com/github/khakers/modmailviewer/configuration/DiscordClient.java create mode 100644 src/main/java/com/github/khakers/modmailviewer/configuration/SSLConfig.java create mode 100644 src/main/resources/default.properties diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..944aa4e2 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,22 @@ +# Changelog +Based on [common changelog spec](https://common-changelog.org/) + +## [Unreleased] + +_If you are upgrading: please see [`UPGRADING.md`](UPGRADING.md)._ + + +### Changed + +- **Breaking:** Migrate configuration system to Gestalt. +- **Breaking:** Change default http port to 7080. +- **Breaking:** Change default https port to 7443. + +### Added + +### Removed + +- Remove support for automatic secret key generation. You must always provide a valid key when using authentication. +- Deprecate support for `MODMAIL_VIEWER_ANALYTICS_BASE64` configuration key. Use `MODMAIL_VIEWER_ANALYTICS` with ${base64Decode:}. + +### Fixed diff --git a/UPGRADING.md b/UPGRADING.md new file mode 100644 index 00000000..c9a21419 --- /dev/null +++ b/UPGRADING.md @@ -0,0 +1,31 @@ +## [UNRELEASED] - 2023-8-18 + +All configuration keys have been changed with the migration of the configuration system. +See below for a list of keys and their new values. + +| Previous Key | New Key | +|--------------------------------------------|---------------------------------------------| +| MODMAIL_VIEWER_URL | MODMAIL_VIEWER_APP_URL | +| MODMAIL_VIEWER_MONGODB_URI | MODMAIL_VIEWER_APP_MONGODB_URI | +| MODMAIL_VIEWER_DISCORD_OAUTH_CLIENT_ID | MODMAIL_VIEWER_APP_DISCORD_CLIENT_ID | +| MODMAIL_VIEWER_DISCORD_OAUTH_CLIENT_SECRET | MODMAIL_VIEWER_APP_DISCORD_CLIENT_SECRET | +| MODMAIL_VIEWER_DISCORD_GUILD_ID | MODMAIL_VIEWER_APP_DISCORD_GUILD_ID | +| MODMAIL_VIEWER_SECRETKEY | MODMAIL_VIEWER_APP_AUTH_SECRETKEY | +| MODMAIL_VIEWER_DEV | MODMAIL_VIEWER_APP_DEV | +| MODMAIL_VIEWER_AUTH_ENABLED | MODMAIL_VIEWER_APP_AUTH_ENABLED | +| MODMAIL_VIEWER_SSL | MODMAIL_VIEWER_APP_SSL_ENABLED | +| MODMAIL_VIEWER_HTTPS_ONLY | MODMAIL_VIEWER_APP_SSL_HTTPS_ONLY | +| MODMAIL_VIEWER_SSL_CERT | MODMAIL_VIEWER_APP_SSL_CERT | +| MODMAIL_VIEWER_SSL_KEY | MODMAIL_VIEWER_APP_SSL_KEY | +| MODMAIL_VIEWER_HTTP_PORT | MODMAIL_VIEWER_APP_PORT_HTTP | +| MODMAIL_VIEWER_HTTPS_PORT | MODMAIL_VIEWER_APP_PORT_HTTPS | +| MODMAIL_VIEWER_SNI | MODMAIL_VIEWER_APP_SSL_SNI | +| MODMAIL_VIEWER_STS | MODMAIL_VIEWER_APP_SSL_STS | +| MODMAIL_VIEWER_INSECURE | MODMAIL_VIEWER_APP_SECURE_COOKIES | +| MODMAIL_VIEWER_BRANDING | **AWAITING MIGRATION** | +| MODMAIL_VIEWER_LOG_LEVEL | **NO CHANGE** | +| MODMAIL_VIEWER_ANALYTICS | **AWAITING MIGRATION** | +| MODMAIL_VIEWER_ANALYTICS_BASE64 | **DEPRECATED** | +| MODMAIL_VIEWER_BOT_ID | MODMAIL_VIEWER_APP_BOT_ID | +| MODMAIL_VIEWER_CSP | MODMAIL_VIEWER_APP_CSP_OVERRIDE | +| MODMAIL_VIEWER_CSP_SCRIPT_SRC_ELEM_EXTRA | MODMAIL_VIEWER_APP_CSP_EXTRA_SCRIPT_SOURCES | diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index dcde1d49..95cce22a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -22,7 +22,7 @@ okhttp = "4.10.0" mongodriver = "4.8.2" mongojack = "4.8.0" owasp-encoder = "1.2.3" -gestalt = "0.22.0" +gestalt = "0.23.0" bootstrap = "5.3.0" bootstrap-icons = "1.10.3" diff --git a/src/main/java/com/github/khakers/modmailviewer/Config.java b/src/main/java/com/github/khakers/modmailviewer/Config.java index e7b7317a..f86666d0 100644 --- a/src/main/java/com/github/khakers/modmailviewer/Config.java +++ b/src/main/java/com/github/khakers/modmailviewer/Config.java @@ -10,6 +10,7 @@ import java.util.Base64; import java.util.Objects; +@Deprecated public class Config { private static final Logger logger = LogManager.getLogger(); @@ -23,7 +24,7 @@ public class Config { ? Integer.parseInt(System.getenv(ENV_PREPEND + "_HTTPs_PORT")) : 443; - public static final String MONGODB_URI = Assert.requireNonEmpty(System.getenv(ENV_PREPEND + "_MONGODB_URI"), "No mongodb URI provided. provide one with the option \""+ENV_PREPEND+"_MONGODB_URI\""); + public static final String MONGODB_URI = System.getenv(ENV_PREPEND + "_APP_MONGODB_URI"); public static final String WEB_URL; @@ -31,11 +32,11 @@ public class Config { @Nullable public static final String SSL_CERT = isSecure - ? Assert.requireNonEmpty(System.getenv(ENV_PREPEND + "_SSL_CERT"), "SSL was enabled but no certificate file path was provided. Provide one with the option \"" + ENV_PREPEND + "_SSL_CERT\"") + ?System.getenv(ENV_PREPEND + "_SSL_CERT") : null; @Nullable public static final String SSL_KEY = isSecure - ? Assert.requireNonEmpty(System.getenv(ENV_PREPEND + "_SSL_KEY"), "SSL was enabled but no key file path was provided. Provide one with the option \"" + ENV_PREPEND + "_DISCORD_SSL_KEY\"") + ? System.getenv(ENV_PREPEND + "_SSL_KEY") : null; public static final boolean isHttpsOnly = isSecure && isNotNullAndFalse(System.getenv(ENV_PREPEND + "_HTTPS_ONLY"), true); @@ -52,13 +53,13 @@ public class Config { public static final boolean isApiAuditingEnabled = true; public static final String DISCORD_CLIENT_ID = isAuthEnabled - ? Assert.requireNonEmpty(System.getenv(ENV_PREPEND + "_DISCORD_OAUTH_CLIENT_ID"), "No Discord client ID provided. Provide one with the option \"" + ENV_PREPEND + "_DISCORD_OAUTH_CLIENT_ID\"") + ? System.getenv(ENV_PREPEND + "_DISCORD_OAUTH_CLIENT_ID") : null; public static final String DISCORD_CLIENT_SECRET = isAuthEnabled - ? Assert.requireNonEmpty(System.getenv(ENV_PREPEND + "_DISCORD_OAUTH_CLIENT_SECRET"), "No Discord client ID provided. Provide one with the option \"" + ENV_PREPEND + "_DISCORD_OAUTH_CLIENT_SECRET\"") + ? System.getenv(ENV_PREPEND + "_DISCORD_OAUTH_CLIENT_SECRET") : null; - public static final long DISCORD_GUILD_ID = Long.parseLong(Assert.requireNonEmpty(System.getenv(ENV_PREPEND + "_DISCORD_GUILD_ID"), "No Discord guild ID provided. Provide one with the option \""+ENV_PREPEND+"_DISCORD_GUILD_ID\"")); + public static final long DISCORD_GUILD_ID = Long.parseLong(System.getenv(ENV_PREPEND + "_DISCORD_GUILD_ID")); public static final long BOT_ID = Long.parseLong(notEmptyOrElse(System.getenv(ENV_PREPEND + "_BOT_ID"), "0")); public static final String JWT_SECRET_KEY; @@ -90,7 +91,7 @@ public class Config { logger.warn("Insecure cookies are enabled. This reduces security and should only be enabled when https is unavailable"); } - var webUrl = Assert.requireNonEmpty(System.getenv(ENV_PREPEND + "_URL"), "No URL provided. provide one with the option \"" + ENV_PREPEND + "_URL\""); + var webUrl = System.getenv(ENV_PREPEND + "_URL"); if (webUrl.endsWith("/")) { logger.warn(ENV_PREPEND + "_WEB_URL has a trailing slash. Removed it due to conflict with the callback."); webUrl = webUrl.substring(0, webUrl.length() - 1); diff --git a/src/main/java/com/github/khakers/modmailviewer/Main.java b/src/main/java/com/github/khakers/modmailviewer/Main.java index 500b6277..6123c219 100644 --- a/src/main/java/com/github/khakers/modmailviewer/Main.java +++ b/src/main/java/com/github/khakers/modmailviewer/Main.java @@ -1,14 +1,15 @@ package com.github.khakers.modmailviewer; import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; -import com.github.khakers.modmailviewer.page.admin.AdminController; -import com.github.khakers.modmailviewer.auditlog.OutboundAuditEventLogger; +import com.github.khakers.modmailviewer.auditlog.AuditEventDAO; import com.github.khakers.modmailviewer.auditlog.MongoAuditEventLogger; import com.github.khakers.modmailviewer.auditlog.NoopAuditEventLogger; +import com.github.khakers.modmailviewer.auditlog.OutboundAuditEventLogger; import com.github.khakers.modmailviewer.auth.AuthHandler; import com.github.khakers.modmailviewer.auth.Role; -import com.github.khakers.modmailviewer.page.dashboard.DashboardController; -import com.github.khakers.modmailviewer.page.dashboard.MetricsAccessor; +import com.github.khakers.modmailviewer.configuration.AppConfig; +import com.github.khakers.modmailviewer.configuration.CSPConfig; +import com.github.khakers.modmailviewer.configuration.SSLConfig; import com.github.khakers.modmailviewer.log.LogController; import com.github.khakers.modmailviewer.markdown.channelmention.ChannelMentionExtension; import com.github.khakers.modmailviewer.markdown.customemoji.CustomEmojiExtension; @@ -16,13 +17,15 @@ import com.github.khakers.modmailviewer.markdown.timestamp.TimestampExtension; import com.github.khakers.modmailviewer.markdown.underline.UnderlineExtension; import com.github.khakers.modmailviewer.markdown.usermention.UserMentionExtension; +import com.github.khakers.modmailviewer.page.admin.AdminController; import com.github.khakers.modmailviewer.page.audit.AuditController; +import com.github.khakers.modmailviewer.page.dashboard.DashboardController; +import com.github.khakers.modmailviewer.page.dashboard.MetricsAccessor; import com.github.khakers.modmailviewer.util.RoleUtils; import com.mongodb.ConnectionString; import com.mongodb.MongoClientSettings; import com.mongodb.client.MongoClient; import com.mongodb.client.MongoClients; -import com.mongodb.client.MongoDatabase; import com.vladsch.flexmark.ext.autolink.AutolinkExtension; import com.vladsch.flexmark.ext.gfm.strikethrough.StrikethroughExtension; import com.vladsch.flexmark.html.HtmlRenderer; @@ -46,7 +49,18 @@ import org.apache.logging.log4j.Logger; import org.bson.codecs.configuration.CodecRegistries; import org.bson.codecs.pojo.PojoCodecProvider; +import org.github.gestalt.config.Gestalt; +import org.github.gestalt.config.builder.GestaltBuilder; +import org.github.gestalt.config.exceptions.GestaltException; +import org.github.gestalt.config.path.mapper.SnakeCasePathMapper; +import org.github.gestalt.config.source.ClassPathConfigSource; +import org.github.gestalt.config.source.EnvironmentConfigSource; +import org.jetbrains.annotations.NotNull; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; import java.nio.file.Path; import java.time.Duration; import java.time.LocalDate; @@ -76,77 +90,127 @@ public class Main { .set(HtmlRenderer.SOFT_BREAK, "
\n") .toImmutable(); public static final Parser PARSER = Parser.builder(OPTIONS) - .build(); + .build(); public static final HtmlRenderer RENDERER = HtmlRenderer.builder(OPTIONS) - .escapeHtml(true) - .build(); + .escapeHtml(true) + .build(); + public static final UpdateChecker updateChecker = new UpdateChecker(); private static final Logger logger = LogManager.getLogger(); private static final String envPrepend = "MODMAIL_VIEWER"; + public static ModMailLogDB modMailLogDB; + public static MetricsAccessor metricsAccessor; + static AuthHandler authHandler; + private static OutboundAuditEventLogger auditLogger; - private static final ConnectionString connectionString = new ConnectionString(Config.MONGODB_URI); - - private static final MongoClient mongoClient = MongoClients.create(MongoClientSettings.builder() - .applyConnectionString(connectionString) - .codecRegistry( - CodecRegistries.fromRegistries( - MongoClientSettings - .getDefaultCodecRegistry(), - CodecRegistries - .fromProviders(PojoCodecProvider.builder() - .automatic(true) - .build()))) - .build()); - - private static final MongoDatabase MODMAIL_DATABASE = mongoClient.getDatabase(connectionString.getDatabase() == null ? "modmail_bot" : connectionString.getDatabase()); - public static final ModMailLogDB MOD_MAIL_LOG_CLIENT = new ModMailLogDB(MODMAIL_DATABASE); - - // We will always need an audit logger for searching, even if pushing to an audit logger is disabled - public static AuditEventDAO AuditLogClient = new MongoAuditEventLogger(mongoClient, Config.MONGODB_URI, "modmail_bot", "audit_log"); - - public static final OutboundAuditEventLogger auditLogger = Config.isAuthEnabled - ? (OutboundAuditEventLogger) AuditLogClient - : new NoopAuditEventLogger(); - - static final AuthHandler authHandler = - Config.isAuthEnabled ? - new AuthHandler(Config.WEB_URL + "/callback", - Config.DISCORD_CLIENT_ID, - Config.DISCORD_CLIENT_SECRET, - Config.JWT_SECRET_KEY, - MOD_MAIL_LOG_CLIENT) - : null; + public static void main(String[] args) throws GestaltException { - public static final UpdateChecker updateChecker = new UpdateChecker(); + Gestalt gestalt = new GestaltBuilder() + .setTreatNullValuesInClassAsErrors(true) + .setTreatMissingValuesAsErrors(false) + .addSource(new ClassPathConfigSource("/default.properties")) + .addSource(new EnvironmentConfigSource(envPrepend)) + .addDefaultPathMappers() + .addPathMapper(new SnakeCasePathMapper()) +// .addSource(new ClassPathConfigSource("/default.properties")) // Load the default property files from resources. +// .addSource(new FileConfigSource(devFile)) +// .addSource(new MapConfigSource(configs)) + .build(); + gestalt.loadConfigs(); - public static MetricsAccessor metricsAccessor = new MetricsAccessor(MODMAIL_DATABASE); + var appConfig = gestalt.getConfig("app", AppConfig.class); + logger.debug(appConfig.toString()); + var authConfig = appConfig.auth(); + var auditLogConfig = appConfig.auditLogConfig(); +// var cspConfig = appConfig.cspConfig(); - public static void main(String[] args) { TemplateEngine templateEngine; - if (Config.isDevMode) { + if (appConfig.dev()) { templateEngine = TemplateEngine.create(new DirectoryCodeResolver(Path.of("src/main/jte")), ContentType.Html); } else { templateEngine = TemplateEngine.createPrecompiled(ContentType.Html); } + JavalinJte.init(templateEngine); registerValidators(); - var adminController = new AdminController(AuditLogClient); - var auditController = new AuditController(AuditLogClient); + URI uri = null; - JavalinJte.init(templateEngine); - var app = Javalin.create(Main::configure) - .get("/hello", ctx -> ctx.status(200).result("hello"), RoleUtils.anyone()) - .post("/logout", ctx -> { - auditLogger.pushAuditEventWithContext(ctx, "viewer.logout", "User logged out"); - ctx.removeCookie("jwt"); - ctx.result("logout successful"); - if (!Config.isAuthEnabled) { - ctx.redirect("/"); - } - }, RoleUtils.atLeastSupporter()) - .get("/", LogController.serveLogsPage, RoleUtils.atLeastSupporter()) + try { + uri = new URL(appConfig.url()).toURI(); // Validate the URL + if (uri.getScheme().equals("http")) { + logger.warn("You are running Modmail-Viewer over HTTP with an http callback URI. It is highly recommended that you use HTTPS."); + } else if (uri.getScheme().equals("https") && !appConfig.secureCookies()) { + logger.warn("You are running Modmail-Viewer over HTTPS but have disabled enabled secure cookies. There should be no reason to do this. "); + } + } catch (URISyntaxException e) { + logger.fatal("Invalid URL: " + appConfig.url()); + System.exit(1); + } catch (MalformedURLException e) { + throw new RuntimeException(e); + } + var callbackUri = uri.resolve("/callback"); // Append the callback path to the URL + logger.debug("Callback URL: " + callbackUri); + + var connectionString = new ConnectionString(appConfig.mongodbUri()); + + MongoClient mongoClient = MongoClients.create(MongoClientSettings.builder() + .applyConnectionString(connectionString) + .applicationName("Modmail-Viewer") + .codecRegistry( + CodecRegistries.fromRegistries( + MongoClientSettings + .getDefaultCodecRegistry(), + CodecRegistries + .fromProviders(PojoCodecProvider.builder() + .automatic(true) + .build()))) + .build()); + + var mongoClientDatabase = mongoClient.getDatabase(Objects.requireNonNullElse(connectionString.getDatabase(), "modmail_bot")); + + modMailLogDB = new ModMailLogDB(mongoClientDatabase, appConfig.botId()); + + // We will always need an audit logger for searching, even if pushing to an audit logger is disabled + AuditEventDAO auditLogClient = new MongoAuditEventLogger(mongoClient, appConfig.mongodbUri(), "modmail_bot", "audit_log"); + auditLogger = authConfig.isPresent() && authConfig.get().enabled() + ? (OutboundAuditEventLogger) auditLogClient + : new NoopAuditEventLogger(); + + + + authHandler = authConfig.isPresent() && authConfig.get().enabled() ? + new AuthHandler(callbackUri.toString(), + authConfig.get().discordClient().clientId(), + authConfig.get().discordClient().clientSecret(), + authConfig.get().secretKey(), + modMailLogDB,auditLogger, authConfig.get().discordClient().guildId(), appConfig.secureCookies()) + : null; + + metricsAccessor = new MetricsAccessor(mongoClientDatabase); + + var adminController = new AdminController(auditLogClient); + var auditController = new AuditController(auditLogClient); + + + var app = Javalin.create(javalinConfig -> { + try { + Main.configure(javalinConfig, appConfig); + } catch (GestaltException e) { + throw new RuntimeException(e); + } + }) + .get("/hello", ctx -> ctx.status(200).result("hello"), RoleUtils.anyone()) + .post("/logout", ctx -> { + auditLogger.pushAuditEventWithContext(ctx, "viewer.logout", "User logged out"); + ctx.removeCookie("jwt"); + ctx.result("logout successful"); + if (authConfig.isEmpty() || !authConfig.get().enabled()) { + ctx.redirect("/"); + } + }, RoleUtils.atLeastSupporter()) + .get("/", LogController.serveLogsPage, RoleUtils.atLeastSupporter()) // .routes(() -> { // path("logs", () -> { // get(LogController.serveLogsPage, RoleUtils.atLeastSupporter()); @@ -155,62 +219,64 @@ public static void main(String[] args) { // ); // }); // }) - .get("/logs", LogController.serveLogsPage, RoleUtils.atLeastSupporter()) - .get("/logs/{id}", LogController.serveLogPage, RoleUtils.atLeastSupporter()) - .get("/dashboard", DashboardController.serveDashboardPage, RoleUtils.atLeastSupporter()) - //todo maybe after? - .after("/logs/{id}", ctx -> { - if (ctx.statusCode() == HttpStatus.FORBIDDEN.getCode()) { - - } - - if (Config.isDetailedAuditingEnabled) { - auditLogger.pushAuditEventWithContext(ctx, "log.accessed", String.format("accessed log id %s", ctx.pathParam("id"))); - } - }) - .get("/admin", adminController.serveAdminPage, RoleUtils.atLeastAdministrator()) - .get("/audit/{id}", auditController.serveAuditPage, RoleUtils.atLeastAdministrator()) - .after("/api/*", ctx -> { - if (Config.isApiAuditingEnabled) { - if (ctx.statusCode() == HttpStatus.FORBIDDEN.getCode()) { - auditLogger.pushAuditEventWithContext(ctx, "viewer.api", String.format("DENIED %s %s", ctx.method(), ctx.path())); - } else { - auditLogger.pushAuditEventWithContext(ctx, "viewer.api", String.format("%s %s", ctx.method(), ctx.path())); - } - } - }) - .start(Config.httpPort); - - if (Config.isAuthEnabled) { - // Register api only if authentication is enabled - app.get("/api/logs/{id}", ctx -> { - var entry = MOD_MAIL_LOG_CLIENT.getModMailLogEntry(ctx.pathParam("id")); - entry.ifPresentOrElse( - ctx::json, - () -> { - throw new NotFoundResponse(); - }); - - }, RoleUtils.atLeastAdministrator()) - .get("/api/config", ctx -> ctx.json(MOD_MAIL_LOG_CLIENT.getConfig()), RoleUtils.atLeastAdministrator()); - - app.get("/callback", authHandler::handleCallback, Role.ANYONE); - } + .get("/logs", LogController.serveLogsPage, RoleUtils.atLeastSupporter()) + .get("/logs/{id}", LogController.serveLogPage, RoleUtils.atLeastSupporter()) + .get("/dashboard", DashboardController.serveDashboardPage, RoleUtils.atLeastSupporter()) + //todo maybe after? + .after("/logs/{id}", ctx -> { +// if (ctx.statusCode() == HttpStatus.FORBIDDEN.getCode()) { +// +// } + if (auditLogConfig.isDetailedAuditingEnabled()) { + auditLogger.pushAuditEventWithContext(ctx, "viewer.log.accessed", String.format("accessed log id %s", ctx.pathParam("id"))); + } + }) + .get("/admin", adminController.serveAdminPage, RoleUtils.atLeastAdministrator()) + .get("/audit/{id}", auditController.serveAuditPage, RoleUtils.atLeastAdministrator()) + .after("/api/*", ctx -> { + if (auditLogConfig.isApiAuditingEnabled()) { + if (ctx.statusCode() == HttpStatus.FORBIDDEN.getCode()) { + auditLogger.pushAuditEventWithContext(ctx, "viewer.api", String.format("DENIED %s %s", ctx.method(), ctx.path())); + } else { + auditLogger.pushAuditEventWithContext(ctx, "viewer.api", String.format("%s %s", ctx.method(), ctx.path())); + } + } + }) + .start(appConfig.httpPort()); - logger.info("You are running Modmail-Viewer {} built on {}", ModmailViewer.VERSION, ModmailViewer.BUILD_TIMESTAMP); + if (authConfig.isPresent() && authConfig.get().enabled()) { + // Register api only if authentication is enabled + app.get("/api/logs/{id}", ctx -> { + var entry = modMailLogDB.getModMailLogEntry(ctx.pathParam("id")); + entry.ifPresentOrElse( + ctx::json, + () -> { + throw new NotFoundResponse(); + }); + + }, RoleUtils.atLeastAdministrator()) + .get("/api/config", ctx -> ctx.json(modMailLogDB.getConfig()), RoleUtils.atLeastAdministrator()); + app.get("/callback", authHandler::handleCallback, Role.ANYONE); + } + + + logger.info("You are running Modmail-Viewer {} built on {}", ModmailViewer.VERSION, ModmailViewer.BUILD_TIMESTAMP); } - private static void configure(JavalinConfig config) { + private static void configure(JavalinConfig config, AppConfig appConfig) throws GestaltException { + var sslOptions = appConfig.sslOptions(); + var cspConfig = appConfig.cspConfig(); + config.showJavalinBanner = false; config.jsonMapper(new JavalinJackson().updateMapper(objectMapper -> objectMapper.registerModule(new Jdk8Module()))); - config.plugins.enableGlobalHeaders(Main::configureHeaders); - if (Config.isHttpsOnly) { + config.plugins.enableGlobalHeaders(() -> configureHeaders(sslOptions.get(), cspConfig)); + if (sslOptions.isPresent() && sslOptions.get().httpsOnly()) { logger.info("HTTPS only is ENABLED"); config.plugins.enableSslRedirects(); } - if (Config.isDevMode) { + if (appConfig.dev()) { logger.info("Loading static files from {}", System.getProperty("user.dir") + "/src/main/resources/static"); config.staticFiles.add(staticFileConfig -> { staticFileConfig.mimeTypes.add(io.javalin.http.ContentType.TEXT_JS, "js"); @@ -227,47 +293,64 @@ private static void configure(JavalinConfig config) { }); } config.staticFiles.enableWebjars(); - if (Config.isDevMode) { + if (appConfig.dev()) { logger.info("Dev mode is ENABLED"); config.showJavalinBanner = true; config.plugins.enableDevLogging(); } - if (Config.isAuthEnabled) { + if (appConfig.auth().isPresent() && appConfig.auth().get().enabled()) { + logger.debug("Authentication is ENABLED"); config.accessManager(authHandler::HandleAuth); } else { logger.warn("Authentication is DISABLED"); config.accessManager((handler, context, set) -> handler.handle(context)); } - if (Config.isSecure) { + if (sslOptions.isPresent() && sslOptions.get().enabled()) { logger.info("SSL is ENABLED"); - SSLPlugin sslPlugin = new SSLPlugin(sslConfig -> { - sslConfig.pemFromPath(Config.SSL_CERT, Config.SSL_KEY); - sslConfig.insecurePort = Config.httpPort; - sslConfig.securePort = Config.httpsPort; - sslConfig.sniHostCheck = Config.isSNIEnabled; - if (!Config.isHttpsOnly) { - logger.warn("SSL is ENABLED but HTTPS only is DISABLED"); - } - }); + logger.debug(sslOptions.get().toString()); + SSLPlugin sslPlugin = getSslPlugin(appConfig, sslOptions.get()); config.plugins.register(sslPlugin); + } else { + if (sslOptions.isPresent()) + logger.warn("SSL options are present but SSL was disabled"); + logger.debug("SSL is DISABLED"); } } - private static GlobalHeaderConfig configureHeaders() { + @NotNull + private static SSLPlugin getSslPlugin(AppConfig appConfig, SSLConfig sslOptions) { + return new SSLPlugin(sslConfig -> { + sslConfig.pemFromPath(sslOptions.cert().get(), sslOptions.key().get()); + sslConfig.insecurePort = appConfig.httpPort(); + sslConfig.securePort = appConfig.httpsPort(); + sslConfig.sniHostCheck = sslOptions.isSNIEnabled(); + if (!sslOptions.httpsOnly()) { + logger.warn("SSL is ENABLED but HTTPS only is DISABLED"); + } + }); + } + + private static GlobalHeaderConfig configureHeaders(SSLConfig sslOptions, CSPConfig cspConfig) { var globalHeaderConfig = new GlobalHeaderConfig(); globalHeaderConfig.xFrameOptions(GlobalHeaderConfig.XFrameOptions.DENY); globalHeaderConfig.xContentTypeOptionsNoSniff(); globalHeaderConfig.xPermittedCrossDomainPolicies(GlobalHeaderConfig.CrossDomainPolicy.NONE); globalHeaderConfig.crossOriginOpenerPolicy(GlobalHeaderConfig.CrossOriginOpenerPolicy.SAME_ORIGIN); globalHeaderConfig.crossOriginResourcePolicy(GlobalHeaderConfig.CrossOriginResourcePolicy.SAME_ORIGIN); - if (Config.isSTSEnabled) { + if (sslOptions.isSTSEnabled()) { globalHeaderConfig.strictTransportSecurity(Duration.ofDays(356), false); } - if (Config.CUSTOM_CSP != null && !Config.CUSTOM_CSP.isBlank()) { - globalHeaderConfig.contentSecurityPolicy(Config.CUSTOM_CSP); + if (cspConfig.override().isPresent() && !cspConfig.override().get().isBlank()) { + globalHeaderConfig.contentSecurityPolicy(cspConfig.override().get()); } else { - globalHeaderConfig.contentSecurityPolicy(String.format("default-src 'self'; img-src * 'self' data:; object-src 'none'; media-src media.discordapp.com; style-src-attr 'unsafe-hashes' 'self' 'sha256-biLFinpqYMtWHmXfkA1BPeCY0/fNt46SAZ+BBk5YUog='; script-src-elem 'self' https://cdn.jsdelivr.net/npm/@twemoji/api@14.1.0/dist/twemoji.min.js %s;", Objects.requireNonNullElse(Config.CSP_SCRIPT_SRC_ELEM_EXTRA, ""))); - + globalHeaderConfig.contentSecurityPolicy(String.format( + "default-src 'self'; " + + "img-src * 'self' data:; " + + "object-src 'none'; " + + "media-src media.discordapp.com; " + + "style-src-attr 'unsafe-hashes' 'self' 'sha256-biLFinpqYMtWHmXfkA1BPeCY0/fNt46SAZ+BBk5YUog='; " + + "script-src-elem 'self' https://cdn.jsdelivr.net/npm/@twemoji/api@14.1.0/dist/twemoji.min.js %s;", + cspConfig.extraScriptSources().orElse(""))); } return globalHeaderConfig; diff --git a/src/main/java/com/github/khakers/modmailviewer/ModMailLogDB.java b/src/main/java/com/github/khakers/modmailviewer/ModMailLogDB.java index e36770bb..5ef02738 100644 --- a/src/main/java/com/github/khakers/modmailviewer/ModMailLogDB.java +++ b/src/main/java/com/github/khakers/modmailviewer/ModMailLogDB.java @@ -44,7 +44,10 @@ public class ModMailLogDB { private final ObjectMapper objectMapper; private final SingleItemCache configCache = new SingleItemCache<>(300000L, this::fetchConfig); - public ModMailLogDB(MongoDatabase modmailDatabase) { + private long modmailBotId; + + public ModMailLogDB(MongoDatabase modmailDatabase, long modmailBotId) { + this.modmailBotId = modmailBotId; this.objectMapper = JsonMapper.builder() .addModule(new JavaTimeModule()) @@ -55,13 +58,12 @@ public ModMailLogDB(MongoDatabase modmailDatabase) { .withConfigOverride(Instant.class, cfg -> cfg.setFormat(JsonFormat.Value.forPattern(DateFormatters.PYTHON_STR_ISO_OFFSET_DATE_TIME_STRING))) .build(); - this.database = modmailDatabase; this.logCollection = JacksonMongoCollection.builder().withObjectMapper(objectMapper).build(database, "logs", ModMailLogEntry.class, UuidRepresentation.STANDARD); this.logAggregateCollection = database.getCollection("logs"); this.configCollection = database.getCollection("config"); - if (configCollection.countDocuments() > 1 && Config.BOT_ID == 0) { + if (configCollection.countDocuments() > 1 && modmailBotId == 0) { logger.warn("Multiple configuration documents were found in your MongoDB database. " + "You *MUST* set the BOT_ID variable to your bots ID in order for the correct modmail configuration to be used."); } @@ -413,18 +415,17 @@ public JacksonMongoCollection getLogCollection() { * @return the config from the database */ private ModmailConfig fetchConfig() throws Exception { - Document conf = null; - if (Config.BOT_ID == 0) { + Document conf; + if (modmailBotId == 0) { conf = configCollection.find().first(); } else { - conf = configCollection.find(Filters.eq("bot_id", Config.BOT_ID)).first(); + conf = configCollection.find(Filters.eq("bot_id", modmailBotId)).first(); } if (conf != null) { try { logger.trace(conf.toJson()); - var config = objectMapper.readValue(conf.toJson(), ModmailConfig.class); - return config; + return objectMapper.readValue(conf.toJson(), ModmailConfig.class); } catch (JsonProcessingException e) { logger.error(e); throw new RuntimeException(e); diff --git a/src/main/java/com/github/khakers/modmailviewer/auth/AuthHandler.java b/src/main/java/com/github/khakers/modmailviewer/auth/AuthHandler.java index 0bb69e74..2125e555 100644 --- a/src/main/java/com/github/khakers/modmailviewer/auth/AuthHandler.java +++ b/src/main/java/com/github/khakers/modmailviewer/auth/AuthHandler.java @@ -2,7 +2,6 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; -import com.github.khakers.modmailviewer.Config; import com.github.khakers.modmailviewer.ModMailLogDB; import com.github.khakers.modmailviewer.ModmailViewer; import com.github.khakers.modmailviewer.auditlog.OutboundAuditEventLogger; @@ -50,9 +49,14 @@ public class AuthHandler { private final OutboundAuditEventLogger auditLogger; + private final long discordMainGuildId; - public AuthHandler(String callback, String clientId, String clientSecret, String jwtSecret, ModMailLogDB modMailLogDB, OutboundAuditEventLogger auditLogger) { + private final boolean secureCookies; + + + public AuthHandler(String callback, String clientId, String clientSecret, String jwtSecret, ModMailLogDB modMailLogDB, OutboundAuditEventLogger auditLogger, long discordMainGuildId, boolean secureCookies) { this.auditLogger = auditLogger; + this.secureCookies = secureCookies; this.service = new ServiceBuilder(clientId) .apiSecret(clientSecret) .defaultScope("identify guilds.members.read") @@ -62,6 +66,7 @@ public AuthHandler(String callback, String clientId, String clientSecret, String .build(DiscordApi.instance()); this.jwtAuth = new JwtAuth(jwtSecret); AuthHandler.modMailLogDB = modMailLogDB; + this.discordMainGuildId = discordMainGuildId; } public static Role getUserRole(UserToken token) { @@ -149,7 +154,7 @@ private String generateOAuthState(Context ctx) { var key = new BigInteger(130, secureRandom).toString(32); var state = new ClientState(ctx.fullUrl()); ouathState.put(key, state); - ctx.cookie(new Cookie("state", key, "/", -1, Config.isCookiesSecure, 1, true, "", "", SameSite.LAX)); + ctx.cookie(new Cookie("state", key, "/", -1, this.secureCookies, 1, true, "", "", SameSite.LAX)); return key; } @@ -176,7 +181,7 @@ public void handleGenerateJWT(Context ctx, UserToken user, long[] roles) throws // Same site strict cause browser not to send the cookie upon redirect from oauth // Which would mean we would need load a page that redirects the user with js var jwt = jwtAuth.generateJWT(user, roles); - ctx.cookie(new Cookie("jwt", jwt, "/", 10800, Config.isCookiesSecure, 1, true, "", "", SameSite.LAX)); + ctx.cookie(new Cookie("jwt", jwt, "/", 10800, this.secureCookies, 1, true, "", "", SameSite.LAX)); logger.trace("new JWT generated with value {}", jwt); } @@ -205,7 +210,7 @@ public void handleCallback(Context ctx) throws IOException, ExecutionException, var user = objectMapper.readValue(userResponse.getBody(), UserToken.class); // Get and map the guild data - var guildRequest = new OAuthRequest(Verb.GET, String.format("https://discord.com/api/v10/users/@me/guilds/%s/member", Config.DISCORD_GUILD_ID)); + var guildRequest = new OAuthRequest(Verb.GET, String.format("https://discord.com/api/v10/users/@me/guilds/%s/member", this.discordMainGuildId)); service.signRequest(token, guildRequest); Response guildResponse = service.execute(guildRequest); logger.trace(guildResponse.getBody()); diff --git a/src/main/java/com/github/khakers/modmailviewer/auth/UserToken.java b/src/main/java/com/github/khakers/modmailviewer/auth/UserToken.java index 0b202e1f..f5e6c037 100644 --- a/src/main/java/com/github/khakers/modmailviewer/auth/UserToken.java +++ b/src/main/java/com/github/khakers/modmailviewer/auth/UserToken.java @@ -3,6 +3,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; +import org.jetbrains.annotations.Nullable; import java.util.Arrays; import java.util.Objects; @@ -13,6 +14,7 @@ public class UserToken { long id; String username; String discriminator; + @Nullable String avatar; @JsonProperty("roles") @@ -33,7 +35,7 @@ public class UserToken { public UserToken() { } - public UserToken(long id, String username, String discriminator, String avatar, long[] discordRoles, boolean isRealUser) { + public UserToken(long id, String username, String discriminator, @Nullable String avatar, long[] discordRoles, boolean isRealUser) { this.id = id; this.username = username; this.discriminator = discriminator; @@ -102,6 +104,6 @@ public String toString() { } public static UserToken getAnonymousUser() { - return new UserToken(0L, "anonymous", "0000", "", new long[]{}, false); + return new UserToken(0L, "anonymous", "0", null, new long[]{}, false); } } diff --git a/src/main/java/com/github/khakers/modmailviewer/configuration/AppConfig.java b/src/main/java/com/github/khakers/modmailviewer/configuration/AppConfig.java new file mode 100644 index 00000000..2b9e20f3 --- /dev/null +++ b/src/main/java/com/github/khakers/modmailviewer/configuration/AppConfig.java @@ -0,0 +1,31 @@ +package com.github.khakers.modmailviewer.configuration; + + +import org.github.gestalt.config.annotations.Config; + +import java.util.Optional; + +public record AppConfig( + // Not sure why, but per class ConfigPrefix is ignored + // So we're setting the path for each config here + @Config(path = "port.http") + int httpPort, + @Config(path = "port.https") + int httpsPort, + String mongodbUri, + String url, + @Config(path = "ssl") + Optional sslOptions, + boolean secureCookies, +// @Config(defaultVal = "true", path = "auth.enabled") +// boolean isAuthEnabled, + @Config(path = "auth") + Optional auth, + boolean dev, + @Config(path = "audit") + AuditLogConfig auditLogConfig, + @Config(path = "csp") + CSPConfig cspConfig, + long botId +) { +} diff --git a/src/main/java/com/github/khakers/modmailviewer/configuration/AuditLogConfig.java b/src/main/java/com/github/khakers/modmailviewer/configuration/AuditLogConfig.java new file mode 100644 index 00000000..9aecea5a --- /dev/null +++ b/src/main/java/com/github/khakers/modmailviewer/configuration/AuditLogConfig.java @@ -0,0 +1,17 @@ +package com.github.khakers.modmailviewer.configuration; + +import org.github.gestalt.config.annotations.Config; +import org.github.gestalt.config.annotations.ConfigPrefix; + +@ConfigPrefix(prefix = "audit") +public record AuditLogConfig( + @Config(path="enabled") + boolean isAuditLoggingEnabled, + @Config(path = "detailed") + boolean isDetailedAuditingEnabled, + @Config(path = "logApiUsage") + boolean isApiAuditingEnabled, + //TODO plug in retentionPeriod + long retentionPeriod +) { +} diff --git a/src/main/java/com/github/khakers/modmailviewer/configuration/AuthConfig.java b/src/main/java/com/github/khakers/modmailviewer/configuration/AuthConfig.java new file mode 100644 index 00000000..48174b02 --- /dev/null +++ b/src/main/java/com/github/khakers/modmailviewer/configuration/AuthConfig.java @@ -0,0 +1,15 @@ +package com.github.khakers.modmailviewer.configuration; + +import org.github.gestalt.config.annotations.Config; +import org.github.gestalt.config.annotations.ConfigPrefix; + +@ConfigPrefix(prefix = "auth") +public record AuthConfig( + boolean enabled, + @Config(path = "secretkey") + String secretKey, + DiscordClient discordClient, + // TODO: Implement + long accessTokenDuration +) { +} diff --git a/src/main/java/com/github/khakers/modmailviewer/configuration/CSPConfig.java b/src/main/java/com/github/khakers/modmailviewer/configuration/CSPConfig.java new file mode 100644 index 00000000..479dd378 --- /dev/null +++ b/src/main/java/com/github/khakers/modmailviewer/configuration/CSPConfig.java @@ -0,0 +1,12 @@ +package com.github.khakers.modmailviewer.configuration; + +import org.github.gestalt.config.annotations.ConfigPrefix; + +import java.util.Optional; +@ConfigPrefix(prefix = "csp") +public record CSPConfig( + boolean enabled, + Optional extraScriptSources, + Optional override +) { +} diff --git a/src/main/java/com/github/khakers/modmailviewer/configuration/DiscordClient.java b/src/main/java/com/github/khakers/modmailviewer/configuration/DiscordClient.java new file mode 100644 index 00000000..28e979a4 --- /dev/null +++ b/src/main/java/com/github/khakers/modmailviewer/configuration/DiscordClient.java @@ -0,0 +1,10 @@ +package com.github.khakers.modmailviewer.configuration; + +import org.github.gestalt.config.annotations.ConfigPrefix; + +@ConfigPrefix(prefix = "discord") +public record DiscordClient( + String clientId, + String clientSecret, + long guildId) { +} diff --git a/src/main/java/com/github/khakers/modmailviewer/configuration/SSLConfig.java b/src/main/java/com/github/khakers/modmailviewer/configuration/SSLConfig.java new file mode 100644 index 00000000..e679cab6 --- /dev/null +++ b/src/main/java/com/github/khakers/modmailviewer/configuration/SSLConfig.java @@ -0,0 +1,18 @@ +package com.github.khakers.modmailviewer.configuration; + +import org.github.gestalt.config.annotations.Config; + +import java.util.Optional; + +public record SSLConfig( + @Config(defaultVal = "false") + boolean enabled, + Optional cert, + Optional key, + @Config(defaultVal = "false") + boolean httpsOnly, + @Config(path = "sni") + boolean isSNIEnabled, + @Config(path = "sts") + boolean isSTSEnabled) { +} diff --git a/src/main/java/com/github/khakers/modmailviewer/log/LogPage.java b/src/main/java/com/github/khakers/modmailviewer/log/LogPage.java index e0c2d5f1..8c9d8a57 100644 --- a/src/main/java/com/github/khakers/modmailviewer/log/LogPage.java +++ b/src/main/java/com/github/khakers/modmailviewer/log/LogPage.java @@ -15,7 +15,7 @@ public class LogPage extends Page { public LogPage(Context ctx) { super(ctx); - var entry = Main.MOD_MAIL_LOG_CLIENT.getModMailLogEntry(ctx.pathParam("id")); + var entry = Main.modMailLogDB.getModMailLogEntry(ctx.pathParam("id")); if (entry.isEmpty()) { throw new NotFoundResponse("No modmail log entry found"); } diff --git a/src/main/java/com/github/khakers/modmailviewer/log/LogsPage.java b/src/main/java/com/github/khakers/modmailviewer/log/LogsPage.java index a1a42769..7c732f98 100644 --- a/src/main/java/com/github/khakers/modmailviewer/log/LogsPage.java +++ b/src/main/java/com/github/khakers/modmailviewer/log/LogsPage.java @@ -1,5 +1,6 @@ package com.github.khakers.modmailviewer.log; +import com.github.khakers.modmailviewer.Main; import com.github.khakers.modmailviewer.ModMailLogDB; import com.github.khakers.modmailviewer.Page; import com.github.khakers.modmailviewer.data.ModMailLogEntry; @@ -8,7 +9,7 @@ import java.util.List; -import static com.github.khakers.modmailviewer.Main.MOD_MAIL_LOG_CLIENT; +import static com.github.khakers.modmailviewer.Main.modMailLogDB; public class LogsPage extends Page { @@ -18,7 +19,7 @@ public class LogsPage extends Page { public final int currentPage; public final int pageCount; - public final ModMailLogDB modMailLogDB = MOD_MAIL_LOG_CLIENT; + public final ModMailLogDB modMailLogDB = Main.modMailLogDB; public final TicketStatus ticketStatusFilter; public final boolean showNSFW; public final String searchString; @@ -54,10 +55,10 @@ public LogsPage(Context ctx) { .getOrDefault(8); ticketStatusFilter = TicketStatus.valueOf(statusFilter.toUpperCase()); - pageCount = MOD_MAIL_LOG_CLIENT.getPaginationCount(itemsPerPage, ticketStatusFilter, searchString); + pageCount = Main.modMailLogDB.getPaginationCount(itemsPerPage, ticketStatusFilter, searchString); page1 = Math.min(pageCount, page1); currentPage = page1; - logEntries = MOD_MAIL_LOG_CLIENT.searchPaginatedMostRecentEntriesByMessageActivity(currentPage, itemsPerPage, ticketStatusFilter, searchString); + logEntries = Main.modMailLogDB.searchPaginatedMostRecentEntriesByMessageActivity(currentPage, itemsPerPage, ticketStatusFilter, searchString); } @Override diff --git a/src/main/java/com/github/khakers/modmailviewer/page/dashboard/MetricsAccessor.java b/src/main/java/com/github/khakers/modmailviewer/page/dashboard/MetricsAccessor.java index 1572873b..e71c88e8 100644 --- a/src/main/java/com/github/khakers/modmailviewer/page/dashboard/MetricsAccessor.java +++ b/src/main/java/com/github/khakers/modmailviewer/page/dashboard/MetricsAccessor.java @@ -51,7 +51,7 @@ public MetricsAccessor(MongoDatabase modmailDatabase) { this.logCollection = JacksonMongoCollection .builder() .withObjectMapper(objectMapper) - .build(modmailDatabase, Constants.MODMAIL_LOG_COLLECTION_NAME, ModMailLogEntry.class, UuidRepresentation.STANDARD);; + .build(modmailDatabase, Constants.MODMAIL_LOG_COLLECTION_NAME, ModMailLogEntry.class, UuidRepresentation.STANDARD); } public String getTicketsPerDayJson(int period) { diff --git a/src/main/jte/pages/DashboardView.jte b/src/main/jte/pages/DashboardView.jte index 92680648..b56e4524 100644 --- a/src/main/jte/pages/DashboardView.jte +++ b/src/main/jte/pages/DashboardView.jte @@ -46,7 +46,7 @@
Total Tickets
-
${Main.MOD_MAIL_LOG_CLIENT.getTotalTickets(TicketStatus.ALL)}
+
${Main.modMailLogDB.getTotalTickets(TicketStatus.ALL)}
@@ -55,7 +55,7 @@
Open Tickets
-
${Main.MOD_MAIL_LOG_CLIENT.getTotalTickets(TicketStatus.OPEN)}
+
${Main.modMailLogDB.getTotalTickets(TicketStatus.OPEN)}
@@ -64,7 +64,7 @@
Closed Tickets
-
${Main.MOD_MAIL_LOG_CLIENT.getTotalTickets(TicketStatus.CLOSED)}
+
${Main.modMailLogDB.getTotalTickets(TicketStatus.CLOSED)}
diff --git a/src/main/resources/default.properties b/src/main/resources/default.properties new file mode 100644 index 00000000..19e18031 --- /dev/null +++ b/src/main/resources/default.properties @@ -0,0 +1,25 @@ +# This is the default configuration file for the application. + +app.port.http = 7080 +app.port.https = 7443 +app.bot.id = 0 +app.dev = false +app.secureCookies = true + +app.auth.enabled = true +# Not yet used +app.auth.accessTokenDuration = 10800 + +app.ssl.enabled = false +#app.ssl.cert +#app.ssl.key +app.ssl.HttpsOnly = false +app.ssl.sni = false +app.ssl.sts = false + +app.audit.enabled = true +app.audit.detailed = true +app.audit.logApiUsage = true +app.audit.retentionPeriod = 30 + +app.csp.enabled = true From a8055de6676ae9fe1961420ed577e82fe57ea56c Mon Sep 17 00:00:00 2001 From: Khakers <22665282+khakers@users.noreply.github.com> Date: Fri, 18 Aug 2023 18:00:21 -0700 Subject: [PATCH 09/21] refactor: clean up URI validation code --- .../github/khakers/modmailviewer/Main.java | 23 +++++-------------- 1 file changed, 6 insertions(+), 17 deletions(-) diff --git a/src/main/java/com/github/khakers/modmailviewer/Main.java b/src/main/java/com/github/khakers/modmailviewer/Main.java index 6123c219..46c9ca5d 100644 --- a/src/main/java/com/github/khakers/modmailviewer/Main.java +++ b/src/main/java/com/github/khakers/modmailviewer/Main.java @@ -57,10 +57,7 @@ import org.github.gestalt.config.source.EnvironmentConfigSource; import org.jetbrains.annotations.NotNull; -import java.net.MalformedURLException; import java.net.URI; -import java.net.URISyntaxException; -import java.net.URL; import java.nio.file.Path; import java.time.Duration; import java.time.LocalDate; @@ -135,22 +132,14 @@ public static void main(String[] args) throws GestaltException { registerValidators(); - URI uri = null; - try { - uri = new URL(appConfig.url()).toURI(); // Validate the URL - if (uri.getScheme().equals("http")) { - logger.warn("You are running Modmail-Viewer over HTTP with an http callback URI. It is highly recommended that you use HTTPS."); - } else if (uri.getScheme().equals("https") && !appConfig.secureCookies()) { - logger.warn("You are running Modmail-Viewer over HTTPS but have disabled enabled secure cookies. There should be no reason to do this. "); - } - } catch (URISyntaxException e) { - logger.fatal("Invalid URL: " + appConfig.url()); - System.exit(1); - } catch (MalformedURLException e) { - throw new RuntimeException(e); + var uri = URI.create(appConfig.url()); // Validate the URL + if (uri.getScheme().equals("http")) { + logger.warn("You are running Modmail-Viewer over HTTP with an http callback URI. It is highly recommended that you use HTTPS."); + } else if (uri.getScheme().equals("https") && !appConfig.secureCookies()) { + logger.warn("You are running Modmail-Viewer over HTTPS but have disabled enabled secure cookies. There should be no reason to do this. "); } - var callbackUri = uri.resolve("/callback"); // Append the callback path to the URL + var callbackUri = uri.resolve("./callback"); // Append the callback path to the URL logger.debug("Callback URL: " + callbackUri); var connectionString = new ConnectionString(appConfig.mongodbUri()); From 4813cd7c64260b13b9624b7756bf525006bc2a8a Mon Sep 17 00:00:00 2001 From: Khakers <22665282+khakers@users.noreply.github.com> Date: Thu, 31 Aug 2023 16:53:58 -0700 Subject: [PATCH 10/21] feat: Add new static Config class, Analytics and branding config, and migrate navbar to use new classes --- .../github/khakers/modmailviewer/Config.java | 204 ------------------ .../configuration/AppConfig.java | 4 +- .../modmailviewer/configuration/Config.java | 68 ++++++ src/main/jte/macros/navbar.jte | 4 +- src/main/resources/default.properties | 2 + 5 files changed, 74 insertions(+), 208 deletions(-) delete mode 100644 src/main/java/com/github/khakers/modmailviewer/Config.java create mode 100644 src/main/java/com/github/khakers/modmailviewer/configuration/Config.java diff --git a/src/main/java/com/github/khakers/modmailviewer/Config.java b/src/main/java/com/github/khakers/modmailviewer/Config.java deleted file mode 100644 index f86666d0..00000000 --- a/src/main/java/com/github/khakers/modmailviewer/Config.java +++ /dev/null @@ -1,204 +0,0 @@ -package com.github.khakers.modmailviewer; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.apache.logging.log4j.core.util.Assert; -import org.jetbrains.annotations.Nullable; - -import java.math.BigInteger; -import java.security.SecureRandom; -import java.util.Base64; -import java.util.Objects; - -@Deprecated -public class Config { - private static final Logger logger = LogManager.getLogger(); - - private static final String ENV_PREPEND = "MODMAIL_VIEWER"; - - public static final int httpPort = Objects.nonNull(System.getenv(ENV_PREPEND + "_HTTP_PORT")) - ? Integer.parseInt(System.getenv(ENV_PREPEND + "_HTTP_PORT")) - : 80; - - static final int httpsPort = Objects.nonNull(System.getenv(ENV_PREPEND + "_HTTPS_PORT")) - ? Integer.parseInt(System.getenv(ENV_PREPEND + "_HTTPs_PORT")) - : 443; - - public static final String MONGODB_URI = System.getenv(ENV_PREPEND + "_APP_MONGODB_URI"); - - public static final String WEB_URL; - - public static final boolean isSecure = isSetToTrue(System.getenv(ENV_PREPEND + "_SSL")); - - @Nullable - public static final String SSL_CERT = isSecure - ?System.getenv(ENV_PREPEND + "_SSL_CERT") - : null; - @Nullable - public static final String SSL_KEY = isSecure - ? System.getenv(ENV_PREPEND + "_SSL_KEY") - : null; - public static final boolean isHttpsOnly = isSecure && isNotNullAndFalse(System.getenv(ENV_PREPEND + "_HTTPS_ONLY"), true); - - public static final boolean isAuthEnabled = isNotNullAndFalse(System.getenv(ENV_PREPEND + "_AUTH_ENABLED"), true); - - public static final boolean isDevMode = isNotNullAndFalse(System.getenv(ENV_PREPEND + "_DEV"), false); - - public static final boolean isSNIEnabled = isSecure && !isDevMode && isNotNullAndFalse(System.getenv(ENV_PREPEND + "_SNI"), false); - - public static final boolean isSTSEnabled = isNotNullAndFalse(System.getenv(ENV_PREPEND + "_HSTS"), false); - - public static final boolean isAuditLoggingEnabled = isNotNullAndFalse(System.getenv(ENV_PREPEND + "_HSTS")); - public static final boolean isDetailedAuditingEnabled = true; - public static final boolean isApiAuditingEnabled = true; - - public static final String DISCORD_CLIENT_ID = isAuthEnabled - ? System.getenv(ENV_PREPEND + "_DISCORD_OAUTH_CLIENT_ID") - : null; - public static final String DISCORD_CLIENT_SECRET = isAuthEnabled - ? System.getenv(ENV_PREPEND + "_DISCORD_OAUTH_CLIENT_SECRET") - : null; - - public static final long DISCORD_GUILD_ID = Long.parseLong(System.getenv(ENV_PREPEND + "_DISCORD_GUILD_ID")); - - public static final long BOT_ID = Long.parseLong(notEmptyOrElse(System.getenv(ENV_PREPEND + "_BOT_ID"), "0")); - public static final String JWT_SECRET_KEY; - - public static final String BRANDING = notEmptyOrElse(System.getenv(ENV_PREPEND + "_BRANDING"), "Modmail-Viewer"); - - public static final boolean isCookiesSecure = isNotSetToTrue(System.getenv(ENV_PREPEND + "_INSECURE"), true); - - public static final String ANALYTICS_STRING = System.getenv(ENV_PREPEND + "_ANALYTICS"); - public static final String ANALYTICS_STRING_BASE64 = new String(Base64.getDecoder().decode(Objects.requireNonNullElse(System.getenv(ENV_PREPEND + "_ANALYTICS_B64"), ""))); - - public static final String CUSTOM_CSP = System.getenv(ENV_PREPEND + "_CSP"); - - public static final String CSP_SCRIPT_SRC_ELEM_EXTRA = System.getenv(ENV_PREPEND + "_CSP_SCRIPT_SRC_ELEM_EXTRA"); - - static { - var jwtSecretKey = System.getenv("MODMAIL_VIEWER_SECRETKEY"); - if (jwtSecretKey == null || jwtSecretKey.isEmpty()) { - logger.warn("Generated a random key for signing tokens. Sessions will not persist between restarts"); - JWT_SECRET_KEY = new BigInteger(256, new SecureRandom()).toString(32); - } else if (jwtSecretKey.length() < 32) { - JWT_SECRET_KEY = jwtSecretKey; - logger.warn("Your secret key is too short! it should be at least 32 characters (256 bits). Short keys can be trivially brute forced allowing an attacker to create their own auth tokens"); - } else { - JWT_SECRET_KEY = jwtSecretKey; - } - - if (!isCookiesSecure) { - logger.warn("Insecure cookies are enabled. This reduces security and should only be enabled when https is unavailable"); - } - - var webUrl = System.getenv(ENV_PREPEND + "_URL"); - if (webUrl.endsWith("/")) { - logger.warn(ENV_PREPEND + "_WEB_URL has a trailing slash. Removed it due to conflict with the callback."); - webUrl = webUrl.substring(0, webUrl.length() - 1); - } - WEB_URL = webUrl; - } - - private static T notNullObjOrElse(T obj, T defaultObj) { - if (obj == null) { - return defaultObj; - } - return obj; - } - - /** - * Returns the value of obj if it's not empty, otherwise returns defaultObj - * - * @param obj the value to check for emptiness and return if not - * @param defaultObj default value to return if obj is empty - * @return obj if not empty, otherwise defaultObj - */ - private static T notEmptyOrElse(T obj, T defaultObj) { - if (Assert.isEmpty(obj)) { - return defaultObj; - } - return obj; - } - -// /** -// * Runs -// * -// * @param obj -// * @param supplier supplier to run if obj is not null -// * @param defaultObj -// * @param -// * @return value of supplier if not null or defaultObj -// */ -// private static T runIfNotNullElse(T obj, Supplier supplier, T defaultObj) { -// Objects.requireNonNullElseGet() -// } - - /** - * Returns true if the given String value is not null and the content is not "false". - * - * @param s String - * @return - */ - private static boolean isNotNullAndFalse(String s) { - return isNotNullAndFalse(s, false); - } - - /** - * Returns true if the given String value is not "false". - * Returns ifNull if the given string is null - * - * @param s String - * @param ifNull Return value if the String is null - * @return - */ - private static boolean isNotNullAndFalse(String s, boolean ifNull) { - if (s == null) { - return ifNull; - } - return !s.equalsIgnoreCase("false"); - } - - /** - * Returns true if the given String value is not null and the content is not "true". - * Explicitly requires a value of "true" to return true - * - * @param s String - * @return - */ - private static boolean isSetToTrue(String s) { - return isSetToTrue(s, false); - } - - - /** - * Returns true if the given String value is not null and the content is "true". - * Returns ifNull if the given string is null - * Explicitly requires a value of "true" to return true - * - * @param s String - * @param ifNull Return value if the String is null - * @return - */ - private static boolean isSetToTrue(String s, boolean ifNull) { - if (s == null) { - return ifNull; - } - return s.equalsIgnoreCase("true"); - } - - /** - * Returns true if the given String value is not null and the content is not "true". - * Returns ifNull if the given string is null - * Explicitly requires a value of "true" to return true - * - * @param s String - * @param ifNull Return value if the String is null - * @return - */ - private static boolean isNotSetToTrue(String s, boolean ifNull) { - if (s == null) { - return ifNull; - } - return !s.equalsIgnoreCase("true"); - } -} diff --git a/src/main/java/com/github/khakers/modmailviewer/configuration/AppConfig.java b/src/main/java/com/github/khakers/modmailviewer/configuration/AppConfig.java index 2b9e20f3..5cb9e9b2 100644 --- a/src/main/java/com/github/khakers/modmailviewer/configuration/AppConfig.java +++ b/src/main/java/com/github/khakers/modmailviewer/configuration/AppConfig.java @@ -26,6 +26,8 @@ public record AppConfig( AuditLogConfig auditLogConfig, @Config(path = "csp") CSPConfig cspConfig, - long botId + long botId, + String branding, + Optional analytics ) { } diff --git a/src/main/java/com/github/khakers/modmailviewer/configuration/Config.java b/src/main/java/com/github/khakers/modmailviewer/configuration/Config.java new file mode 100644 index 00000000..90328c05 --- /dev/null +++ b/src/main/java/com/github/khakers/modmailviewer/configuration/Config.java @@ -0,0 +1,68 @@ +package com.github.khakers.modmailviewer.configuration; + +import com.github.khakers.modmailviewer.Main; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.github.gestalt.config.Gestalt; +import org.github.gestalt.config.builder.GestaltBuilder; +import org.github.gestalt.config.exceptions.GestaltException; +import org.github.gestalt.config.path.mapper.SnakeCasePathMapper; +import org.github.gestalt.config.source.ClassPathConfigSource; +import org.github.gestalt.config.source.EnvironmentConfigSource; +import org.github.gestalt.config.source.FileConfigSource; +import org.github.gestalt.config.source.SystemPropertiesConfigSource; + +import java.io.File; + +public class Config { + private static final Logger logger = LogManager.getLogger(); + + + public static final Gestalt gestalt; + public static final AppConfig appConfig; + + static { + try { + var configURI = System.getProperty("configFile"); + + var gestaltBuilder = new GestaltBuilder() + .setTreatNullValuesInClassAsErrors(true) + .setTreatMissingValuesAsErrors(false) + .addSource(new ClassPathConfigSource("default.properties")); + if (configURI != null) { + File file = new File(configURI); + if (file.exists() && file.isFile()) { + logger.info("Using config file: " + file.getPath()); + gestaltBuilder.addSource(new FileConfigSource(file)); + } + else { + logger.fatal("Config file does not exist: " + configURI); + throw new RuntimeException("Config file does not exist: " + configURI); + } + + } + gestalt = gestaltBuilder.addSource(new EnvironmentConfigSource(Main.envPrepend)) + .addSource(new SystemPropertiesConfigSource()) + .addDefaultPathMappers() + .addPathMapper(new SnakeCasePathMapper()) + // .addSource(new FileConfigSource(devFile)) + .build(); + + } catch (GestaltException e) { + throw new RuntimeException(e); + } + try { + gestalt.loadConfigs(); + } catch (GestaltException e) { + logger.fatal("Failed to load configs", e); + throw new RuntimeException(e); + } + + try { + appConfig = gestalt.getConfig("app", AppConfig.class); + } catch (GestaltException e) { + logger.fatal("Failed to load application configuration", e); + throw new RuntimeException(e); + } + } +} diff --git a/src/main/jte/macros/navbar.jte b/src/main/jte/macros/navbar.jte index 1c87a8be..4db8b570 100644 --- a/src/main/jte/macros/navbar.jte +++ b/src/main/jte/macros/navbar.jte @@ -1,6 +1,4 @@ -@import com.github.khakers.modmailviewer.Config @import com.github.khakers.modmailviewer.Page -@import com.github.khakers.modmailviewer.auth.AuthHandler @import com.github.khakers.modmailviewer.auth.Role @import com.github.khakers.modmailviewer.util.DiscordUtils @import com.github.khakers.modmailviewer.util.RoleUtils @@ -31,7 +29,7 @@