diff --git a/pom.xml b/pom.xml
index fcffb29..93eb0bf 100644
--- a/pom.xml
+++ b/pom.xml
@@ -57,7 +57,6 @@
io.quarkus
quarkus-elytron-security
- 3.26.2
io.quarkus
@@ -79,6 +78,18 @@
org.flywaydb
flyway-database-postgresql
+
+ org.apache.commons
+ commons-lang3
+ 3.20.0
+ compile
+
+
+ com.fasterxml.jackson.dataformat
+ jackson-dataformat-yaml
+ 2.21.1
+ compile
+
io.quarkus
quarkus-junit5
diff --git a/postgres.yaml b/postgres.yaml
new file mode 100644
index 0000000..5cc26f2
--- /dev/null
+++ b/postgres.yaml
@@ -0,0 +1,11 @@
+version: "3.9"
+services:
+ postgres:
+ image: postgres:16
+ container_name: iam-dev-postgres
+ environment:
+ POSTGRES_DB: postgres
+ POSTGRES_USER: postgres
+ POSTGRES_PASSWORD: postgres
+ ports:
+ - "5432:5432"
\ No newline at end of file
diff --git a/src/main/java/de/tavolio/AuthenticationService.java b/src/main/java/de/tavolio/AuthenticationService.java
index 57a893d..913e80b 100644
--- a/src/main/java/de/tavolio/AuthenticationService.java
+++ b/src/main/java/de/tavolio/AuthenticationService.java
@@ -1,93 +1,36 @@
package de.tavolio;
-import de.tavolio.account.AccountEntity;
-import de.tavolio.account.AccountRepo;
+import de.tavolio.realm.user.UserEntity;
+import de.tavolio.realm.user.UserRepo;
import io.quarkus.security.UnauthorizedException;
-import io.vertx.core.http.HttpServerRequest;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.ws.rs.NotFoundException;
-import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.SecurityContext;
-import org.eclipse.microprofile.config.inject.ConfigProperty;
-import org.jboss.logging.Logger;
import java.security.Principal;
-import java.util.Base64;
@ApplicationScoped
public class AuthenticationService
{
@Inject
- Logger LOG;
-
- @Inject
- AccountRepo accountRepo;
+ UserRepo userRepo;
@Inject
SecurityContext securityContext;
- @Inject
- HttpServerRequest request;
-
- @ConfigProperty(name = "iam.user.name")
- String superuserName;
-
- @ConfigProperty(name = "iam.user.password")
- String superuserPassword;
-
- public boolean isSuperUser()
- {
- String authHeader = request.getHeader(HttpHeaders.AUTHORIZATION);
- if (authHeader != null && !authHeader.isBlank())
- {
- String[] sections = authHeader.split("\\s+");
- if (sections.length == 2 && sections[0].equals("Basic"))
- {
- String value = new String(Base64.getDecoder().decode(sections[1]));
- String[] parts = value.split(":");
- if (parts.length == 2)
- {
- String username = parts[0];
- String password = parts[1];
- boolean isSuperuser = username.equals(superuserName) && password.equals(superuserPassword);
- if (isSuperuser)
- {
- return true;
- }
- {
- LOG.errorf("Invalid username or password", getRequestPath());
- return false;
- }
- }
- else
- {
- LOG.errorf("Invalid base64 credentials %s", getRequestPath());
- }
- }
- }
- return false;
- }
-
- public AccountEntity requireUser()
+ public UserEntity requireUser()
{
Principal principal = securityContext.getUserPrincipal();
if(principal != null)
{
- AccountEntity accountEntity = accountRepo.findById(principal.getName());
- if(accountEntity != null)
+ UserEntity userEntity = userRepo.findById(principal.getName());
+ if(userEntity != null)
{
- return accountEntity;
+ return userEntity;
}
- LOG.warnf("No account found for request %s", getRequestPath());
throw new NotFoundException();
}
- LOG.warnf("Unauthorized request %s", getRequestPath());
throw new UnauthorizedException();
}
-
- private String getRequestPath()
- {
- return String.format("[%s, %s]", request.method().name(), request.path());
- }
}
diff --git a/src/main/java/de/tavolio/Role.java b/src/main/java/de/tavolio/Role.java
new file mode 100644
index 0000000..a49f11e
--- /dev/null
+++ b/src/main/java/de/tavolio/Role.java
@@ -0,0 +1,6 @@
+package de.tavolio;
+
+public enum Role
+{
+ ROOT, CLIENT, USER
+}
diff --git a/src/main/java/de/tavolio/account/AccountEntity.java b/src/main/java/de/tavolio/account/AccountEntity.java
deleted file mode 100644
index b9b54e6..0000000
--- a/src/main/java/de/tavolio/account/AccountEntity.java
+++ /dev/null
@@ -1,113 +0,0 @@
-package de.tavolio.account;
-
-import de.tavolio.member.MembershipEntity;
-import io.quarkus.hibernate.orm.panache.PanacheEntityBase;
-import jakarta.persistence.*;
-
-import java.util.Set;
-import java.util.UUID;
-
-@Entity
-@Table(name = "account")
-public class AccountEntity extends PanacheEntityBase
-{
- @Id
- private String id;
-
- private String firstname;
-
- private String lastname;
-
- private String email;
-
- @Column(name = "account_password")
- private String password;
-
- @Enumerated(EnumType.STRING)
- private AccountStatus status;
-
- @OneToMany(mappedBy = "account")
- private Set memberships;
-
- public static AccountEntity init()
- {
- return new AccountEntity().setId(UUID.randomUUID().toString());
- }
-
- public String getId()
- {
- return id;
- }
-
- public AccountEntity setId(String id)
- {
- this.id = id;
- return this;
- }
-
- public String getFirstname()
- {
- return firstname;
- }
-
- public AccountEntity setFirstname(String firstname)
- {
- this.firstname = firstname;
- return this;
- }
-
- public String getLastname()
- {
- return lastname;
- }
-
- public AccountEntity setLastname(String lastname)
- {
- this.lastname = lastname;
- return this;
- }
-
- public String getEmail()
- {
- return email;
- }
-
- public AccountEntity setEmail(String email)
- {
- this.email = email;
- return this;
- }
-
- public String getPassword()
- {
- return password;
- }
-
- public AccountEntity setPassword(String password)
- {
- this.password = password;
- return this;
- }
-
- public Set getMemberships()
- {
- return memberships;
- }
-
- public AccountEntity setMemberships(Set memberships)
- {
- this.memberships = memberships;
- return this;
- }
-
- public AccountStatus getStatus()
- {
- return status;
- }
-
- public AccountEntity setStatus(AccountStatus status)
- {
- this.status = status;
- return this;
- }
-}
diff --git a/src/main/java/de/tavolio/account/AccountMapper.java b/src/main/java/de/tavolio/account/AccountMapper.java
deleted file mode 100644
index bf8c58c..0000000
--- a/src/main/java/de/tavolio/account/AccountMapper.java
+++ /dev/null
@@ -1,13 +0,0 @@
-package de.tavolio.account;
-
-import de.tavolio.account.dto.Account;
-import jakarta.enterprise.context.ApplicationScoped;
-
-@ApplicationScoped
-public class AccountMapper
-{
- public Account map(AccountEntity accountEntity)
- {
- return new Account(accountEntity.getId(), accountEntity.getFirstname(), accountEntity.getLastname(), accountEntity.getEmail(), accountEntity.getStatus());
- }
-}
diff --git a/src/main/java/de/tavolio/account/AccountRepo.java b/src/main/java/de/tavolio/account/AccountRepo.java
deleted file mode 100644
index 3580a12..0000000
--- a/src/main/java/de/tavolio/account/AccountRepo.java
+++ /dev/null
@@ -1,16 +0,0 @@
-package de.tavolio.account;
-
-import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase;
-import io.quarkus.panache.common.Parameters;
-import jakarta.enterprise.context.ApplicationScoped;
-
-import java.util.Optional;
-
-@ApplicationScoped
-public class AccountRepo implements PanacheRepositoryBase
-{
- public Optional findOptionalByEmail(String email)
- {
- return find("email = :email", Parameters.with("email", email)).firstResultOptional();
- }
-}
diff --git a/src/main/java/de/tavolio/account/AccountResource.java b/src/main/java/de/tavolio/account/AccountResource.java
deleted file mode 100644
index 3cb9b27..0000000
--- a/src/main/java/de/tavolio/account/AccountResource.java
+++ /dev/null
@@ -1,36 +0,0 @@
-package de.tavolio.account;
-
-import de.tavolio.account.dto.Account;
-import de.tavolio.account.dto.AccountCreation;
-import jakarta.inject.Inject;
-import jakarta.validation.Valid;
-import jakarta.ws.rs.GET;
-import jakarta.ws.rs.POST;
-import jakarta.ws.rs.Path;
-import jakarta.ws.rs.PathParam;
-import org.jboss.logging.Logger;
-
-@Path("/accounts")
-public class AccountResource
-{
- @Inject
- Logger LOG;
-
- @Inject
- AccountService accountService;
-
- @POST
- public Account post(@Valid AccountCreation account)
- {
- Account createdAccount = accountService.create(account);
- LOG.infof("Created account successfully: %s", account.email());
- return createdAccount;
- }
-
- @GET
- @Path("/{id}")
- public Account get(@PathParam("id") String id)
- {
- return accountService.getUser(id);
- }
-}
diff --git a/src/main/java/de/tavolio/account/AccountService.java b/src/main/java/de/tavolio/account/AccountService.java
deleted file mode 100644
index 61577d5..0000000
--- a/src/main/java/de/tavolio/account/AccountService.java
+++ /dev/null
@@ -1,52 +0,0 @@
-package de.tavolio.account;
-
-import de.tavolio.AuthenticationService;
-import de.tavolio.account.dto.Account;
-import de.tavolio.account.dto.AccountCreation;
-import io.quarkus.elytron.security.common.BcryptUtil;
-import io.quarkus.security.UnauthorizedException;
-import jakarta.enterprise.context.ApplicationScoped;
-import jakarta.inject.Inject;
-import jakarta.transaction.Transactional;
-import org.jboss.logging.Logger;
-
-@ApplicationScoped
-public class AccountService
-{
- @Inject
- Logger LOG;
-
- @Inject
- AccountRepo accountRepo;
-
- @Inject
- AccountMapper accountMapper;
-
- @Inject
- AuthenticationService authenticationService;
-
- @Transactional
- public Account create(AccountCreation account)
- {
- AccountEntity accountEntity = AccountEntity.init();
- accountEntity.setEmail(account.email())
- .setFirstname(account.firstname())
- .setLastname(account.lastname())
- .setPassword(BcryptUtil.bcryptHash(account.password()))
- .setStatus(AccountStatus.INIT);
- accountRepo.persist(accountEntity);
- return accountMapper.map(accountEntity);
- }
-
- public Account getUser(String id)
- {
- AccountEntity account = authenticationService.requireUser();
- AccountEntity requestedAccount = accountRepo.findById(id);
- if (requestedAccount != null && requestedAccount.getId().equals(account.getId()))
- {
- return accountMapper.map(authenticationService.requireUser());
- }
- LOG.errorf("Cannot access account");
- throw new UnauthorizedException();
- }
-}
diff --git a/src/main/java/de/tavolio/account/AccountStatus.java b/src/main/java/de/tavolio/account/AccountStatus.java
deleted file mode 100644
index ec92f05..0000000
--- a/src/main/java/de/tavolio/account/AccountStatus.java
+++ /dev/null
@@ -1,6 +0,0 @@
-package de.tavolio.account;
-
-public enum AccountStatus
-{
- INIT, REGISTERED
-}
diff --git a/src/main/java/de/tavolio/account/dto/Account.java b/src/main/java/de/tavolio/account/dto/Account.java
deleted file mode 100644
index 1398fb2..0000000
--- a/src/main/java/de/tavolio/account/dto/Account.java
+++ /dev/null
@@ -1,7 +0,0 @@
-package de.tavolio.account.dto;
-
-import de.tavolio.account.AccountStatus;
-
-public record Account(String id, String firstname, String lastname, String email, AccountStatus status)
-{
-}
diff --git a/src/main/java/de/tavolio/bootstrap/AccountBootstrapper.java b/src/main/java/de/tavolio/bootstrap/AccountBootstrapper.java
new file mode 100644
index 0000000..c0c3e4b
--- /dev/null
+++ b/src/main/java/de/tavolio/bootstrap/AccountBootstrapper.java
@@ -0,0 +1,59 @@
+package de.tavolio.bootstrap;
+
+import de.tavolio.realm.user.UserEntity;
+import de.tavolio.realm.user.UserRepo;
+import de.tavolio.realm.user.UserStatus;
+import de.tavolio.bootstrap.model.Account;
+import de.tavolio.realm.RealmEntity;
+import io.quarkus.elytron.security.common.BcryptUtil;
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.inject.Inject;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+
+@ApplicationScoped
+public class AccountBootstrapper
+{
+ @Inject
+ UserRepo userRepo;
+
+ public void bootstrap(RealmEntity realm, List accounts)
+ {
+ for (Account account : accounts)
+ {
+ run(realm, account);
+ }
+ }
+
+ public void run(RealmEntity realm, Account account)
+ {
+ Optional existingAccount = userRepo.findOptionalByRealmAndEmail(realm, account.email());
+ if (existingAccount.isEmpty())
+ {
+ UserEntity newAccount = new UserEntity();
+ newAccount.setId(UUID.randomUUID().toString());
+ newAccount.setEmail(account.email());
+ newAccount.setFirstname(account.firstname());
+ newAccount.setLastname(account.lastname());
+ newAccount.setRealm(realm);
+ newAccount.setStatus(UserStatus.INIT);
+ newAccount.setPassword(resolvePassword(account));
+ userRepo.persist(newAccount);
+ }
+ }
+
+ private String resolvePassword(Account account)
+ {
+ if (account.passwordFromEnv() != null)
+ {
+ return BcryptUtil.bcryptHash(System.getenv(account.passwordFromEnv()));
+ }
+ if (account.passwordPlain() != null)
+ {
+ return BcryptUtil.bcryptHash(account.passwordPlain());
+ }
+ return null;
+ }
+}
diff --git a/src/main/java/de/tavolio/bootstrap/BootstrapService.java b/src/main/java/de/tavolio/bootstrap/BootstrapService.java
new file mode 100644
index 0000000..bf68a9c
--- /dev/null
+++ b/src/main/java/de/tavolio/bootstrap/BootstrapService.java
@@ -0,0 +1,40 @@
+package de.tavolio.bootstrap;
+
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
+import de.tavolio.bootstrap.model.Bootstrap;
+import de.tavolio.bootstrap.model.Realm;
+import io.quarkus.runtime.Startup;
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.inject.Inject;
+import jakarta.transaction.Transactional;
+
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.Map;
+
+@ApplicationScoped
+public class BootstrapService
+{
+ private static final Path PATH = Path.of("/home/andreas/Documents/dev/iam-backend/src/main/resources/bootstrap.yaml");
+ private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(new YAMLFactory()).configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
+
+ @Inject
+ RealmBootstrapService realmBootstrapService;
+
+ @Inject
+ SuperuserBootstrapper superuserBootstrapper;
+
+ @Startup
+ @Transactional
+ void bootstrap() throws IOException
+ {
+ superuserBootstrapper.bootstrap();
+ Bootstrap bootstrap = OBJECT_MAPPER.readValue(PATH.toFile(), Bootstrap.class);
+ for (Map.Entry realmEntry : bootstrap.realms().entrySet())
+ {
+ realmBootstrapService.bootstrap(realmEntry);
+ }
+ }
+}
diff --git a/src/main/java/de/tavolio/bootstrap/ClientBootstrapper.java b/src/main/java/de/tavolio/bootstrap/ClientBootstrapper.java
new file mode 100644
index 0000000..4ddc35a
--- /dev/null
+++ b/src/main/java/de/tavolio/bootstrap/ClientBootstrapper.java
@@ -0,0 +1,30 @@
+package de.tavolio.bootstrap;
+
+import de.tavolio.bootstrap.model.Client;
+import de.tavolio.realm.client.ClientEntity;
+import de.tavolio.realm.client.ClientRepo;
+import de.tavolio.realm.client.ClientService;
+import de.tavolio.realm.RealmEntity;
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.inject.Inject;
+
+import java.util.Map;
+
+@ApplicationScoped
+public class ClientBootstrapper
+{
+ @Inject
+ ClientService clientService;
+
+ @Inject
+ ClientRepo clientRepo;
+
+ public void bootstrap(RealmEntity realm, Map clients)
+ {
+ for (Map.Entry clientEntry : clients.entrySet())
+ {
+ ClientEntity client = clientService.findOrCreate(realm, clientEntry);
+ clientRepo.persist(client);
+ }
+ }
+}
diff --git a/src/main/java/de/tavolio/bootstrap/KeyBootstrapper.java b/src/main/java/de/tavolio/bootstrap/KeyBootstrapper.java
new file mode 100644
index 0000000..69d9a69
--- /dev/null
+++ b/src/main/java/de/tavolio/bootstrap/KeyBootstrapper.java
@@ -0,0 +1,22 @@
+package de.tavolio.bootstrap;
+
+import de.tavolio.bootstrap.model.Key;
+import de.tavolio.realm.RealmEntity;
+import de.tavolio.realm.key.KeypairService;
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.inject.Inject;
+
+@ApplicationScoped
+public class KeyBootstrapper
+{
+ @Inject
+ KeypairService keypairService;
+
+ void bootstrap(RealmEntity realm, Key key)
+ {
+ if (realm.getKeys().isEmpty())
+ {
+ keypairService.create(realm, key.type(), key.alg());
+ }
+ }
+}
diff --git a/src/main/java/de/tavolio/bootstrap/RealmBootstrapService.java b/src/main/java/de/tavolio/bootstrap/RealmBootstrapService.java
new file mode 100644
index 0000000..ab63e20
--- /dev/null
+++ b/src/main/java/de/tavolio/bootstrap/RealmBootstrapService.java
@@ -0,0 +1,87 @@
+package de.tavolio.bootstrap;
+
+import de.tavolio.bootstrap.model.Audience;
+import de.tavolio.bootstrap.model.Realm;
+import de.tavolio.realm.RealmEntity;
+import de.tavolio.realm.RealmRepo;
+import de.tavolio.realm.RealmService;
+import de.tavolio.realm.audience.AudienceStrategyEntity;
+import de.tavolio.realm.audience.AudienceStrategyRepo;
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.inject.Inject;
+
+import java.util.Map;
+import java.util.Optional;
+import java.util.UUID;
+
+@ApplicationScoped
+public class RealmBootstrapService
+{
+ @Inject
+ RealmService realmService;
+
+ @Inject
+ KeyBootstrapper keyBootstrapper;
+
+ @Inject
+ ClientBootstrapper clientBootstrapper;
+
+ @Inject
+ RoleBootstrapper roleBootstrapper;
+
+ @Inject
+ RealmRepo realmRepo;
+
+ @Inject
+ AccountBootstrapper accountBootstrapper;
+
+ @Inject
+ AudienceStrategyRepo audienceStrategyRepo;
+
+ public void bootstrap(Map.Entry realmEntry)
+ {
+ Realm realmValue = realmEntry.getValue();
+ RealmEntity realm = run(realmEntry.getKey(), realmEntry.getValue());
+ roleBootstrapper.bootstrap(realm, realmValue.roles());
+ keyBootstrapper.bootstrap(realm, realmValue.key());
+ clientBootstrapper.bootstrap(realm, realmValue.clients());
+ accountBootstrapper.bootstrap(realm, realmValue.accounts());
+ }
+
+ public RealmEntity run(String key, Realm realm)
+ {
+ Optional existingRealm = realmRepo.findByIdOptional(key);
+ if (existingRealm.isPresent())
+ {
+ return existingRealm.get();
+ }
+ else
+ {
+ RealmEntity newRealm = new RealmEntity().setKey(key).setName(realm.name());
+ realmRepo.persist(newRealm);
+ bootstrapAudience(newRealm, realm.audience());
+ return newRealm;
+ }
+ }
+
+ private void bootstrapAudience(RealmEntity realm, Audience audience)
+ {
+ if ("static".equals(audience.strategy()))
+ {
+ AudienceStrategyEntity entity = new AudienceStrategyEntity();
+ entity.setId(UUID.randomUUID().toString());
+ entity.setStrategy("static");
+ entity.setValue(audience.value());
+ entity.setRealm(realm);
+ audienceStrategyRepo.persist(entity);
+ }
+ else
+ {
+ AudienceStrategyEntity entity = new AudienceStrategyEntity();
+ entity.setId(UUID.randomUUID().toString());
+ entity.setStrategy("realm");
+ entity.setRealm(realm);
+ audienceStrategyRepo.persist(entity);
+ }
+ }
+}
diff --git a/src/main/java/de/tavolio/bootstrap/RoleBootstrapper.java b/src/main/java/de/tavolio/bootstrap/RoleBootstrapper.java
new file mode 100644
index 0000000..d58f31d
--- /dev/null
+++ b/src/main/java/de/tavolio/bootstrap/RoleBootstrapper.java
@@ -0,0 +1,43 @@
+package de.tavolio.bootstrap;
+
+import de.tavolio.bootstrap.model.Role;
+import de.tavolio.realm.RealmEntity;
+import de.tavolio.realm.role.RoleEntity;
+import de.tavolio.realm.role.RoleRepo;
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.inject.Inject;
+import java.util.Map;
+import java.util.UUID;
+
+@ApplicationScoped
+public class RoleBootstrapper
+{
+ @Inject
+ RoleRepo roleRepo;
+
+ public void bootstrap(RealmEntity realm, Map roles)
+ {
+ for (Map.Entry roleEntry : roles.entrySet())
+ {
+ run(realm, roleEntry);
+ }
+ }
+
+ public void run(RealmEntity realm, Map.Entry roleEntry)
+ {
+ RoleEntity role = getOrCreateRole(realm, roleEntry.getKey());
+ for (String permission : roleEntry.getValue().permissions())
+ {
+ if (!role.hasPermission(permission))
+ {
+ role.getPermissions().add(permission);
+ }
+ }
+ roleRepo.persist(role);
+ }
+
+ public RoleEntity getOrCreateRole(RealmEntity realm, String role)
+ {
+ return roleRepo.findByNameAndRealmOptional(role, realm).orElse(new RoleEntity().setId(UUID.randomUUID().toString()).setName(role).setRealm(realm));
+ }
+}
diff --git a/src/main/java/de/tavolio/bootstrap/SuperuserBootstrapper.java b/src/main/java/de/tavolio/bootstrap/SuperuserBootstrapper.java
new file mode 100644
index 0000000..97d62f7
--- /dev/null
+++ b/src/main/java/de/tavolio/bootstrap/SuperuserBootstrapper.java
@@ -0,0 +1,33 @@
+package de.tavolio.bootstrap;
+
+import de.tavolio.superuser.SuperuserEntity;
+import de.tavolio.superuser.SuperuserRepo;
+import io.quarkus.elytron.security.common.BcryptUtil;
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.inject.Inject;
+import org.apache.commons.lang3.StringUtils;
+import org.eclipse.microprofile.config.Config;
+import org.eclipse.microprofile.config.ConfigProvider;
+
+import java.util.UUID;
+
+@ApplicationScoped
+public class SuperuserBootstrapper
+{
+ @Inject
+ SuperuserRepo superuserRepo;
+
+ public void bootstrap()
+ {
+ Config config = ConfigProvider.getConfig();
+ String username = config.getValue("dev.dinauer.idp.superuser.username", String.class);
+ String password = config.getValue("dev.dinauer.idp.superuser.password", String.class);
+ if (!StringUtils.isBlank(username) && !StringUtils.isBlank(password) && superuserRepo.count() == 0)
+ {
+ SuperuserEntity superuser = new SuperuserEntity();
+ superuser.setId(username);
+ superuser.setPassword(BcryptUtil.bcryptHash(password));
+ superuserRepo.persist(superuser);
+ }
+ }
+}
diff --git a/src/main/java/de/tavolio/bootstrap/model/Account.java b/src/main/java/de/tavolio/bootstrap/model/Account.java
new file mode 100644
index 0000000..1e37fb6
--- /dev/null
+++ b/src/main/java/de/tavolio/bootstrap/model/Account.java
@@ -0,0 +1,7 @@
+package de.tavolio.bootstrap.model;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public record Account(String email, @JsonProperty("first-name") String firstname, @JsonProperty("last-name") String lastname, @JsonProperty("password-env") String passwordFromEnv, @JsonProperty("password-plain") String passwordPlain)
+{
+}
\ No newline at end of file
diff --git a/src/main/java/de/tavolio/bootstrap/model/Audience.java b/src/main/java/de/tavolio/bootstrap/model/Audience.java
new file mode 100644
index 0000000..505ab1b
--- /dev/null
+++ b/src/main/java/de/tavolio/bootstrap/model/Audience.java
@@ -0,0 +1,5 @@
+package de.tavolio.bootstrap.model;
+
+public record Audience(String strategy, String value)
+{
+}
diff --git a/src/main/java/de/tavolio/bootstrap/model/Bootstrap.java b/src/main/java/de/tavolio/bootstrap/model/Bootstrap.java
new file mode 100644
index 0000000..cdb5936
--- /dev/null
+++ b/src/main/java/de/tavolio/bootstrap/model/Bootstrap.java
@@ -0,0 +1,7 @@
+package de.tavolio.bootstrap.model;
+
+import java.util.Map;
+
+public record Bootstrap(Map realms)
+{
+}
diff --git a/src/main/java/de/tavolio/bootstrap/model/Client.java b/src/main/java/de/tavolio/bootstrap/model/Client.java
new file mode 100644
index 0000000..b45acbe
--- /dev/null
+++ b/src/main/java/de/tavolio/bootstrap/model/Client.java
@@ -0,0 +1,9 @@
+package de.tavolio.bootstrap.model;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import java.util.List;
+
+public record Client(@JsonProperty("client-secret") String clientSecret, @JsonProperty("redirect-uri") String redirectURI, List roles)
+{
+}
\ No newline at end of file
diff --git a/src/main/java/de/tavolio/bootstrap/model/Key.java b/src/main/java/de/tavolio/bootstrap/model/Key.java
new file mode 100644
index 0000000..38ea785
--- /dev/null
+++ b/src/main/java/de/tavolio/bootstrap/model/Key.java
@@ -0,0 +1,5 @@
+package de.tavolio.bootstrap.model;
+
+public record Key(String type, String alg)
+{
+}
diff --git a/src/main/java/de/tavolio/bootstrap/model/Realm.java b/src/main/java/de/tavolio/bootstrap/model/Realm.java
new file mode 100644
index 0000000..89d5141
--- /dev/null
+++ b/src/main/java/de/tavolio/bootstrap/model/Realm.java
@@ -0,0 +1,8 @@
+package de.tavolio.bootstrap.model;
+
+import java.util.List;
+import java.util.Map;
+
+public record Realm(String name, Key key, Audience audience, Map clients, Map roles, List permissions, List accounts)
+{
+}
diff --git a/src/main/java/de/tavolio/bootstrap/model/Role.java b/src/main/java/de/tavolio/bootstrap/model/Role.java
new file mode 100644
index 0000000..32bc2d6
--- /dev/null
+++ b/src/main/java/de/tavolio/bootstrap/model/Role.java
@@ -0,0 +1,7 @@
+package de.tavolio.bootstrap.model;
+
+import java.util.List;
+
+public record Role(List permissions)
+{
+}
diff --git a/src/main/java/de/tavolio/member/AccountMembershipResource.java b/src/main/java/de/tavolio/member/AccountMembershipResource.java
deleted file mode 100644
index 00d942e..0000000
--- a/src/main/java/de/tavolio/member/AccountMembershipResource.java
+++ /dev/null
@@ -1,33 +0,0 @@
-package de.tavolio.member;
-
-import de.tavolio.AuthenticationService;
-import de.tavolio.account.AccountEntity;
-import de.tavolio.member.dto.AccountMemberships;
-import jakarta.enterprise.context.ApplicationScoped;
-import jakarta.inject.Inject;
-import jakarta.ws.rs.GET;
-import jakarta.ws.rs.Path;
-
-@ApplicationScoped
-@Path("/memberships")
-public class AccountMembershipResource
-{
- @Inject
- MembershipRepo membershipRepo;
-
- @Inject
- AuthenticationService authenticationService;
-
- @Inject
- MembershipMapper membershipMapper;
-
- @GET
- public AccountMemberships get()
- {
- AccountEntity account = authenticationService.requireUser();
- return new AccountMemberships(
- membershipMapper.map(membershipRepo.findByTenantTypeAndAccount(TenantType.ORGANISATION, account)),
- membershipMapper.map(membershipRepo.findByTenantTypeAndAccount(TenantType.RESTAURANT, account))
- );
- }
-}
diff --git a/src/main/java/de/tavolio/member/MembershipEntity.java b/src/main/java/de/tavolio/member/MembershipEntity.java
deleted file mode 100644
index dc0df40..0000000
--- a/src/main/java/de/tavolio/member/MembershipEntity.java
+++ /dev/null
@@ -1,105 +0,0 @@
-package de.tavolio.member;
-
-import de.tavolio.account.AccountEntity;
-import io.quarkus.hibernate.orm.panache.PanacheEntityBase;
-import jakarta.persistence.*;
-
-import java.time.ZonedDateTime;
-import java.util.UUID;
-
-@Entity
-@Table(name = "membership")
-public class MembershipEntity extends PanacheEntityBase
-{
- @Id
- private String id;
-
- @Column(name = "tenant_type")
- @Enumerated(EnumType.STRING)
- private TenantType tenantType;
-
- @Column(name = "tenant_id")
- private String tenantId;
-
- @Column(name = "member_role")
- @Enumerated(EnumType.STRING)
- private MembershipRole role;
-
- @Column(name = "member_since")
- private ZonedDateTime memberSince;
-
- @ManyToOne
- @JoinColumn(name = "account_id")
- private AccountEntity account;
-
- public static MembershipEntity init()
- {
- return new MembershipEntity().setId(UUID.randomUUID().toString());
- }
-
- public String getId()
- {
- return id;
- }
-
- public MembershipEntity setId(String id)
- {
- this.id = id;
- return this;
- }
-
- public TenantType getTenantType()
- {
- return tenantType;
- }
-
- public MembershipEntity setTenantType(TenantType tenantType)
- {
- this.tenantType = tenantType;
- return this;
- }
-
- public String getTenantId()
- {
- return tenantId;
- }
-
- public MembershipEntity setTenantId(String tenantId)
- {
- this.tenantId = tenantId;
- return this;
- }
-
- public MembershipRole getRole()
- {
- return role;
- }
-
- public MembershipEntity setRole(MembershipRole role)
- {
- this.role = role;
- return this;
- }
-
- public ZonedDateTime getMemberSince()
- {
- return memberSince;
- }
-
- public MembershipEntity setMemberSince(ZonedDateTime memberSince)
- {
- this.memberSince = memberSince;
- return this;
- }
-
- public AccountEntity getAccount()
- {
- return account;
- }
-
- public MembershipEntity setAccount(AccountEntity account)
- {
- this.account = account;
- return this;
- }
-}
diff --git a/src/main/java/de/tavolio/member/MembershipMapper.java b/src/main/java/de/tavolio/member/MembershipMapper.java
deleted file mode 100644
index 7071e29..0000000
--- a/src/main/java/de/tavolio/member/MembershipMapper.java
+++ /dev/null
@@ -1,25 +0,0 @@
-package de.tavolio.member;
-
-import de.tavolio.account.AccountMapper;
-import de.tavolio.member.dto.Membership;
-import jakarta.enterprise.context.ApplicationScoped;
-import jakarta.inject.Inject;
-
-import java.util.List;
-
-@ApplicationScoped
-public class MembershipMapper
-{
- @Inject
- AccountMapper accountMapper;
-
- public Membership map(MembershipEntity membership)
- {
- return new Membership(membership.getId(), membership.getTenantType(), membership.getTenantId(), membership.getRole(), accountMapper.map(membership.getAccount()));
- }
-
- public List map(List memberships)
- {
- return memberships.stream().map(this::map).toList();
- }
-}
diff --git a/src/main/java/de/tavolio/member/MembershipRepo.java b/src/main/java/de/tavolio/member/MembershipRepo.java
deleted file mode 100644
index 4143778..0000000
--- a/src/main/java/de/tavolio/member/MembershipRepo.java
+++ /dev/null
@@ -1,22 +0,0 @@
-package de.tavolio.member;
-
-import de.tavolio.account.AccountEntity;
-import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase;
-import io.quarkus.panache.common.Parameters;
-import jakarta.enterprise.context.ApplicationScoped;
-
-import java.util.List;
-
-@ApplicationScoped
-public class MembershipRepo implements PanacheRepositoryBase
-{
- public List findByTenantTypeAndAccount(TenantType tenantType, AccountEntity account)
- {
- return list("tenantType = :tenantType AND account = :account", Parameters.with("tenantType", tenantType).and("account", account));
- }
-
- public List findByTenantTypeAndTenantId(TenantType tenantType, String tenantId)
- {
- return list("tenantType = :tenantType AND tenantId = :tenantId", Parameters.with("tenantType", tenantType).and("tenantId", tenantId));
- }
-}
diff --git a/src/main/java/de/tavolio/member/MembershipRole.java b/src/main/java/de/tavolio/member/MembershipRole.java
deleted file mode 100644
index c872438..0000000
--- a/src/main/java/de/tavolio/member/MembershipRole.java
+++ /dev/null
@@ -1,6 +0,0 @@
-package de.tavolio.member;
-
-public enum MembershipRole
-{
- OWNER, ADMIN, MEMBER
-}
diff --git a/src/main/java/de/tavolio/member/MembershipService.java b/src/main/java/de/tavolio/member/MembershipService.java
deleted file mode 100644
index 58fe509..0000000
--- a/src/main/java/de/tavolio/member/MembershipService.java
+++ /dev/null
@@ -1,87 +0,0 @@
-package de.tavolio.member;
-
-import de.tavolio.AuthenticationService;
-import de.tavolio.account.AccountEntity;
-import de.tavolio.account.AccountRepo;
-import de.tavolio.account.AccountStatus;
-import de.tavolio.member.dto.Membership;
-import de.tavolio.member.dto.MembershipCreation;
-import io.quarkus.security.UnauthorizedException;
-import jakarta.enterprise.context.ApplicationScoped;
-import jakarta.inject.Inject;
-import jakarta.transaction.Transactional;
-import jakarta.ws.rs.BadRequestException;
-import org.jboss.logging.Logger;
-
-import java.time.ZonedDateTime;
-import java.util.List;
-
-@ApplicationScoped
-public class MembershipService
-{
- @Inject
- Logger LOG;
-
- @Inject
- AuthenticationService authenticationService;
-
- @Inject
- MembershipRepo membershipRepo;
-
- @Inject
- MembershipMapper membershipMapper;
- @Inject
- AccountRepo accountRepo;
-
- @Transactional
- public Membership create(TenantType tenantType, String tenantId, MembershipCreation membershipCreation)
- {
- switch (tenantType)
- {
- case ORGANISATION ->
- {
- if (membershipCreation.role().equals(MembershipRole.OWNER))
- {
- if (authenticationService.isSuperUser())
- {
- AccountEntity account = accountRepo.findById(membershipCreation.accountId());
-
- MembershipEntity membership = MembershipEntity.init();
- membership.setAccount(account);
- membership.setRole(membershipCreation.role());
- membership.setTenantType(TenantType.ORGANISATION);
- membership.setTenantId(tenantId);
- membership.setMemberSince(ZonedDateTime.now());
- membershipRepo.persist(membership);
-
- account.setStatus(AccountStatus.REGISTERED);
- accountRepo.persist(account);
-
- return membershipMapper.map(membership);
- }
- LOG.errorf("Membership with role 'Owner' cannot be created without superuser permissions");
- throw new UnauthorizedException();
- }
- }
- case RESTAURANT ->
- {
-
- }
- default ->
- {
-
- }
- }
- throw new BadRequestException();
- }
-
- public List findByTenantType(TenantType tenantType)
- {
- return membershipMapper.map(membershipRepo.findByTenantTypeAndAccount(tenantType, authenticationService.requireUser()));
- }
-
- public List findByTenantTypeAndTenantId(TenantType tenantType, String tenantId)
- {
- return membershipMapper.map(membershipRepo.findByTenantTypeAndTenantId(tenantType, tenantId));
- }
-}
diff --git a/src/main/java/de/tavolio/member/TenantMembershipResource.java b/src/main/java/de/tavolio/member/TenantMembershipResource.java
deleted file mode 100644
index 3d887e9..0000000
--- a/src/main/java/de/tavolio/member/TenantMembershipResource.java
+++ /dev/null
@@ -1,80 +0,0 @@
-package de.tavolio.member;
-
-import de.tavolio.member.dto.Membership;
-import de.tavolio.member.dto.MembershipCreation;
-import jakarta.inject.Inject;
-import jakarta.ws.rs.*;
-import org.jboss.logging.Logger;
-import org.jboss.resteasy.reactive.common.NotImplementedYet;
-
-import java.util.List;
-
-@Path("/{tenant-type}")
-public class TenantMembershipResource
-{
- @Inject
- Logger LOG;
-
- @Inject
- MembershipService membershipService;
-
- @POST
- @Path("/{tenant-id}/memberships")
- public Membership post(@PathParam("tenant-type") String tenantType, @PathParam("tenant-id") String tenantId, MembershipCreation membershipCreation)
- {
- switch (tenantType)
- {
- case "organisations" ->
- {
- Membership membership = membershipService.create(TenantType.ORGANISATION, tenantId, membershipCreation);
- LOG.infof("Created membership for organisation %s", tenantId);
- return membership;
- }
- case "restaurants" ->
- {
- Membership membership = membershipService.create(TenantType.RESTAURANT, tenantId, membershipCreation);
- LOG.infof("Created membership for restaurant %s", tenantId);
- return membership;
- }
- }
- throw new BadRequestException();
- }
-
- @GET
- @Path("/{tenant-id}/memberships")
- public List get(@PathParam("tenant-type") String tenantType, @PathParam("tenant-id") String tenantId)
- {
- switch (tenantType)
- {
- case "organisations" ->
- {
- return membershipService.findByTenantTypeAndTenantId(TenantType.ORGANISATION, tenantId);
- }
- case "restaurants" ->
- {
- throw new NotImplementedYet();
- }
- }
- LOG.errorf("Unknown tenant type %s", tenantType);
- throw new BadRequestException();
- }
-
- @GET
- @Path("/memberships")
- public List get(@PathParam("tenant-type") String tenantType)
- {
- switch (tenantType)
- {
- case "organisations" ->
- {
- return membershipService.findByTenantType(TenantType.ORGANISATION);
- }
- case "restaurants" ->
- {
- return membershipService.findByTenantType(TenantType.RESTAURANT);
- }
- }
- LOG.errorf("Unknown tenant type %s", tenantType);
- throw new BadRequestException();
- }
-}
diff --git a/src/main/java/de/tavolio/member/TenantType.java b/src/main/java/de/tavolio/member/TenantType.java
deleted file mode 100644
index 6f226b9..0000000
--- a/src/main/java/de/tavolio/member/TenantType.java
+++ /dev/null
@@ -1,6 +0,0 @@
-package de.tavolio.member;
-
-public enum TenantType
-{
- ORGANISATION, RESTAURANT
-}
diff --git a/src/main/java/de/tavolio/member/dto/AccountMemberships.java b/src/main/java/de/tavolio/member/dto/AccountMemberships.java
deleted file mode 100644
index 00d1308..0000000
--- a/src/main/java/de/tavolio/member/dto/AccountMemberships.java
+++ /dev/null
@@ -1,7 +0,0 @@
-package de.tavolio.member.dto;
-
-import java.util.List;
-
-public record AccountMemberships(List organisations, List restaurants)
-{
-}
diff --git a/src/main/java/de/tavolio/member/dto/Membership.java b/src/main/java/de/tavolio/member/dto/Membership.java
deleted file mode 100644
index e064db7..0000000
--- a/src/main/java/de/tavolio/member/dto/Membership.java
+++ /dev/null
@@ -1,9 +0,0 @@
-package de.tavolio.member.dto;
-
-import de.tavolio.account.dto.Account;
-import de.tavolio.member.MembershipRole;
-import de.tavolio.member.TenantType;
-
-public record Membership(String id, TenantType tenantType, String tenantId, MembershipRole role, Account account)
-{
-}
diff --git a/src/main/java/de/tavolio/member/dto/MembershipCreation.java b/src/main/java/de/tavolio/member/dto/MembershipCreation.java
deleted file mode 100644
index a321d8e..0000000
--- a/src/main/java/de/tavolio/member/dto/MembershipCreation.java
+++ /dev/null
@@ -1,7 +0,0 @@
-package de.tavolio.member.dto;
-
-import de.tavolio.member.MembershipRole;
-
-public record MembershipCreation(String accountId, MembershipRole role)
-{
-}
diff --git a/src/main/java/de/tavolio/oidc/GrantType.java b/src/main/java/de/tavolio/oidc/GrantType.java
new file mode 100644
index 0000000..a4552e1
--- /dev/null
+++ b/src/main/java/de/tavolio/oidc/GrantType.java
@@ -0,0 +1,30 @@
+package de.tavolio.oidc;
+
+public enum GrantType
+{
+ AUTH_CODE("authorization_code"),
+ CLIENT_CREDENTIALS("client_credentials");
+
+ private final String value;
+
+ GrantType(String value)
+ {
+ this.value = value;
+ }
+
+ public String getValue()
+ {
+ return value;
+ }
+
+ public static GrantType fromValue(String value)
+ {
+ for (GrantType type : GrantType.values())
+ {
+ if (type.value.equalsIgnoreCase(value))
+ {
+ return type;
+ }
+ } throw new IllegalArgumentException("Unknown grant type: " + value);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/de/tavolio/oidc/IssuerService.java b/src/main/java/de/tavolio/oidc/IssuerService.java
new file mode 100644
index 0000000..c10e3b3
--- /dev/null
+++ b/src/main/java/de/tavolio/oidc/IssuerService.java
@@ -0,0 +1,16 @@
+package de.tavolio.oidc;
+
+import jakarta.enterprise.context.ApplicationScoped;
+import org.eclipse.microprofile.config.inject.ConfigProperty;
+
+@ApplicationScoped
+public class IssuerService
+{
+ @ConfigProperty(name = "dev.dinauer.idp.origin")
+ String origin;
+
+ public String getIssuer(String realmKey)
+ {
+ return String.format("%s/api/iam-backend/realms/%s", origin, realmKey);
+ }
+}
diff --git a/src/main/java/de/tavolio/oidc/JwksService.java b/src/main/java/de/tavolio/oidc/JwksService.java
new file mode 100644
index 0000000..04fe4c3
--- /dev/null
+++ b/src/main/java/de/tavolio/oidc/JwksService.java
@@ -0,0 +1,47 @@
+package de.tavolio.oidc;
+
+import de.tavolio.realm.RealmEntity;
+import de.tavolio.realm.RealmRepo;
+import de.tavolio.realm.key.KeypairEntity;
+import jakarta.inject.Inject;
+import jakarta.ws.rs.*;
+import jakarta.ws.rs.core.MediaType;
+
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+@Path("/realms/{realm-key}/oidc/keys")
+public class JwksService
+{
+ @Inject
+ RealmRepo realmRepo;
+
+ @GET
+ @Produces(MediaType.APPLICATION_JSON)
+ public Map get(@PathParam("realm-key") String realmKey)
+ {
+ RealmEntity realm = realmRepo.findByKey(realmKey);
+ if (realm != null)
+ {
+ List