diff --git a/src/main/java/eu/m724/GlobalAccessLimits.java b/src/main/java/eu/m724/GlobalAccessLimits.java index 750da8c..c494bf5 100644 --- a/src/main/java/eu/m724/GlobalAccessLimits.java +++ b/src/main/java/eu/m724/GlobalAccessLimits.java @@ -13,34 +13,24 @@ import eu.m724.orm.AccessLimits; * if you were to sell access you can "tweak" some of this and rip off people */ public class GlobalAccessLimits { + public static AccessLimits kilo, mega, giga, tera; + public static void initialize() { - AccessLimits kilo = new AccessLimits(); - kilo.label = "kilo"; - kilo.thunder = true; - kilo.weather = true; - kilo.weatherRequestsHourly = 60; - - AccessLimits mega = new AccessLimits(); - mega.label = "plus"; - mega.thunder = true; - mega.weather = true; - mega.weatherRequestsHourly = 120; - - AccessLimits giga = new AccessLimits(); - giga.label = "giga"; - giga.thunder = true; - giga.weather = true; - giga.weatherRequestsHourly = 240; - - AccessLimits tera = new AccessLimits(); - tera.label = "giga"; - tera.thunder = true; - tera.weather = true; - tera.weatherRequestsHourly = 480; - + AccessLimits kilo = new AccessLimits("kilo", true, true, 60); kilo.persist(); + + AccessLimits mega = new AccessLimits("mega", true, true, 120); mega.persist(); + + AccessLimits giga = new AccessLimits("giga", true, true, 240); giga.persist(); + + AccessLimits tera = new AccessLimits("tera", true, true, 480); tera.persist(); + + GlobalAccessLimits.kilo = kilo; + GlobalAccessLimits.mega = mega; + GlobalAccessLimits.giga = giga; + GlobalAccessLimits.tera = tera; } } diff --git a/src/main/java/eu/m724/GlobalExceptionHandler.java b/src/main/java/eu/m724/GlobalExceptionHandler.java new file mode 100644 index 0000000..b922b7a --- /dev/null +++ b/src/main/java/eu/m724/GlobalExceptionHandler.java @@ -0,0 +1,39 @@ +package eu.m724; + +import jakarta.json.Json; +import jakarta.json.JsonObject; +import jakarta.json.stream.JsonParsingException; +import jakarta.ws.rs.ClientErrorException; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.ext.ExceptionMapper; +import jakarta.ws.rs.ext.Provider; + +@Provider +public class GlobalExceptionHandler implements ExceptionMapper { + + @Override + public Response toResponse(Throwable exception) { + System.out.println("andling error" + exception.getMessage()); + + int status = Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(); + String message = "An error has occurred"; + + if (exception instanceof ClientErrorException) { + status = ((ClientErrorException) exception).getResponse().getStatus(); + message = exception.getMessage(); + } else if (exception instanceof JsonParsingException) { + status = Response.Status.BAD_REQUEST.getStatusCode(); + message = "Valid JSON expected"; // TODO make this error better + + } + + JsonObject errorJson = Json.createObjectBuilder() + .add("status", status) + .add("message", message) + .build(); + + return Response.status(status) + .entity(errorJson) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/eu/m724/KeysResource.java b/src/main/java/eu/m724/KeysResource.java deleted file mode 100644 index 8b54fa2..0000000 --- a/src/main/java/eu/m724/KeysResource.java +++ /dev/null @@ -1,21 +0,0 @@ -package eu.m724; - -import eu.m724.auth.master.AccountService; -import io.quarkus.security.identity.SecurityIdentity; -import jakarta.inject.Inject; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.Produces; -import jakarta.ws.rs.core.MediaType; - -/** - * managing access keys (not master keys or accounts) - */ -@Path("/api/keys") -@Produces(MediaType.APPLICATION_JSON) -public class KeysResource { - @Inject - SecurityIdentity securityIdentity; - - @Inject - AccountService accountService; -} diff --git a/src/main/java/eu/m724/Startup.java b/src/main/java/eu/m724/Startup.java index 2e65a16..2e37ea4 100644 --- a/src/main/java/eu/m724/Startup.java +++ b/src/main/java/eu/m724/Startup.java @@ -1,10 +1,8 @@ package eu.m724; -import eu.m724.auth.master.AccountService; -import eu.m724.orm.Account; +import eu.m724.orm.Token; import io.quarkus.runtime.StartupEvent; import jakarta.enterprise.event.Observes; -import jakarta.inject.Inject; import jakarta.inject.Singleton; import jakarta.transaction.Transactional; @@ -12,15 +10,18 @@ import java.util.Base64; @Singleton public class Startup { - @Inject - AccountService accountService; - @Transactional public void loadUsers(@Observes StartupEvent ignoredEvent) { - Account.deleteAll(); + GlobalAccessLimits.initialize(); + Token.deleteAll(); byte[] adminKey = new byte[18]; - accountService.add(adminKey, "admin"); - System.out.println("Admin user created: " + Base64.getEncoder().encodeToString(adminKey)); + Token token = new Token(); + token.accessLimits = GlobalAccessLimits.kilo; + token.role = "admin"; + token.tokenBytes = adminKey; + token.persist(); + + System.out.println("Admin token created: " + Base64.getEncoder().encodeToString(adminKey)); } } diff --git a/src/main/java/eu/m724/TokensResource.java b/src/main/java/eu/m724/TokensResource.java new file mode 100644 index 0000000..87efeef --- /dev/null +++ b/src/main/java/eu/m724/TokensResource.java @@ -0,0 +1,57 @@ +package eu.m724; + +import eu.m724.orm.AccessLimits; +import eu.m724.orm.Token; +import io.quarkus.security.identity.SecurityIdentity; +import jakarta.annotation.security.RolesAllowed; +import jakarta.inject.Inject; +import jakarta.json.Json; +import jakarta.json.JsonObject; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.MediaType; + +@Path("/api/tokens") +@Produces(MediaType.APPLICATION_JSON) +public class TokensResource { + @Inject + SecurityIdentity securityIdentity; + + @PUT + @Consumes(MediaType.APPLICATION_JSON) + @Path("/create") + @RolesAllowed("admin") + public JsonObject createToken(JsonObject data) { + String label = data.getString("accessLimits", null); + if (label == null) { + throw new BadRequestException("Specify access limits in the 'accessLimits' key"); + } + + AccessLimits accessLimits = AccessLimits.findByLabel(label); + if (accessLimits == null) { + throw new BadRequestException("Unknown access limits: " + label); + } + + + String token = Token.generate(accessLimits); + + return Json.createObjectBuilder() + .add("token", token) + .build(); + } + + @GET + @Path("/me") + @RolesAllowed({"user", "admin"}) + public JsonObject me() { + Token token = securityIdentity.getAttribute("token"); + String tokenEncoded = securityIdentity.getPrincipal().getName(); + + String censoredToken = tokenEncoded.substring(0, 5) + "..." + tokenEncoded.substring(tokenEncoded.length() - 5); + + return Json.createObjectBuilder() + .add("token", censoredToken) + .add("role", token.role) + .add("accessLimits", token.accessLimits.label) + .build(); + } +} diff --git a/src/main/java/eu/m724/UsersResource.java b/src/main/java/eu/m724/UsersResource.java deleted file mode 100644 index a3d2cd5..0000000 --- a/src/main/java/eu/m724/UsersResource.java +++ /dev/null @@ -1,50 +0,0 @@ -package eu.m724; - -import eu.m724.auth.master.AccountService; -import eu.m724.orm.Account; -import io.quarkus.security.identity.SecurityIdentity; -import jakarta.annotation.security.RolesAllowed; -import jakarta.inject.Inject; -import jakarta.json.Json; -import jakarta.json.JsonObject; -import jakarta.ws.rs.GET; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.Produces; -import jakarta.ws.rs.core.MediaType; - -@Path("/api/users") -@Produces(MediaType.APPLICATION_JSON) -public class UsersResource { - @Inject - SecurityIdentity securityIdentity; - - @Inject - AccountService accountService; - - @GET - @Path("/create") - @RolesAllowed("admin") - public JsonObject createAccount() { - String masterKey = accountService.create("user"); - - return Json.createObjectBuilder() - .add("masterKey", masterKey) - .build(); - } - - @GET - @Path("/me") - @RolesAllowed({"user", "admin"}) - public JsonObject me() { - Account account = securityIdentity.getAttribute("account"); - String masterKey = securityIdentity.getPrincipal().getName(); - - String censoredKey = masterKey.substring(0, 5) + "..." + masterKey.substring(masterKey.length() - 5); - - return Json.createObjectBuilder() - .add("masterKey", censoredKey) - .add("role", account.role) - .add("accessKeys", account.accessKeys.size()) - .build(); - } -} diff --git a/src/main/java/eu/m724/auth/master/AccessKeyService.java b/src/main/java/eu/m724/auth/master/AccessKeyService.java deleted file mode 100644 index b281a87..0000000 --- a/src/main/java/eu/m724/auth/master/AccessKeyService.java +++ /dev/null @@ -1,42 +0,0 @@ -package eu.m724.auth.master; - -import eu.m724.orm.AccessKey; -import eu.m724.orm.AccessLimits; -import eu.m724.orm.Account; -import jakarta.enterprise.context.ApplicationScoped; -import jakarta.transaction.Transactional; - -import java.security.SecureRandom; -import java.util.Base64; - -@ApplicationScoped -public class AccessKeyService { - private final SecureRandom random = new SecureRandom(); - - /** - * generates an access key for an account - * @param account the account - * @param accessLimits access limits - * @return base64 encoded access key - */ - @Transactional - public String createAccessKey(Account account, AccessLimits accessLimits) { - byte[] key = new byte[18]; - random.nextBytes(key); - - AccessKey accessKey = new AccessKey(); - accessKey.key = key; - accessKey.account = account; - accessKey.accessLimits = accessLimits; - - account.accessKeys.add(accessKey); - account.persist(); - - return Base64.getEncoder().encodeToString(key); - } - - @Transactional - public void deleteAccessKey(AccessKey accessKey) { - accessKey.account = null; // TODO hopefully that works - } -} diff --git a/src/main/java/eu/m724/auth/master/AccountService.java b/src/main/java/eu/m724/auth/master/AccountService.java deleted file mode 100644 index 55bce50..0000000 --- a/src/main/java/eu/m724/auth/master/AccountService.java +++ /dev/null @@ -1,73 +0,0 @@ -package eu.m724.auth.master; - -import eu.m724.orm.AccessKey; -import eu.m724.orm.Account; -import jakarta.enterprise.context.ApplicationScoped; -import jakarta.transaction.Transactional; - -import java.security.SecureRandom; -import java.util.Base64; - -@ApplicationScoped -public class AccountService { - private final SecureRandom random = new SecureRandom(); - - /** - * find a master user by key - * @param key base64 encoded key - * @return the master user or null if wrong key or key is null - */ - @Transactional - public Account findByKey(String key) { - if (key == null) return null; - - try { - return Account.find("masterKey", (Object) Base64.getDecoder().decode(key)).firstResult(); - } catch (IllegalArgumentException e) { - return null; - } - } - - /** - * gets an access key - * @param bytes access key as bytes - * @return the {@link AccessKey} if correct else null even if key is null - */ - @Transactional - public AccessKey findByAccessKey(byte[] bytes) { - if (bytes == null) return null; - - try { - return AccessKey.find("key", (Object) bytes).firstResult(); - } catch (IllegalArgumentException e) { - return null; - } - } - - // TODO maybe move some of these methods somewhere else and reconsider making them static - - /** - * creates an account with the specified key - * @param masterKey the desired master key - */ - @Transactional - public void add(byte[] masterKey, String role) { - Account account = new Account(); - account.masterKey = masterKey; - account.role = role; - account.persist(); - } - - /** - * creates an account with random key - * @param role new account's role - * @return base64 encoded key - */ - public String create(String role) { - byte[] key = new byte[18]; // 144 bits of entropy - random.nextBytes(key); - - add(key, role); - return Base64.getEncoder().encodeToString(key); - } -} diff --git a/src/main/java/eu/m724/auth/master/MyHttpAuthenticationMechanism.java b/src/main/java/eu/m724/auth/master/MyHttpAuthenticationMechanism.java index 398782d..897058c 100644 --- a/src/main/java/eu/m724/auth/master/MyHttpAuthenticationMechanism.java +++ b/src/main/java/eu/m724/auth/master/MyHttpAuthenticationMechanism.java @@ -1,6 +1,6 @@ package eu.m724.auth.master; -import eu.m724.orm.Account; +import eu.m724.orm.Token; import io.quarkus.security.identity.IdentityProviderManager; import io.quarkus.security.identity.SecurityIdentity; import io.quarkus.security.runtime.QuarkusPrincipal; @@ -11,27 +11,23 @@ import io.smallrye.mutiny.Uni; import io.smallrye.mutiny.infrastructure.Infrastructure; import io.vertx.ext.web.RoutingContext; import jakarta.enterprise.context.ApplicationScoped; -import jakarta.inject.Inject; @ApplicationScoped public class MyHttpAuthenticationMechanism implements HttpAuthenticationMechanism { - - @Inject - AccountService accountService; - @Override public Uni authenticate(RoutingContext context, IdentityProviderManager identityProviderManager) { - String encodedKey = context.request().getHeader("X-Master-Key"); + String encodedToken = context.request().getHeader("X-Token"); return Uni.createFrom().item(() -> { - Account account = accountService.findByKey(encodedKey); + Token token = Token.findByToken(encodedToken); - if (account != null) { + if (token != null) { QuarkusSecurityIdentity identity = QuarkusSecurityIdentity.builder() - .setPrincipal(new QuarkusPrincipal(encodedKey)) - .addRole(account.role) - .addAttribute("account", account) + .setPrincipal(new QuarkusPrincipal(encodedToken)) + .addRole(token.role) + .addAttribute("token", token) .build(); + return (SecurityIdentity) identity; } return null; @@ -41,6 +37,6 @@ public class MyHttpAuthenticationMechanism implements HttpAuthenticationMechanis @Override public Uni getChallenge(RoutingContext context) { - return Uni.createFrom().item(new ChallengeData(401, "WWW-Authenticate", "X-Master-Key")); + return Uni.createFrom().item(new ChallengeData(401, "WWW-Authenticate", "X-Token")); } } diff --git a/src/main/java/eu/m724/orm/AccessKey.java b/src/main/java/eu/m724/orm/AccessKey.java deleted file mode 100644 index b63b7e6..0000000 --- a/src/main/java/eu/m724/orm/AccessKey.java +++ /dev/null @@ -1,25 +0,0 @@ -package eu.m724.orm; - -import io.quarkus.hibernate.orm.panache.PanacheEntity; -import jakarta.persistence.Column; -import jakarta.persistence.ManyToOne; - -//@Entity -public class AccessKey extends PanacheEntity { - /** - * raw bytes of this key, it's provided to users in base64 - */ - @Column(unique = true) - public byte[] key; - - /** - * access limits of this key - */ - public AccessLimits accessLimits; - - /** - * the user owning this access key - */ - @ManyToOne - public Account account; -} diff --git a/src/main/java/eu/m724/orm/AccessLimits.java b/src/main/java/eu/m724/orm/AccessLimits.java index 7f9a784..1517261 100644 --- a/src/main/java/eu/m724/orm/AccessLimits.java +++ b/src/main/java/eu/m724/orm/AccessLimits.java @@ -6,6 +6,15 @@ import jakarta.persistence.Entity; @Entity public class AccessLimits extends PanacheEntity { + public AccessLimits() {} + + public AccessLimits(String label, boolean thunder, boolean weather, int weatherRequestsHourly) { + this.label = label; + this.thunder = thunder; + this.weather = weather; + this.weatherRequestsHourly = weatherRequestsHourly; + } + /** * label of these limits, displayed to user and used to identify the limits */ @@ -28,4 +37,13 @@ public class AccessLimits extends PanacheEntity { * max requests per hour */ public int weatherRequestsHourly; + + /** + * find {@link AccessLimits} by label + * @param label label + * @return {@link AccessLimits} or null if not found + */ + public static AccessLimits findByLabel(String label) { + return AccessLimits.find("label", label).firstResult(); + } } diff --git a/src/main/java/eu/m724/orm/Account.java b/src/main/java/eu/m724/orm/Account.java deleted file mode 100644 index 37dee94..0000000 --- a/src/main/java/eu/m724/orm/Account.java +++ /dev/null @@ -1,25 +0,0 @@ -package eu.m724.orm; - -import io.quarkus.hibernate.orm.panache.PanacheEntity; -import io.quarkus.security.jpa.Roles; -import jakarta.persistence.CascadeType; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.OneToMany; - -import java.util.HashSet; -import java.util.Set; - -// TODO organize all this like work on variable names move functions etc - -@Entity -public class Account extends PanacheEntity { - @Column(unique = true) - public byte[] masterKey; - - @OneToMany(mappedBy = "account", cascade = CascadeType.ALL, orphanRemoval = true) - public Set accessKeys = new HashSet<>(); - - @Roles - public String role = "user"; -} diff --git a/src/main/java/eu/m724/orm/Token.java b/src/main/java/eu/m724/orm/Token.java new file mode 100644 index 0000000..33918d8 --- /dev/null +++ b/src/main/java/eu/m724/orm/Token.java @@ -0,0 +1,68 @@ +package eu.m724.orm; + +import io.quarkus.hibernate.orm.panache.PanacheEntity; +import io.quarkus.security.jpa.Roles; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.ManyToOne; +import jakarta.transaction.Transactional; + +import java.security.SecureRandom; +import java.util.Base64; + +// TODO organize all this like work on variable names move functions etc + +@Entity +public class Token extends PanacheEntity { + @Column(unique = true) + public byte[] tokenBytes; + + /** + * access limits of this key + */ + @ManyToOne + public AccessLimits accessLimits; + + @Roles + public String role = "user"; + + // TODO maybe move some stuff + + /** + * creates a random token + * @return base64 encoded key + */ + @Transactional + public static String generate(AccessLimits accessLimits) { + byte[] tokenBytes = new byte[18]; // 144 bits of entropy + new SecureRandom().nextBytes(tokenBytes); + + Token token = new Token(); + token.tokenBytes = tokenBytes; + token.accessLimits = accessLimits; + token.persist(); + + return Base64.getEncoder().encodeToString(tokenBytes); + } + + /** + * find an {@link Token} by master key + * @param tokenBytes token in bytes + * @return an {@link Token} or null if not found + */ + @Transactional + public static Token findByToken(byte[] tokenBytes) { + return Token.find("tokenBytes", (Object) tokenBytes).firstResult(); + } + + /** + * find an {@link Token} by master key + * @param tokenEncoded base64 encoded token + * @return an {@link Token} or null if not found + * @throws IllegalArgumentException if base64 is invalid + */ + @Transactional + public static Token findByToken(String tokenEncoded) { + return findByToken(Base64.getDecoder().decode(tokenEncoded)); + } +} diff --git a/src/main/java/eu/m724/websocket/WebsocketService.java b/src/main/java/eu/m724/websocket/WebsocketService.java index 76db035..e06d919 100644 --- a/src/main/java/eu/m724/websocket/WebsocketService.java +++ b/src/main/java/eu/m724/websocket/WebsocketService.java @@ -1,11 +1,9 @@ package eu.m724.websocket; -import eu.m724.auth.master.AccountService; -import eu.m724.orm.AccessKey; +import eu.m724.orm.Token; import eu.m724.websocket.packet.DisconnectReason; import eu.m724.websocket.packet.clientbound.PongPacket; import jakarta.enterprise.context.ApplicationScoped; -import jakarta.inject.Inject; import jakarta.websocket.Session; import org.eclipse.microprofile.config.inject.ConfigProperty; @@ -18,39 +16,42 @@ public class WebsocketService { @ConfigProperty(name = "rwws.protocol_version") byte protocolVersion; - @Inject - AccountService accountService; + private final Map tokens = new ConcurrentHashMap<>(); - private final Map accounts = new ConcurrentHashMap<>(); + // void addSession(String sessionId) { - accounts.put(sessionId, null); + tokens.put(sessionId, null); } void removeConnection(String sessionId) { - accounts.remove(sessionId); + tokens.remove(sessionId); } + // + + boolean authenticate(String sessionId, byte[] tokenBytes) { + Token token = Token.findByToken(tokenBytes); + + if (token == null) + return false; + + tokens.put(sessionId, token); + return true; + } + + boolean isAuthenticated(String sessionId) { + return tokens.containsKey(sessionId); + } + + // + void disconnect(Session session, DisconnectReason reason, String message) { try { session.close(reason.asCloseReason(message)); } catch (IOException ignored) { } } - boolean authenticate(String sessionId, byte[] bytes) { - AccessKey accessKey = accountService.findByAccessKey(bytes); - - if (accessKey == null) - return false; - - accounts.put(sessionId, accessKey); - return true; - } - - boolean isAuthenticated(String sessionId) { - return accounts.containsKey(sessionId); - } - void pong(Session session) { new PongPacket().send(session); }