diff options
author | Trumeet <yuuta@yuuta.moe> | 2021-03-31 15:22:10 -0700 |
---|---|---|
committer | Trumeet <yuuta@yuuta.moe> | 2021-03-31 15:22:10 -0700 |
commit | 8a161b9756edb0ed19217f07a56b3455907f3941 (patch) | |
tree | 991cde0b86d19ac91971899b27114978e1c52447 | |
parent | 2745a3e1d15a35492ce98f8d20ee8d1d242020d0 (diff) | |
download | dn42peering-8a161b9756edb0ed19217f07a56b3455907f3941.tar dn42peering-8a161b9756edb0ed19217f07a56b3455907f3941.tar.gz dn42peering-8a161b9756edb0ed19217f07a56b3455907f3941.tar.bz2 dn42peering-8a161b9756edb0ed19217f07a56b3455907f3941.zip |
feat(central): add sudo and admin panelv1.10
8 files changed, 297 insertions, 12 deletions
diff --git a/central/src/main/java/moe/yuuta/dn42peering/admin/AdminHandler.java b/central/src/main/java/moe/yuuta/dn42peering/admin/AdminHandler.java new file mode 100644 index 0000000..4844913 --- /dev/null +++ b/central/src/main/java/moe/yuuta/dn42peering/admin/AdminHandler.java @@ -0,0 +1,104 @@ +package moe.yuuta.dn42peering.admin; + +import io.vertx.core.Vertx; +import io.vertx.core.http.Cookie; +import io.vertx.core.impl.logging.Logger; +import io.vertx.core.impl.logging.LoggerFactory; +import io.vertx.core.json.JsonObject; +import io.vertx.ext.web.Router; +import io.vertx.ext.web.common.template.TemplateEngine; +import io.vertx.ext.web.handler.BasicAuthHandler; +import io.vertx.ext.web.handler.BodyHandler; +import io.vertx.ext.web.handler.SessionHandler; +import io.vertx.ext.web.sstore.LocalSessionStore; +import io.vertx.ext.web.templ.freemarker.FreeMarkerTemplateEngine; +import io.vertx.ext.web.validation.RequestParameters; +import io.vertx.ext.web.validation.RequestPredicate; +import io.vertx.ext.web.validation.ValidationHandler; +import io.vertx.ext.web.validation.builder.Bodies; +import io.vertx.json.schema.SchemaParser; +import io.vertx.json.schema.SchemaRouter; +import io.vertx.json.schema.SchemaRouterOptions; +import io.vertx.json.schema.common.dsl.ObjectSchemaBuilder; +import moe.yuuta.dn42peering.asn.IASNService; +import moe.yuuta.dn42peering.manage.AdminASNAuthProvider; +import moe.yuuta.dn42peering.portal.ISubRouter; + +import javax.annotation.Nonnull; + +import java.util.UUID; + +import static io.vertx.json.schema.common.dsl.Schemas.*; + +public class AdminHandler implements ISubRouter { + private final Logger logger = LoggerFactory.getLogger(getClass().getSimpleName()); + + @Nonnull + @Override + public Router mount(@Nonnull Vertx vertx) { + final IASNService asnService = IASNService.createProxy(vertx, IASNService.ADDRESS); + final TemplateEngine engine = FreeMarkerTemplateEngine.create(vertx, "ftlh"); + + final Router router = Router.router(vertx); + router.post().handler(BodyHandler.create().setBodyLimit(100 * 1024)); + router.route().handler(SessionHandler.create(LocalSessionStore.create(vertx))); + router.route().handler( + BasicAuthHandler.create( + new AdminASNAuthProvider(vertx.getOrCreateContext() + .config() + .getString("admin", UUID.randomUUID().toString()), + asnService), "admin portal")); + + router.get("/") + .produces("text/html") + .handler(ctx -> { + final String asn = ctx.user().principal().getString("username"); + AdminUI.renderIndex(engine, asn, ctx); + }); + + router.get("/sudo") + .produces("text/html") + .handler(ctx -> { + final String asn = ctx.user().principal().getString("username"); + final Cookie cookie = ctx.getCookie(SudoUtils.SUDO_COOKIE); + AdminUI.renderSudo(engine, asn, null, + cookie == null ? null : SudoUtils.getTargetASN(cookie), ctx); + }); + + final ObjectSchemaBuilder registerSchema = objectSchema() + .allowAdditionalProperties(false) + .property("asn", stringSchema()); + final SchemaParser parser = SchemaParser.createDraft7SchemaParser( + SchemaRouter.create(vertx, new SchemaRouterOptions())); + + router.post("/sudo") + .handler(BodyHandler.create().setBodyLimit(100 * 1024)) + .handler(ValidationHandler + .builder(parser) + .body(Bodies.formUrlEncoded(registerSchema)) + .predicate(RequestPredicate.BODY_REQUIRED) + .build()) + .handler(ctx -> { + final String asn = ctx.user().principal().getString("username"); + final JsonObject parameters = ctx.<RequestParameters>get(ValidationHandler.REQUEST_CONTEXT_KEY) + .body().getJsonObject(); + final String targetASN = parameters.getString("asn"); + if (asn == null || asn.equals("")) { + // Clear + ctx.removeCookie(SudoUtils.SUDO_COOKIE); + ctx.response() + .setStatusCode(303) + .putHeader("Location", "/admin") + .end(); + } else { + ctx.addCookie(Cookie.cookie(SudoUtils.SUDO_COOKIE, targetASN).setPath("/")); + ctx.response() + .setStatusCode(303) + .putHeader("Location", "/manage") + .end(); + } + }); + + return router; + } +} diff --git a/central/src/main/java/moe/yuuta/dn42peering/admin/AdminUI.java b/central/src/main/java/moe/yuuta/dn42peering/admin/AdminUI.java new file mode 100644 index 0000000..59dbe08 --- /dev/null +++ b/central/src/main/java/moe/yuuta/dn42peering/admin/AdminUI.java @@ -0,0 +1,52 @@ +package moe.yuuta.dn42peering.admin; + +import com.wireguard.crypto.Key; +import com.wireguard.crypto.KeyFormatException; +import edazdarevic.commons.net.CIDRUtils; +import io.vertx.core.AsyncResult; +import io.vertx.core.Future; +import io.vertx.core.Handler; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.json.JsonObject; +import io.vertx.ext.web.RoutingContext; +import io.vertx.ext.web.common.template.TemplateEngine; +import moe.yuuta.dn42peering.node.INodeService; +import moe.yuuta.dn42peering.node.Node; +import moe.yuuta.dn42peering.peer.Peer; +import moe.yuuta.dn42peering.peer.ProvisionStatus; +import moe.yuuta.dn42peering.portal.FormException; +import org.apache.commons.validator.routines.InetAddressValidator; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.io.IOException; +import java.net.Inet6Address; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import static moe.yuuta.dn42peering.portal.RenderingUtils.getGeneralRenderingHandler; + +class AdminUI { + public static void renderIndex(@Nonnull TemplateEngine engine, + @Nonnull String asn, + @Nonnull RoutingContext ctx) { + final Map<String, Object> root = new HashMap<>(); + root.put("asn", asn); + engine.render(root, "admin/index.ftlh", getGeneralRenderingHandler(ctx)); + } + + public static void renderSudo(@Nonnull TemplateEngine engine, + @Nonnull String asn, + @Nullable List<String> errors, + @Nullable String targetASN, + @Nonnull RoutingContext ctx) { + final Map<String, Object> root = new HashMap<>(); + root.put("asn", asn); + root.put("errors", errors); + root.put("target_asn", targetASN); + engine.render(root, "admin/sudo.ftlh", getGeneralRenderingHandler(ctx)); + } +} diff --git a/central/src/main/java/moe/yuuta/dn42peering/admin/SudoUtils.java b/central/src/main/java/moe/yuuta/dn42peering/admin/SudoUtils.java new file mode 100644 index 0000000..790d016 --- /dev/null +++ b/central/src/main/java/moe/yuuta/dn42peering/admin/SudoUtils.java @@ -0,0 +1,14 @@ +package moe.yuuta.dn42peering.admin; + +import io.vertx.core.http.Cookie; + +import javax.annotation.Nonnull; + +public class SudoUtils { + public static final String SUDO_COOKIE = "sudo"; + + @Nonnull + public static String getTargetASN(@Nonnull Cookie cookie) { + return cookie.getValue(); + } +} diff --git a/central/src/main/java/moe/yuuta/dn42peering/manage/AdminASNAuthProvider.java b/central/src/main/java/moe/yuuta/dn42peering/manage/AdminASNAuthProvider.java new file mode 100644 index 0000000..d737597 --- /dev/null +++ b/central/src/main/java/moe/yuuta/dn42peering/manage/AdminASNAuthProvider.java @@ -0,0 +1,43 @@ +package moe.yuuta.dn42peering.manage; + +import io.vertx.core.AsyncResult; +import io.vertx.core.Future; +import io.vertx.core.Handler; +import io.vertx.core.impl.logging.Logger; +import io.vertx.core.impl.logging.LoggerFactory; +import io.vertx.core.json.JsonObject; +import io.vertx.ext.auth.User; +import moe.yuuta.dn42peering.asn.IASNService; + +import javax.annotation.Nonnull; + +public class AdminASNAuthProvider extends ASNAuthProvider { + private final Logger logger = LoggerFactory.getLogger(getClass().getSimpleName()); + + private final String adminASN; + + public AdminASNAuthProvider(@Nonnull String adminASN, + @Nonnull IASNService asnService) { + super(asnService); + this.adminASN = adminASN; + } + + @Override + public void authenticate(JsonObject credentials, Handler<AsyncResult<User>> resultHandler) { + super.authenticate(credentials, ar -> { + if(ar.failed()) { + resultHandler.handle(ar); + return; + } + final User user = ar.result(); + if(!adminASN.equals(user.principal().getString("username"))) { + logger.warn("Unsuccessful admin attempt by " + user.principal().getString("username")); + resultHandler.handle(Future.failedFuture("Incorrect logon")); + } else { + logger.info("Successful admin attempt by " + user.principal().getString("username")); + user.attributes().put("admin", true); + resultHandler.handle(ar); + } + }); + } +} diff --git a/central/src/main/java/moe/yuuta/dn42peering/manage/ManageHandler.java b/central/src/main/java/moe/yuuta/dn42peering/manage/ManageHandler.java index b64bb13..b6ff237 100644 --- a/central/src/main/java/moe/yuuta/dn42peering/manage/ManageHandler.java +++ b/central/src/main/java/moe/yuuta/dn42peering/manage/ManageHandler.java @@ -2,10 +2,12 @@ package moe.yuuta.dn42peering.manage; import io.vertx.core.Future; import io.vertx.core.Vertx; +import io.vertx.core.http.Cookie; import io.vertx.core.impl.logging.Logger; import io.vertx.core.impl.logging.LoggerFactory; import io.vertx.core.json.JsonObject; import io.vertx.ext.web.Router; +import io.vertx.ext.web.RoutingContext; import io.vertx.ext.web.common.template.TemplateEngine; import io.vertx.ext.web.handler.BasicAuthHandler; import io.vertx.ext.web.handler.BodyHandler; @@ -20,6 +22,7 @@ import io.vertx.json.schema.SchemaParser; import io.vertx.json.schema.SchemaRouter; import io.vertx.json.schema.SchemaRouterOptions; import io.vertx.json.schema.common.dsl.ObjectSchemaBuilder; +import moe.yuuta.dn42peering.admin.SudoUtils; import moe.yuuta.dn42peering.asn.IASNService; import moe.yuuta.dn42peering.jaba.Pair; import moe.yuuta.dn42peering.node.INodeService; @@ -59,7 +62,7 @@ public class ManageHandler implements ISubRouter { router.route().handler(BasicAuthHandler.create(new ASNAuthProvider(asnService), "manage portal")); router.route().handler(ctx -> { // Mark as activated. - asnService.markAsActivated(ctx.user().principal().getString("username"), ar -> { + asnService.markAsActivated(getActualASN(ctx, false), ar -> { if (ar.succeeded()) { ctx.next(); } else { @@ -71,7 +74,7 @@ public class ManageHandler implements ISubRouter { router.get("/") .produces("text/html") .handler(ctx -> { - final String asn = ctx.user().principal().getString("username"); + final String asn = getActualASN(ctx, true); Future.<List<moe.yuuta.dn42peering.peer.Peer>>future(f -> peerService.listUnderASN(asn, f)) .onSuccess(peers -> renderIndex(engine, asn, peers, ctx)) @@ -81,7 +84,7 @@ public class ManageHandler implements ISubRouter { router.get("/new") .produces("text/html") .handler(ctx -> { - final String asn = ctx.user().principal().getString("username"); + final String asn = getActualASN(ctx, true); renderForm(engine, nodeService, true, asn, null, null, ctx); }); @@ -106,7 +109,7 @@ public class ManageHandler implements ISubRouter { .predicate(RequestPredicate.BODY_REQUIRED) .build()) .handler(ctx -> { - final String asn = ctx.user().principal().getString("username"); + final String asn = getActualASN(ctx, true); final JsonObject parameters = ctx.<RequestParameters>get(ValidationHandler.REQUEST_CONTEXT_KEY) .body().getJsonObject(); // Parse peer @@ -174,7 +177,7 @@ public class ManageHandler implements ISubRouter { .queryParameter(param("id", stringSchema())) .build()) .handler(ctx -> { - final String asn = ctx.user().principal().getString("username"); + final String asn = getActualASN(ctx, true); final String id = ctx.<RequestParameters>get(ValidationHandler.REQUEST_CONTEXT_KEY) .queryParameter("id").getString(); Future.<Peer>future(f -> peerService.getSingle(asn, id, f)) @@ -198,7 +201,7 @@ public class ManageHandler implements ISubRouter { .predicate(RequestPredicate.BODY_REQUIRED) .build()) .handler(ctx -> { - final String asn = ctx.user().principal().getString("username"); + final String asn = getActualASN(ctx, true); final JsonObject parameters = ctx.<RequestParameters>get(ValidationHandler.REQUEST_CONTEXT_KEY) .body().getJsonObject(); final String id = ctx.<RequestParameters>get(ValidationHandler.REQUEST_CONTEXT_KEY) @@ -291,7 +294,7 @@ public class ManageHandler implements ISubRouter { .queryParameter(param("id", stringSchema())) .build()) .handler(ctx -> { - final String asn = ctx.user().principal().getString("username"); + final String asn = getActualASN(ctx, true); final String id = ctx.<RequestParameters>get(ValidationHandler.REQUEST_CONTEXT_KEY) .queryParameter("id").getString(); Future.<Peer>future(f -> peerService.getSingle(asn, id, f)) @@ -316,7 +319,7 @@ public class ManageHandler implements ISubRouter { router.get("/change-password") .produces("text/html") .handler(ctx -> { - final String asn = ctx.user().principal().getString("username"); + final String asn = getActualASN(ctx, true); renderChangepw(engine, asn, null, ctx); }); @@ -331,7 +334,7 @@ public class ManageHandler implements ISubRouter { .predicate(RequestPredicate.BODY_REQUIRED) .build()) .handler(ctx -> { - final String asn = ctx.user().principal().getString("username"); + final String asn = getActualASN(ctx, true); final JsonObject parameters = ctx.<RequestParameters>get(ValidationHandler.REQUEST_CONTEXT_KEY) .body().getJsonObject(); final String passwd = parameters.getString("passwd"); @@ -369,14 +372,14 @@ public class ManageHandler implements ISubRouter { router.get("/delete-account") .produces("text/html") .handler(ctx -> { - final String asn = ctx.user().principal().getString("username"); + final String asn = getActualASN(ctx, true); renderDA(engine, asn, null, ctx); }); router.post("/delete-account") .produces("text/html") .handler(ctx -> { - final String asn = ctx.user().principal().getString("username"); + final String asn = getActualASN(ctx, true); Future.<Void>future(f -> peerService.existsUnderASN(asn, ar -> { if (ar.succeeded()) { if (ar.result()) { @@ -414,7 +417,7 @@ public class ManageHandler implements ISubRouter { .queryParameter(param("id", stringSchema())) .build()) .handler(ctx -> { - final String asn = ctx.user().principal().getString("username"); + final String asn = getActualASN(ctx, true); final String id = ctx.<RequestParameters>get(ValidationHandler.REQUEST_CONTEXT_KEY) .queryParameter("id").getString(); Future.<Peer>future(f -> peerService.getSingle(asn, id, f)) @@ -430,4 +433,22 @@ public class ManageHandler implements ISubRouter { return router; } + + @Nonnull + private String getActualASN(@Nonnull RoutingContext ctx, boolean acceptSudo) { + final String authASN = ctx.user().principal().getString("username"); + if(!acceptSudo) { + return authASN; + } + if(ctx.getCookie(SudoUtils.SUDO_COOKIE) == null) { + return authASN; + } + final Cookie sudoCookie = ctx.getCookie(SudoUtils.SUDO_COOKIE); + if(ctx.user().attributes().getBoolean("admin") == null) { + // Unauthorized + logger.warn("Unauthorized sudo attempt by " + authASN + ", target ASN: " + SudoUtils.getTargetASN(sudoCookie)); + return authASN; + } + return SudoUtils.getTargetASN(sudoCookie); + } } diff --git a/central/src/main/java/moe/yuuta/dn42peering/portal/HTTPPortalVerticle.java b/central/src/main/java/moe/yuuta/dn42peering/portal/HTTPPortalVerticle.java index c68447a..d7c6d9f 100644 --- a/central/src/main/java/moe/yuuta/dn42peering/portal/HTTPPortalVerticle.java +++ b/central/src/main/java/moe/yuuta/dn42peering/portal/HTTPPortalVerticle.java @@ -9,6 +9,7 @@ import io.vertx.core.json.JsonObject; import io.vertx.ext.web.Router; import io.vertx.ext.web.common.template.TemplateEngine; import io.vertx.ext.web.templ.freemarker.FreeMarkerTemplateEngine; +import moe.yuuta.dn42peering.admin.AdminHandler; import moe.yuuta.dn42peering.asn.ASNHandler; import moe.yuuta.dn42peering.manage.ManageHandler; @@ -37,6 +38,7 @@ public class HTTPPortalVerticle extends AbstractVerticle { }); router.mountSubRouter("/asn", new ASNHandler().mount(vertx)); router.mountSubRouter("/manage", new ManageHandler().mount(vertx)); + router.mountSubRouter("/admin", new AdminHandler().mount(vertx)); router.errorHandler(500, ctx -> { if(ctx.failure() instanceof HTTPException) { ctx.response().setStatusCode(((HTTPException) ctx.failure()).code).end(); diff --git a/central/src/main/resources/admin/index.ftlh b/central/src/main/resources/admin/index.ftlh new file mode 100644 index 0000000..fa6e28f --- /dev/null +++ b/central/src/main/resources/admin/index.ftlh @@ -0,0 +1,16 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <#include "../style.ftlh"> + <title>Admin</title> +</head> +<body class="markdown-body"> +<h1>Admin Portal</h1> +<p>You are logged in as: ${asn}.</p> +<p>dn42peering administrative tasks.</p> +<ul> +<li><a href="/admin/sudo">sudo</a></li> +</ul> +</body> +</html>
\ No newline at end of file diff --git a/central/src/main/resources/admin/sudo.ftlh b/central/src/main/resources/admin/sudo.ftlh new file mode 100644 index 0000000..65db517 --- /dev/null +++ b/central/src/main/resources/admin/sudo.ftlh @@ -0,0 +1,33 @@ +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <#include "../style.ftlh"> + <title>sudo | Admin</title> +</head> +<body class="markdown-body"> +<h1>sudo</h1> +<p>You are logged in as: ${asn}.</p> +<p>Set a cookie that allows you to sudo as another ASN.</p> +<p>The target ASN will not be created if it does not exist.</p> +<p>The target ASN may not exist in the WHOIS database.</p> +<#if errors??> +<div> + <p style="color:red">Errors in the previous form:</p> + <ul> + <#list errors as error> + <li>${error}</li> + </#list> + </ul> +</div> +</#if> +<form action="/admin/sudo" method="post"> + <label for="asn">Target ASN:</label><br /> + <input type="text" id="asn" name="asn" + placeholder="${asn}" + value="${(target_asn)!}"><br /> + <br /> + <input type="submit"> +</form> +</body> +</html>
\ No newline at end of file |