diff --git a/src/main/java/eu/m724/AdminResource.java b/src/main/java/eu/m724/AdminResource.java deleted file mode 100644 index cf55cf9..0000000 --- a/src/main/java/eu/m724/AdminResource.java +++ /dev/null @@ -1,19 +0,0 @@ -package eu.m724; - -import jakarta.annotation.security.RolesAllowed; -import jakarta.ws.rs.GET; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.Produces; -import jakarta.ws.rs.core.MediaType; -import org.jboss.resteasy.reactive.NoCache; - -@Path("/api/admin") -public class AdminResource { - - @GET - @RolesAllowed("admin") - @Produces(MediaType.TEXT_PLAIN) - public String admin() { - return "You're admin"; - } -} diff --git a/src/main/java/eu/m724/GlobalAccessLimits.java b/src/main/java/eu/m724/GlobalAccessLimits.java new file mode 100644 index 0000000..750da8c --- /dev/null +++ b/src/main/java/eu/m724/GlobalAccessLimits.java @@ -0,0 +1,46 @@ +package eu.m724; + +import eu.m724.orm.AccessLimits; + +/** + * some default access limits + * there are 4: (the recommends are of 2-5 minute intervals) + * kilo: 60 rph, good for 2-5 players + * mega: 120 rph, good for 4-10 players + * giga: 240 rph, good for 8-20 players + * tera: 480 rph, good for 16-40 players + * every plan has lightning streaming + * if you were to sell access you can "tweak" some of this and rip off people + */ +public class GlobalAccessLimits { + 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; + + kilo.persist(); + mega.persist(); + giga.persist(); + tera.persist(); + } +} diff --git a/src/main/java/eu/m724/HelloService.java b/src/main/java/eu/m724/HelloService.java deleted file mode 100644 index 1498d86..0000000 --- a/src/main/java/eu/m724/HelloService.java +++ /dev/null @@ -1,14 +0,0 @@ -package eu.m724; - -import jakarta.enterprise.context.ApplicationScoped; -import org.eclipse.microprofile.config.inject.ConfigProperty; - -@ApplicationScoped -public class HelloService { - @ConfigProperty(name = "greeting") - private String greeting; - - public String hello(String name) { - return greeting + " " + name; - } -} diff --git a/src/main/java/eu/m724/PublicResource.java b/src/main/java/eu/m724/PublicResource.java deleted file mode 100644 index 3839e35..0000000 --- a/src/main/java/eu/m724/PublicResource.java +++ /dev/null @@ -1,29 +0,0 @@ -package eu.m724; - -import jakarta.annotation.security.PermitAll; -import jakarta.inject.Inject; -import jakarta.ws.rs.GET; -import jakarta.ws.rs.Path; -import jakarta.ws.rs.Produces; -import jakarta.ws.rs.core.MediaType; - -@Path("/api/public") -public class PublicResource { - @Inject - HelloService helloService; - - @GET - @PermitAll - @Produces(MediaType.TEXT_PLAIN) - public String hello() { - return "Hello from Quarkus REST"; - } - - @GET - @PermitAll - @Produces(MediaType.TEXT_PLAIN) - @Path("{name}") - public String namedHello(String name) { - return helloService.hello(name); - } -} diff --git a/src/main/java/eu/m724/Startup.java b/src/main/java/eu/m724/Startup.java index 8eb1cbe..1023955 100644 --- a/src/main/java/eu/m724/Startup.java +++ b/src/main/java/eu/m724/Startup.java @@ -1,17 +1,26 @@ package eu.m724; -import eu.m724.orm.MasterKey; +import eu.m724.auth.master.AccountService; +import eu.m724.orm.Account; import io.quarkus.runtime.StartupEvent; import jakarta.enterprise.event.Observes; +import jakarta.inject.Inject; import jakarta.inject.Singleton; import jakarta.transaction.Transactional; +import java.util.Base64; + @Singleton public class Startup { + @Inject + AccountService accountService; + @Transactional public void loadUsers(@Observes StartupEvent event) { - MasterKey.deleteAll(); - MasterKey.add("admin", "admin1", "admin"); - MasterKey.add("user", "2user", "user"); + Account.deleteAll(); + byte[] adminKey = new byte[18]; + + UserManager.add(adminKey, "admin"); + System.out.println("Admin user created: " + Base64.getEncoder().encodeToString(adminKey)); } } diff --git a/src/main/java/eu/m724/UserManager.java b/src/main/java/eu/m724/UserManager.java new file mode 100644 index 0000000..52b89ab --- /dev/null +++ b/src/main/java/eu/m724/UserManager.java @@ -0,0 +1,65 @@ +package eu.m724; + +import eu.m724.orm.AccessKey; +import eu.m724.orm.AccessLimits; +import eu.m724.orm.Account; +import jakarta.transaction.Transactional; + +import java.security.SecureRandom; +import java.util.Base64; + +// TODO figure out all this maybe move to account service +public class UserManager { + private static final SecureRandom random = new SecureRandom(); + + /** + * creates an account with the specified key + * @param masterKey the desired master key + */ + @Transactional + public static void add(byte[] masterKey, String role) { + Account account = new Account(); + account.masterKey = masterKey; + account.role = role; + account.persist(); + } + + /** + * creates an account with random key + * the account's role is "user" + * @return base64 encoded key + */ + public static String create() { + return create("user"); + } + + /** + * creates an account with random key + * @param role new account's role + * @return base64 encoded key + */ + public static String create(String role) { + byte[] key = new byte[18]; // 144 bits of entropy + random.nextBytes(key); + + add(key, role); + return Base64.getEncoder().encodeToString(key); + } + + /** + * generates an access key for this account + * @return base64 encoded access key + */ + public static String createMaster(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; + accessKey.persist(); + + return Base64.getEncoder().encodeToString(key); + } +} diff --git a/src/main/java/eu/m724/UsersResource.java b/src/main/java/eu/m724/UsersResource.java index c549d58..dea59bf 100644 --- a/src/main/java/eu/m724/UsersResource.java +++ b/src/main/java/eu/m724/UsersResource.java @@ -1,20 +1,46 @@ package eu.m724; +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.core.Context; -import jakarta.ws.rs.core.SecurityContext; -import org.jboss.resteasy.reactive.NoCache; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; @Path("/api/users") +@Produces(MediaType.APPLICATION_JSON) public class UsersResource { + @Inject + SecurityIdentity securityIdentity; + + @GET + @Path("/create") + @RolesAllowed("admin") + public JsonObject createAccount() { + String masterKey = UserManager.create(); + + return Json.createObjectBuilder() + .add("masterKey", masterKey) + .build(); + } + @GET @Path("/me") - @RolesAllowed("user") - public String me(@Context SecurityContext securityContext) { - return securityContext.getUserPrincipal().getName(); + @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", user.accessKeys.size()) + .build(); } } diff --git a/src/main/java/eu/m724/auth/master/AccountService.java b/src/main/java/eu/m724/auth/master/AccountService.java new file mode 100644 index 0000000..d30fee2 --- /dev/null +++ b/src/main/java/eu/m724/auth/master/AccountService.java @@ -0,0 +1,26 @@ +package eu.m724.auth.master; + +import eu.m724.orm.Account; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.transaction.Transactional; + +import java.util.Base64; + +@ApplicationScoped +public class AccountService { + /** + * 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", Base64.getDecoder().decode(key)).firstResult(); + } catch (IllegalArgumentException e) { + return null; + } + } +} diff --git a/src/main/java/eu/m724/auth/master/MyHttpAuthenticationMechanism.java b/src/main/java/eu/m724/auth/master/MyHttpAuthenticationMechanism.java new file mode 100644 index 0000000..398782d --- /dev/null +++ b/src/main/java/eu/m724/auth/master/MyHttpAuthenticationMechanism.java @@ -0,0 +1,46 @@ +package eu.m724.auth.master; + +import eu.m724.orm.Account; +import io.quarkus.security.identity.IdentityProviderManager; +import io.quarkus.security.identity.SecurityIdentity; +import io.quarkus.security.runtime.QuarkusPrincipal; +import io.quarkus.security.runtime.QuarkusSecurityIdentity; +import io.quarkus.vertx.http.runtime.security.ChallengeData; +import io.quarkus.vertx.http.runtime.security.HttpAuthenticationMechanism; +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"); + + return Uni.createFrom().item(() -> { + Account account = accountService.findByKey(encodedKey); + + if (account != null) { + QuarkusSecurityIdentity identity = QuarkusSecurityIdentity.builder() + .setPrincipal(new QuarkusPrincipal(encodedKey)) + .addRole(account.role) + .addAttribute("account", account) + .build(); + return (SecurityIdentity) identity; + } + return null; + }).runSubscriptionOn(Infrastructure.getDefaultWorkerPool()); + + } + + @Override + public Uni getChallenge(RoutingContext context) { + return Uni.createFrom().item(new ChallengeData(401, "WWW-Authenticate", "X-Master-Key")); + } +} diff --git a/src/main/java/eu/m724/orm/AccessKey.java b/src/main/java/eu/m724/orm/AccessKey.java index ff03300..5ac722c 100644 --- a/src/main/java/eu/m724/orm/AccessKey.java +++ b/src/main/java/eu/m724/orm/AccessKey.java @@ -1,7 +1,25 @@ package eu.m724.orm; -import jakarta.persistence.Entity; +import io.quarkus.hibernate.orm.panache.PanacheEntity; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToOne; -@Entity -public class AccessKey { +//@Entity +public class AccessKey extends PanacheEntity { + /** + * the user owning this access key + */ + @ManyToOne + public Account account; + + /** + * raw bytes of this key, it's provided to users in base64 + */ + public byte[] key; + + /** + * access limits of this key + */ + @OneToOne + public AccessLimits accessLimits; } diff --git a/src/main/java/eu/m724/orm/AccessLimits.java b/src/main/java/eu/m724/orm/AccessLimits.java new file mode 100644 index 0000000..fb22677 --- /dev/null +++ b/src/main/java/eu/m724/orm/AccessLimits.java @@ -0,0 +1,34 @@ +package eu.m724.orm; + +import io.quarkus.hibernate.orm.panache.PanacheEntity; +import jakarta.persistence.Column; +import jakarta.persistence.OneToOne; + +//@Entity +public class AccessLimits extends PanacheEntity { + @OneToOne + public AccessKey accessKey; + + /** + * label of these limits, displayed to user and used to identify the limits + */ + @Column(unique = true) + public String label; + + /** + * can this access key access live lightning data + * there's no fine controls here because it just streams everything + */ + public boolean thunder; + + /** + * can this access key request weather data + * there are fine controls for this + */ + public boolean weather; + + /** + * max requests per hour + */ + public int weatherRequestsHourly; +} diff --git a/src/main/java/eu/m724/orm/Account.java b/src/main/java/eu/m724/orm/Account.java new file mode 100644 index 0000000..be71f2d --- /dev/null +++ b/src/main/java/eu/m724/orm/Account.java @@ -0,0 +1,20 @@ +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; + +// 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 + //public List accessKeys = new ArrayList<>(); + + @Roles + public String role = "user"; +} diff --git a/src/main/java/eu/m724/orm/LimitsTier.java b/src/main/java/eu/m724/orm/LimitsTier.java deleted file mode 100644 index f6a085b..0000000 --- a/src/main/java/eu/m724/orm/LimitsTier.java +++ /dev/null @@ -1,18 +0,0 @@ -package eu.m724.orm; - -import io.quarkus.hibernate.orm.panache.PanacheEntity; -import io.quarkus.security.jpa.RolesValue; -import jakarta.persistence.Entity; -import jakarta.persistence.OneToMany; - -import java.util.List; - -@Entity -public class LimitsTier extends PanacheEntity { - @OneToMany(mappedBy = "tier") - public List masterKeys; - - - @RolesValue - public String role; -} diff --git a/src/main/java/eu/m724/orm/MasterKey.java b/src/main/java/eu/m724/orm/MasterKey.java deleted file mode 100644 index 5eff790..0000000 --- a/src/main/java/eu/m724/orm/MasterKey.java +++ /dev/null @@ -1,36 +0,0 @@ -package eu.m724.orm; - -import io.quarkus.elytron.security.common.BcryptUtil; -import io.quarkus.hibernate.orm.panache.PanacheEntity; -import io.quarkus.security.jpa.Password; -import io.quarkus.security.jpa.Roles; -import io.quarkus.security.jpa.UserDefinition; -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.Id; -import jakarta.persistence.OneToMany; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; - -@Entity -public class MasterKey extends PanacheEntity { - @Id - @GeneratedValue - public long id; - - public String hashedKey; - - @OneToMany - public List accessKeys = new ArrayList<>(); - - @Roles - public String role = "user"; - - public static void add(String key) { - MasterKey user = new MasterKey(); - user.hashedKey = BcryptUtil.bcryptHash(key); - user.persist(); - } -} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index e65d5e5..24fc8b2 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,7 +1,3 @@ -quarkus.http.auth.basic=true - quarkus.datasource.db.kind=h2 -quarkus.hibernate-orm.database.generation=drop-and-create - -greeting=Welcome \ No newline at end of file +quarkus.hibernate-orm.database.generation=drop-and-create \ No newline at end of file