aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTrumeet <yuuta@yuuta.moe>2021-03-31 15:22:10 -0700
committerTrumeet <yuuta@yuuta.moe>2021-03-31 15:22:10 -0700
commit8a161b9756edb0ed19217f07a56b3455907f3941 (patch)
tree991cde0b86d19ac91971899b27114978e1c52447
parent2745a3e1d15a35492ce98f8d20ee8d1d242020d0 (diff)
downloaddn42peering-8a161b9756edb0ed19217f07a56b3455907f3941.tar
dn42peering-8a161b9756edb0ed19217f07a56b3455907f3941.tar.gz
dn42peering-8a161b9756edb0ed19217f07a56b3455907f3941.tar.bz2
dn42peering-8a161b9756edb0ed19217f07a56b3455907f3941.zip
feat(central): add sudo and admin panelv1.10
-rw-r--r--central/src/main/java/moe/yuuta/dn42peering/admin/AdminHandler.java104
-rw-r--r--central/src/main/java/moe/yuuta/dn42peering/admin/AdminUI.java52
-rw-r--r--central/src/main/java/moe/yuuta/dn42peering/admin/SudoUtils.java14
-rw-r--r--central/src/main/java/moe/yuuta/dn42peering/manage/AdminASNAuthProvider.java43
-rw-r--r--central/src/main/java/moe/yuuta/dn42peering/manage/ManageHandler.java45
-rw-r--r--central/src/main/java/moe/yuuta/dn42peering/portal/HTTPPortalVerticle.java2
-rw-r--r--central/src/main/resources/admin/index.ftlh16
-rw-r--r--central/src/main/resources/admin/sudo.ftlh33
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