From 79104dd02f98efa6f9e553c412a6f29e7ce549b5 Mon Sep 17 00:00:00 2001 From: Andreas Dinauer Date: Sun, 8 Mar 2026 08:59:03 +0100 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Transform=20to=20IDP=20pro?= =?UTF-8?q?vider?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 13 +- postgres.yaml | 11 ++ .../de/tavolio/AuthenticationService.java | 71 +--------- src/main/java/de/tavolio/Role.java | 6 + .../de/tavolio/account/AccountEntity.java | 113 --------------- .../de/tavolio/account/AccountMapper.java | 13 -- .../java/de/tavolio/account/AccountRepo.java | 16 --- .../de/tavolio/account/AccountResource.java | 36 ----- .../de/tavolio/account/AccountService.java | 52 ------- .../de/tavolio/account/AccountStatus.java | 6 - .../java/de/tavolio/account/dto/Account.java | 7 - .../bootstrap/AccountBootstrapper.java | 59 ++++++++ .../tavolio/bootstrap/BootstrapService.java | 40 ++++++ .../tavolio/bootstrap/ClientBootstrapper.java | 30 ++++ .../de/tavolio/bootstrap/KeyBootstrapper.java | 22 +++ .../bootstrap/RealmBootstrapService.java | 87 ++++++++++++ .../tavolio/bootstrap/RoleBootstrapper.java | 43 ++++++ .../bootstrap/SuperuserBootstrapper.java | 33 +++++ .../de/tavolio/bootstrap/model/Account.java | 7 + .../de/tavolio/bootstrap/model/Audience.java | 5 + .../de/tavolio/bootstrap/model/Bootstrap.java | 7 + .../de/tavolio/bootstrap/model/Client.java | 9 ++ .../java/de/tavolio/bootstrap/model/Key.java | 5 + .../de/tavolio/bootstrap/model/Realm.java | 8 ++ .../java/de/tavolio/bootstrap/model/Role.java | 7 + .../member/AccountMembershipResource.java | 33 ----- .../de/tavolio/member/MembershipEntity.java | 105 -------------- .../de/tavolio/member/MembershipMapper.java | 25 ---- .../de/tavolio/member/MembershipRepo.java | 22 --- .../de/tavolio/member/MembershipRole.java | 6 - .../de/tavolio/member/MembershipService.java | 87 ------------ .../member/TenantMembershipResource.java | 80 ----------- .../java/de/tavolio/member/TenantType.java | 6 - .../member/dto/AccountMemberships.java | 7 - .../de/tavolio/member/dto/Membership.java | 9 -- .../member/dto/MembershipCreation.java | 7 - src/main/java/de/tavolio/oidc/GrantType.java | 30 ++++ .../java/de/tavolio/oidc/IssuerService.java | 16 +++ .../java/de/tavolio/oidc/JwksService.java | 47 +++++++ .../de/tavolio/oidc/OidcConfiguration.java | 61 ++++++++ .../oidc/OidcConfigurationResource.java | 36 +++++ .../java/de/tavolio/oidc/OidcResource.java | 65 +++++++++ .../oidc/auth/OidcClientIdentityProvider.java | 66 +++++++++ .../oidc/session/AuthorizationService.java | 60 ++++++++ .../session/dto/SessionCreation.java | 2 +- .../de/tavolio/oidc/token/TokenGenerator.java | 39 ++++++ .../de/tavolio/oidc/token/TokenService.java | 75 ++++++++++ .../oidc/token/model/TokenResponse.java | 76 ++++++++++ .../java/de/tavolio/realm/RealmCreation.java | 5 + .../java/de/tavolio/realm/RealmEntity.java | 129 +++++++++++++++++ src/main/java/de/tavolio/realm/RealmRepo.java | 21 +++ .../java/de/tavolio/realm/RealmResource.java | 29 ++++ .../java/de/tavolio/realm/RealmScoped.java | 6 + .../java/de/tavolio/realm/RealmService.java | 44 ++++++ .../audience/AudienceStrategyEntity.java | 66 +++++++++ .../realm/audience/AudienceStrategyRepo.java | 9 ++ .../tavolio/realm/client/ClientCreation.java | 5 + .../de/tavolio/realm/client/ClientEntity.java | 83 +++++++++++ .../de/tavolio/realm/client/ClientRepo.java | 22 +++ .../tavolio/realm/client/ClientService.java | 59 ++++++++ .../de/tavolio/realm/code/CodeEntity.java | 86 ++++++++++++ .../java/de/tavolio/realm/code/CodeRepo.java | 15 ++ .../de/tavolio/realm/key/KeypairEntity.java | 131 ++++++++++++++++++ .../de/tavolio/realm/key/KeypairRepo.java | 9 ++ .../de/tavolio/realm/key/KeypairService.java | 82 +++++++++++ .../de/tavolio/realm/role/RoleEntity.java | 87 ++++++++++++ .../java/de/tavolio/realm/role/RoleRepo.java | 17 +++ .../de/tavolio/realm/user/UserEntity.java | 129 +++++++++++++++++ .../de/tavolio/realm/user/UserMapper.java | 20 +++ .../java/de/tavolio/realm/user/UserRepo.java | 23 +++ .../de/tavolio/realm/user/UserResource.java | 53 +++++++ .../de/tavolio/realm/user/UserService.java | 83 +++++++++++ .../de/tavolio/realm/user/UserStatus.java | 6 + .../java/de/tavolio/realm/user/dto/User.java | 7 + .../user/dto/UserCreation.java} | 4 +- .../de/tavolio/session/SessionResource.java | 26 ---- .../de/tavolio/session/SessionService.java | 39 ------ .../de/tavolio/superuser/SuperuserEntity.java | 37 +++++ .../de/tavolio/superuser/SuperuserRepo.java | 9 ++ .../java/de/tavolio/verify/JwksService.java | 65 +++++++++ .../java/de/tavolio/verify/TokenVerifier.java | 81 +++++++++++ .../de/tavolio/verify/jwks/EcPublicKey.java | 64 +++++++++ .../java/de/tavolio/verify/jwks/JwksKey.java | 41 ++++++ .../de/tavolio/verify/jwks/RsaPublicKey.java | 5 + src/main/resources/application.properties | 29 +++- src/main/resources/bootstrap.yaml | 22 +++ src/main/resources/db/dev/V9999__init.sql | 5 +- .../resources/db/migration/V1.0.1__init.sql | 13 -- src/main/resources/import.sql | 0 ...esourceTest.java => UserResourceTest.java} | 12 +- .../de/tavolio/realm/RealmServiceTest.java | 19 +++ 91 files changed, 2572 insertions(+), 791 deletions(-) create mode 100644 postgres.yaml create mode 100644 src/main/java/de/tavolio/Role.java delete mode 100644 src/main/java/de/tavolio/account/AccountEntity.java delete mode 100644 src/main/java/de/tavolio/account/AccountMapper.java delete mode 100644 src/main/java/de/tavolio/account/AccountRepo.java delete mode 100644 src/main/java/de/tavolio/account/AccountResource.java delete mode 100644 src/main/java/de/tavolio/account/AccountService.java delete mode 100644 src/main/java/de/tavolio/account/AccountStatus.java delete mode 100644 src/main/java/de/tavolio/account/dto/Account.java create mode 100644 src/main/java/de/tavolio/bootstrap/AccountBootstrapper.java create mode 100644 src/main/java/de/tavolio/bootstrap/BootstrapService.java create mode 100644 src/main/java/de/tavolio/bootstrap/ClientBootstrapper.java create mode 100644 src/main/java/de/tavolio/bootstrap/KeyBootstrapper.java create mode 100644 src/main/java/de/tavolio/bootstrap/RealmBootstrapService.java create mode 100644 src/main/java/de/tavolio/bootstrap/RoleBootstrapper.java create mode 100644 src/main/java/de/tavolio/bootstrap/SuperuserBootstrapper.java create mode 100644 src/main/java/de/tavolio/bootstrap/model/Account.java create mode 100644 src/main/java/de/tavolio/bootstrap/model/Audience.java create mode 100644 src/main/java/de/tavolio/bootstrap/model/Bootstrap.java create mode 100644 src/main/java/de/tavolio/bootstrap/model/Client.java create mode 100644 src/main/java/de/tavolio/bootstrap/model/Key.java create mode 100644 src/main/java/de/tavolio/bootstrap/model/Realm.java create mode 100644 src/main/java/de/tavolio/bootstrap/model/Role.java delete mode 100644 src/main/java/de/tavolio/member/AccountMembershipResource.java delete mode 100644 src/main/java/de/tavolio/member/MembershipEntity.java delete mode 100644 src/main/java/de/tavolio/member/MembershipMapper.java delete mode 100644 src/main/java/de/tavolio/member/MembershipRepo.java delete mode 100644 src/main/java/de/tavolio/member/MembershipRole.java delete mode 100644 src/main/java/de/tavolio/member/MembershipService.java delete mode 100644 src/main/java/de/tavolio/member/TenantMembershipResource.java delete mode 100644 src/main/java/de/tavolio/member/TenantType.java delete mode 100644 src/main/java/de/tavolio/member/dto/AccountMemberships.java delete mode 100644 src/main/java/de/tavolio/member/dto/Membership.java delete mode 100644 src/main/java/de/tavolio/member/dto/MembershipCreation.java create mode 100644 src/main/java/de/tavolio/oidc/GrantType.java create mode 100644 src/main/java/de/tavolio/oidc/IssuerService.java create mode 100644 src/main/java/de/tavolio/oidc/JwksService.java create mode 100644 src/main/java/de/tavolio/oidc/OidcConfiguration.java create mode 100644 src/main/java/de/tavolio/oidc/OidcConfigurationResource.java create mode 100644 src/main/java/de/tavolio/oidc/OidcResource.java create mode 100644 src/main/java/de/tavolio/oidc/auth/OidcClientIdentityProvider.java create mode 100644 src/main/java/de/tavolio/oidc/session/AuthorizationService.java rename src/main/java/de/tavolio/{ => oidc}/session/dto/SessionCreation.java (83%) create mode 100644 src/main/java/de/tavolio/oidc/token/TokenGenerator.java create mode 100644 src/main/java/de/tavolio/oidc/token/TokenService.java create mode 100644 src/main/java/de/tavolio/oidc/token/model/TokenResponse.java create mode 100644 src/main/java/de/tavolio/realm/RealmCreation.java create mode 100644 src/main/java/de/tavolio/realm/RealmEntity.java create mode 100644 src/main/java/de/tavolio/realm/RealmRepo.java create mode 100644 src/main/java/de/tavolio/realm/RealmResource.java create mode 100644 src/main/java/de/tavolio/realm/RealmScoped.java create mode 100644 src/main/java/de/tavolio/realm/RealmService.java create mode 100644 src/main/java/de/tavolio/realm/audience/AudienceStrategyEntity.java create mode 100644 src/main/java/de/tavolio/realm/audience/AudienceStrategyRepo.java create mode 100644 src/main/java/de/tavolio/realm/client/ClientCreation.java create mode 100644 src/main/java/de/tavolio/realm/client/ClientEntity.java create mode 100644 src/main/java/de/tavolio/realm/client/ClientRepo.java create mode 100644 src/main/java/de/tavolio/realm/client/ClientService.java create mode 100644 src/main/java/de/tavolio/realm/code/CodeEntity.java create mode 100644 src/main/java/de/tavolio/realm/code/CodeRepo.java create mode 100644 src/main/java/de/tavolio/realm/key/KeypairEntity.java create mode 100644 src/main/java/de/tavolio/realm/key/KeypairRepo.java create mode 100644 src/main/java/de/tavolio/realm/key/KeypairService.java create mode 100644 src/main/java/de/tavolio/realm/role/RoleEntity.java create mode 100644 src/main/java/de/tavolio/realm/role/RoleRepo.java create mode 100644 src/main/java/de/tavolio/realm/user/UserEntity.java create mode 100644 src/main/java/de/tavolio/realm/user/UserMapper.java create mode 100644 src/main/java/de/tavolio/realm/user/UserRepo.java create mode 100644 src/main/java/de/tavolio/realm/user/UserResource.java create mode 100644 src/main/java/de/tavolio/realm/user/UserService.java create mode 100644 src/main/java/de/tavolio/realm/user/UserStatus.java create mode 100644 src/main/java/de/tavolio/realm/user/dto/User.java rename src/main/java/de/tavolio/{account/dto/AccountCreation.java => realm/user/dto/UserCreation.java} (77%) delete mode 100644 src/main/java/de/tavolio/session/SessionResource.java delete mode 100644 src/main/java/de/tavolio/session/SessionService.java create mode 100644 src/main/java/de/tavolio/superuser/SuperuserEntity.java create mode 100644 src/main/java/de/tavolio/superuser/SuperuserRepo.java create mode 100644 src/main/java/de/tavolio/verify/JwksService.java create mode 100644 src/main/java/de/tavolio/verify/TokenVerifier.java create mode 100644 src/main/java/de/tavolio/verify/jwks/EcPublicKey.java create mode 100644 src/main/java/de/tavolio/verify/jwks/JwksKey.java create mode 100644 src/main/java/de/tavolio/verify/jwks/RsaPublicKey.java create mode 100644 src/main/resources/bootstrap.yaml create mode 100644 src/main/resources/import.sql rename src/test/java/de/tavolio/account/{AccountResourceTest.java => UserResourceTest.java} (88%) create mode 100644 src/test/java/de/tavolio/realm/RealmServiceTest.java 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> result = new LinkedList<>(); + for (KeypairEntity keypair : realm.getKeys()) + { + if ("EC".equals(keypair.getType())) + { + result.add(Map.ofEntries( + Map.entry("kty", "EC"), + Map.entry("alg", keypair.getAlg()), + Map.entry("use", keypair.getUse()), + Map.entry("crv", keypair.getCrv()), + Map.entry("kid", keypair.getId()), + Map.entry("x", keypair.getX()), + Map.entry("y", keypair.getY()) + )); + } + } + return Map.ofEntries(Map.entry("keys", result)); + } + throw new NotFoundException(); + } +} diff --git a/src/main/java/de/tavolio/oidc/OidcConfiguration.java b/src/main/java/de/tavolio/oidc/OidcConfiguration.java new file mode 100644 index 0000000..72125ef --- /dev/null +++ b/src/main/java/de/tavolio/oidc/OidcConfiguration.java @@ -0,0 +1,61 @@ +package de.tavolio.oidc; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class OidcConfiguration +{ + private String issuer; + + @JsonProperty("token_endpoint") + private String tokenEndpoint; + + @JsonProperty("authorization_endpoint") + private String authorizationEndpoint; + + @JsonProperty("jwks_uri") + private String jwksURI; + + public String getIssuer() + { + return issuer; + } + + public OidcConfiguration setIssuer(String issuer) + { + this.issuer = issuer; + return this; + } + + public String getTokenEndpoint() + { + return tokenEndpoint; + } + + public OidcConfiguration setTokenEndpoint(String tokenEndpoint) + { + this.tokenEndpoint = tokenEndpoint; + return this; + } + + public String getAuthorizationEndpoint() + { + return authorizationEndpoint; + } + + public OidcConfiguration setAuthorizationEndpoint(String authorizationEndpoint) + { + this.authorizationEndpoint = authorizationEndpoint; + return this; + } + + public String getJwksURI() + { + return jwksURI; + } + + public OidcConfiguration setJwksURI(String jwksURI) + { + this.jwksURI = jwksURI; + return this; + } +} diff --git a/src/main/java/de/tavolio/oidc/OidcConfigurationResource.java b/src/main/java/de/tavolio/oidc/OidcConfigurationResource.java new file mode 100644 index 0000000..5f9b863 --- /dev/null +++ b/src/main/java/de/tavolio/oidc/OidcConfigurationResource.java @@ -0,0 +1,36 @@ +package de.tavolio.oidc; + +import de.tavolio.realm.RealmEntity; +import de.tavolio.realm.RealmRepo; +import jakarta.enterprise.context.RequestScoped; +import jakarta.inject.Inject; +import jakarta.ws.rs.*; +import org.eclipse.microprofile.config.inject.ConfigProperty; + +@RequestScoped +public class OidcConfigurationResource +{ + @Inject + RealmRepo realmRepo; + + @Inject + IssuerService issuerService; + + @ConfigProperty(name = "dev.dinauer.idp.origin") + String origin; + + @GET + public OidcConfiguration get(@PathParam("realm-key") String realmKey) + { + RealmEntity realm = realmRepo.findByKey(realmKey); + if (realm != null) + { + return new OidcConfiguration() + .setIssuer(issuerService.getIssuer(realmKey)) + .setTokenEndpoint(String.format("%s/api/iam-backend/realms/%s/protocol/openid-connect/token", origin, realmKey)) + .setAuthorizationEndpoint(String.format("%s/api/iam-backend/realms/%s/protocol/openid-connect/auth", origin, realmKey)) + .setJwksURI(String.format("%s/api/iam-backend/realms/%s/protocol/openid-connect/certs", origin, realmKey)); + } + throw new NotFoundException(); + } +} diff --git a/src/main/java/de/tavolio/oidc/OidcResource.java b/src/main/java/de/tavolio/oidc/OidcResource.java new file mode 100644 index 0000000..1fae4c1 --- /dev/null +++ b/src/main/java/de/tavolio/oidc/OidcResource.java @@ -0,0 +1,65 @@ +package de.tavolio.oidc; + +import de.tavolio.oidc.session.AuthorizationService; +import de.tavolio.oidc.session.dto.SessionCreation; +import de.tavolio.oidc.token.model.TokenResponse; +import de.tavolio.oidc.token.TokenService; +import jakarta.annotation.security.RolesAllowed; +import jakarta.enterprise.context.RequestScoped; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.Response; + +import java.net.URI; +import java.security.NoSuchAlgorithmException; +import java.security.spec.InvalidKeySpecException; +import java.util.Map; + +@RequestScoped +public class OidcResource +{ + @PathParam("realm-key") + String realmKey; + + @Inject + JwksService jwksService; + + @Inject + TokenService tokenService; + + @Inject + AuthorizationService authorizationService; + + @GET + @Path("/certs") + public Map certs() + { + return jwksService.get(realmKey); + } + + @POST + @Path("/auth") + public Response auth(@QueryParam("client_id") String clientId, @FormParam("email") String email, @FormParam("password") String password) + { + String code = authorizationService.generateBySessionCreation(realmKey, clientId, new SessionCreation(email, password)); + return Response.status(302).location(URI.create("http://localhost:8080/callback?code=" + code + "&state=d")).build(); + } + + @POST + @Path("/token") + @RolesAllowed("CLIENT") + @Transactional + public TokenResponse token(@FormParam("grant_type") String grantType, @FormParam("client_id") String clientId, @FormParam("code") String code) throws NoSuchAlgorithmException, InvalidKeySpecException + { + if (GrantType.AUTH_CODE.equals(GrantType.fromValue(grantType))) + { + return tokenService.getUserToken(realmKey, code); + } + if (GrantType.CLIENT_CREDENTIALS.equals(GrantType.fromValue(grantType))) + { + return tokenService.getClientToken(realmKey); + } + throw new RuntimeException(); + } +} diff --git a/src/main/java/de/tavolio/oidc/auth/OidcClientIdentityProvider.java b/src/main/java/de/tavolio/oidc/auth/OidcClientIdentityProvider.java new file mode 100644 index 0000000..7ba318e --- /dev/null +++ b/src/main/java/de/tavolio/oidc/auth/OidcClientIdentityProvider.java @@ -0,0 +1,66 @@ +package de.tavolio.oidc.auth; + +import de.tavolio.Role; +import de.tavolio.realm.user.UserRepo; +import de.tavolio.realm.client.ClientEntity; +import de.tavolio.realm.client.ClientRepo; +import de.tavolio.superuser.SuperuserEntity; +import de.tavolio.superuser.SuperuserRepo; +import io.quarkus.elytron.security.common.BcryptUtil; +import io.quarkus.security.AuthenticationFailedException; +import io.quarkus.security.identity.AuthenticationRequestContext; +import io.quarkus.security.identity.IdentityProvider; +import io.quarkus.security.identity.SecurityIdentity; +import io.quarkus.security.identity.request.UsernamePasswordAuthenticationRequest; +import io.quarkus.security.runtime.QuarkusPrincipal; +import io.quarkus.security.runtime.QuarkusSecurityIdentity; +import io.smallrye.mutiny.Uni; +import io.smallrye.mutiny.infrastructure.Infrastructure; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.context.control.ActivateRequestContext; +import jakarta.inject.Inject; + +@ApplicationScoped +public class OidcClientIdentityProvider implements IdentityProvider +{ + @Inject + ClientRepo clientRepo; + + @Inject + SuperuserRepo superuserRepo; + @Inject + UserRepo userRepo; + + @Override + public Class getRequestType() + { + return UsernamePasswordAuthenticationRequest.class; + } + + @Override + @ActivateRequestContext + public Uni authenticate(UsernamePasswordAuthenticationRequest usernamePasswordAuthenticationRequest, AuthenticationRequestContext authenticationRequestContext) + { + return Uni.createFrom().item(() -> { + String username = usernamePasswordAuthenticationRequest.getUsername(); + String credential = new String(usernamePasswordAuthenticationRequest.getPassword().getPassword()); + SuperuserEntity superuser = superuserRepo.findById(username); + if (superuser != null) + { + if (BcryptUtil.matches(credential, superuser.getPassword())) + { + return (SecurityIdentity) QuarkusSecurityIdentity.builder().setPrincipal(new QuarkusPrincipal(username)).addRole(Role.ROOT.toString()).build(); + } + } + ClientEntity client = clientRepo.findById(username); + if (client != null) + { + if (BcryptUtil.matches(credential, client.getSecret())) + { + return (SecurityIdentity) QuarkusSecurityIdentity.builder().setPrincipal(new QuarkusPrincipal(username)).addRole(Role.CLIENT.toString()).build(); + } + } + throw new AuthenticationFailedException(); + }).runSubscriptionOn(Infrastructure.getDefaultWorkerPool()); + } +} diff --git a/src/main/java/de/tavolio/oidc/session/AuthorizationService.java b/src/main/java/de/tavolio/oidc/session/AuthorizationService.java new file mode 100644 index 0000000..87450dd --- /dev/null +++ b/src/main/java/de/tavolio/oidc/session/AuthorizationService.java @@ -0,0 +1,60 @@ +package de.tavolio.oidc.session; + +import de.tavolio.realm.RealmService; +import de.tavolio.realm.user.UserEntity; +import de.tavolio.realm.user.UserRepo; +import de.tavolio.realm.client.ClientEntity; +import de.tavolio.realm.client.ClientService; +import de.tavolio.realm.code.CodeEntity; +import de.tavolio.realm.code.CodeRepo; +import de.tavolio.realm.RealmEntity; +import de.tavolio.oidc.session.dto.SessionCreation; +import io.quarkus.elytron.security.common.BcryptUtil; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; +import jakarta.ws.rs.NotFoundException; + +import java.time.ZonedDateTime; +import java.util.Optional; +import java.util.UUID; + +@ApplicationScoped +public class AuthorizationService +{ + @Inject + UserRepo userRepo; + + @Inject + CodeRepo codeRepo; + + @Inject + RealmService realmService; + + @Inject + ClientService clientService; + + @Transactional + public String generateBySessionCreation(String realmKey, String clientId, SessionCreation sessionCreation) + { + + RealmEntity realm = realmService.requireByKey(realmKey); + ClientEntity client = clientService.findByIdAndRealm(clientId, realm); + Optional accountEntityOptional = userRepo.findOptionalByRealmAndEmail(realm, sessionCreation.email()); + if (accountEntityOptional.isPresent()) + { + UserEntity userEntity = accountEntityOptional.get(); + if (BcryptUtil.matches(sessionCreation.password(), userEntity.getPassword())) + { + CodeEntity code = new CodeEntity().setId(UUID.randomUUID().toString()); + code.setExpiresAt(ZonedDateTime.now().plusMinutes(1)); + code.setAccount(userEntity); + code.setRealm(realm); + code.setClient(client); + codeRepo.persist(code); + return code.getId(); + } + } + throw new NotFoundException(); + } +} diff --git a/src/main/java/de/tavolio/session/dto/SessionCreation.java b/src/main/java/de/tavolio/oidc/session/dto/SessionCreation.java similarity index 83% rename from src/main/java/de/tavolio/session/dto/SessionCreation.java rename to src/main/java/de/tavolio/oidc/session/dto/SessionCreation.java index cafc6b4..5f1c002 100644 --- a/src/main/java/de/tavolio/session/dto/SessionCreation.java +++ b/src/main/java/de/tavolio/oidc/session/dto/SessionCreation.java @@ -1,4 +1,4 @@ -package de.tavolio.session.dto; +package de.tavolio.oidc.session.dto; import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; diff --git a/src/main/java/de/tavolio/oidc/token/TokenGenerator.java b/src/main/java/de/tavolio/oidc/token/TokenGenerator.java new file mode 100644 index 0000000..cd5788b --- /dev/null +++ b/src/main/java/de/tavolio/oidc/token/TokenGenerator.java @@ -0,0 +1,39 @@ +package de.tavolio.oidc.token; + +import de.tavolio.oidc.IssuerService; +import io.smallrye.jwt.build.Jwt; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import java.security.PrivateKey; +import java.time.ZonedDateTime; + +@ApplicationScoped +public class TokenGenerator +{ + @Inject + IssuerService issuerService; + + public String generateAccessToken(String realmKey, String clientId, String upn, ZonedDateTime expiresAt, PrivateKey key, String keyId) + { + return Jwt.claims() + .upn(upn) + .audience(clientId) + .claim("realm_key", realmKey) + .claim("client_id", clientId) + .expiresAt(expiresAt.toInstant()) + .issuer(issuerService.getIssuer(realmKey)).jws().keyId(keyId) + .sign(key); + } + + public String generateIDToken(String realmKey, String clientId, String upn, ZonedDateTime expiresAt, PrivateKey key, String keyId) + { + return Jwt.claims() + .upn(upn) + .claim("realm_key", realmKey) + .claim("client_id", clientId) + .expiresAt(expiresAt.toInstant()) + .issuer(issuerService.getIssuer(realmKey)).jws().keyId(keyId) + .sign(key); + } +} \ No newline at end of file diff --git a/src/main/java/de/tavolio/oidc/token/TokenService.java b/src/main/java/de/tavolio/oidc/token/TokenService.java new file mode 100644 index 0000000..6c9de0e --- /dev/null +++ b/src/main/java/de/tavolio/oidc/token/TokenService.java @@ -0,0 +1,75 @@ +package de.tavolio.oidc.token; + +import de.tavolio.oidc.token.model.TokenResponse; +import de.tavolio.realm.RealmRepo; +import de.tavolio.realm.code.CodeEntity; +import de.tavolio.realm.code.CodeRepo; +import de.tavolio.realm.RealmEntity; +import de.tavolio.realm.RealmService; +import de.tavolio.realm.key.KeypairEntity; +import io.quarkus.security.identity.SecurityIdentity; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.ws.rs.*; +import jakarta.ws.rs.core.Context; +import org.apache.commons.lang3.NotImplementedException; + +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; +import java.time.ZonedDateTime; +import java.util.Set; +import java.util.UUID; + +@ApplicationScoped +public class TokenService +{ + @Inject + CodeRepo codeRepo; + + @Inject + TokenGenerator tokenGenerator; + + @Inject + SecurityIdentity identity; + + @Inject + RealmRepo realmRepo; + + @POST + public TokenResponse getClientToken(String realmKey) + { + return null; + } + + public TokenResponse getUserToken(String realmKey, String code) throws NoSuchAlgorithmException, InvalidKeySpecException + { + return generateUserToken(realmKey, code); + } + + private TokenResponse generateUserToken(String realmKey, String code) throws NoSuchAlgorithmException, InvalidKeySpecException + { + String principal = identity.getPrincipal().getName(); + RealmEntity realm = realmRepo.findById(realmKey); + CodeEntity entity = codeRepo.findByRealmAndId(realm, code); + if (entity != null && !ZonedDateTime.now().isAfter(entity.getExpiresAt()) && principal.equals(entity.getClient().getId())) + { + KeypairEntity keypair = realm.getKeys().getFirst(); + PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(keypair.getPrivateKey()); + ZonedDateTime expiresAt = ZonedDateTime.now().plusYears(1); + PrivateKey signingKey = KeyFactory.getInstance(RealmService.EC).generatePrivate(spec); + TokenResponse response = new TokenResponse() + .setAccessToken(tokenGenerator.generateAccessToken(realm.getKey(), principal, entity.getAccount().getId(), expiresAt, signingKey, keypair.getId())) + .setRefreshToken(UUID.randomUUID().toString()) + .setIdToken(tokenGenerator.generateIDToken(realm.getKey(), principal, entity.getAccount().getId(), expiresAt, signingKey, keypair.getId())) + .setTokenType("Bearer") + .setExpiresAt(expiresAt.toInstant().getEpochSecond()); + codeRepo.delete(entity); + return response; + } + throw new BadRequestException(); + } +} diff --git a/src/main/java/de/tavolio/oidc/token/model/TokenResponse.java b/src/main/java/de/tavolio/oidc/token/model/TokenResponse.java new file mode 100644 index 0000000..373bd3d --- /dev/null +++ b/src/main/java/de/tavolio/oidc/token/model/TokenResponse.java @@ -0,0 +1,76 @@ +package de.tavolio.oidc.token.model; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class TokenResponse +{ + @JsonProperty("access_token") + private String accessToken; + + @JsonProperty("refresh_token") + private String refreshToken; + + @JsonProperty("id_token") + private String idToken; + + @JsonProperty("token_type") + private String tokenType; + + @JsonProperty("expires_at") + private Long expiresAt; + + public String getAccessToken() + { + return accessToken; + } + + public TokenResponse setAccessToken(String accessToken) + { + this.accessToken = accessToken; + return this; + } + + public String getRefreshToken() + { + return refreshToken; + } + + public TokenResponse setRefreshToken(String refreshToken) + { + this.refreshToken = refreshToken; + return this; + } + + public String getIdToken() + { + return idToken; + } + + public TokenResponse setIdToken(String idToken) + { + this.idToken = idToken; + return this; + } + + public String getTokenType() + { + return tokenType; + } + + public TokenResponse setTokenType(String tokenType) + { + this.tokenType = tokenType; + return this; + } + + public Long getExpiresAt() + { + return expiresAt; + } + + public TokenResponse setExpiresAt(Long expiresAt) + { + this.expiresAt = expiresAt; + return this; + } +} diff --git a/src/main/java/de/tavolio/realm/RealmCreation.java b/src/main/java/de/tavolio/realm/RealmCreation.java new file mode 100644 index 0000000..76c2a25 --- /dev/null +++ b/src/main/java/de/tavolio/realm/RealmCreation.java @@ -0,0 +1,5 @@ +package de.tavolio.realm; + +public record RealmCreation(String name, String key) +{ +} diff --git a/src/main/java/de/tavolio/realm/RealmEntity.java b/src/main/java/de/tavolio/realm/RealmEntity.java new file mode 100644 index 0000000..cfa3d21 --- /dev/null +++ b/src/main/java/de/tavolio/realm/RealmEntity.java @@ -0,0 +1,129 @@ +package de.tavolio.realm; + +import de.tavolio.realm.audience.AudienceStrategyEntity; +import de.tavolio.realm.key.KeypairEntity; +import de.tavolio.realm.user.UserEntity; +import de.tavolio.realm.client.ClientEntity; +import de.tavolio.realm.code.CodeEntity; +import de.tavolio.realm.role.RoleEntity; +import jakarta.persistence.*; + +import java.util.ArrayList; +import java.util.List; + +@Entity +@Table(name = "realm") +public class RealmEntity +{ + @Id + private String key; + + @Column(name = "realm_name") + private String name; + + @OneToMany(mappedBy = "realm", cascade = CascadeType.ALL) + private List keys = new ArrayList<>(); + + @OneToMany(mappedBy = "realm") + private List accounts = new ArrayList<>(); + + @OneToMany(mappedBy = "realm") + private List clients = new ArrayList<>(); + + @OneToMany(mappedBy = "realm") + private List codes = new ArrayList<>(); + + @OneToMany(mappedBy = "realm") + private List roles = new ArrayList<>(); + + @OneToOne(mappedBy = "realm") + private AudienceStrategyEntity audienceStrategy; + + public String getKey() + { + return key; + } + + public RealmEntity setKey(String key) + { + this.key = key; + return this; + } + + public String getName() + { + return name; + } + + public RealmEntity setName(String name) + { + this.name = name; + return this; + } + + public List getKeys() + { + return keys; + } + + public RealmEntity setKeys(List keys) + { + this.keys = keys; + return this; + } + + public List getAccounts() + { + return accounts; + } + + public RealmEntity setAccounts(List accounts) + { + this.accounts = accounts; + return this; + } + + public List getClients() + { + return clients; + } + + public RealmEntity setClients(List clients) + { + this.clients = clients; + return this; + } + + public List getCodes() + { + return codes; + } + + public RealmEntity setCodes(List codes) + { + this.codes = codes; + return this; + } + + public List getRoles() + { + return roles; + } + + public RealmEntity setRoles(List roles) + { + this.roles = roles; + return this; + } + + public AudienceStrategyEntity getAudienceStrategy() + { + return audienceStrategy; + } + + public RealmEntity setAudienceStrategy(AudienceStrategyEntity audienceStrategy) + { + this.audienceStrategy = audienceStrategy; + return this; + } +} diff --git a/src/main/java/de/tavolio/realm/RealmRepo.java b/src/main/java/de/tavolio/realm/RealmRepo.java new file mode 100644 index 0000000..d842969 --- /dev/null +++ b/src/main/java/de/tavolio/realm/RealmRepo.java @@ -0,0 +1,21 @@ +package de.tavolio.realm; + +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 RealmRepo implements PanacheRepositoryBase +{ + public Optional findByKeyOptional(String key) + { + return find("key = :key", Parameters.with("key", key)).firstResultOptional(); + } + + public RealmEntity findByKey(String key) + { + return find("key = :key", Parameters.with("key", key)).firstResult(); + } +} diff --git a/src/main/java/de/tavolio/realm/RealmResource.java b/src/main/java/de/tavolio/realm/RealmResource.java new file mode 100644 index 0000000..4452088 --- /dev/null +++ b/src/main/java/de/tavolio/realm/RealmResource.java @@ -0,0 +1,29 @@ +package de.tavolio.realm; + +import de.tavolio.oidc.OidcConfigurationResource; +import de.tavolio.oidc.OidcResource; +import de.tavolio.realm.user.UserResource; +import jakarta.enterprise.inject.spi.CDI; +import jakarta.ws.rs.Path; + +@Path("/realms/{realm-key}") +public class RealmResource +{ + @Path("/accounts") + public UserResource accounts() + { + return CDI.current().select(UserResource.class).get(); + } + + @Path("/protocol/openid-connect") + public OidcResource oidc() + { + return CDI.current().select(OidcResource.class).get(); + } + + @Path("/.well-known/openid-configuration") + public OidcConfigurationResource oidcConfigurationResource() + { + return CDI.current().select(OidcConfigurationResource.class).get(); + } +} diff --git a/src/main/java/de/tavolio/realm/RealmScoped.java b/src/main/java/de/tavolio/realm/RealmScoped.java new file mode 100644 index 0000000..87a8b06 --- /dev/null +++ b/src/main/java/de/tavolio/realm/RealmScoped.java @@ -0,0 +1,6 @@ +package de.tavolio.realm; + +public interface RealmScoped +{ + RealmEntity getRealm(); +} diff --git a/src/main/java/de/tavolio/realm/RealmService.java b/src/main/java/de/tavolio/realm/RealmService.java new file mode 100644 index 0000000..691dcb8 --- /dev/null +++ b/src/main/java/de/tavolio/realm/RealmService.java @@ -0,0 +1,44 @@ +package de.tavolio.realm; + +import de.tavolio.bootstrap.model.Realm; +import de.tavolio.realm.key.KeypairEntity; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; +import jakarta.ws.rs.NotFoundException; + +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.interfaces.ECPublicKey; +import java.security.spec.ECGenParameterSpec; +import java.util.*; + +@ApplicationScoped +public class RealmService +{ + public static final String EC = "EC"; + public static final String P256 = "secp256r1"; + + @Inject + RealmRepo repo; + + public RealmEntity requireByKey(String key) + { + RealmEntity realm = repo.findById(key); + if (realm != null) + { + return realm; + } + throw new NotFoundException(); + } + + @Transactional + public RealmEntity create(RealmCreation creation) + { + RealmEntity realm = new RealmEntity(); + realm.setKey(creation.key()); + realm.setName(creation.name()); + repo.persist(realm); + return realm; + } +} diff --git a/src/main/java/de/tavolio/realm/audience/AudienceStrategyEntity.java b/src/main/java/de/tavolio/realm/audience/AudienceStrategyEntity.java new file mode 100644 index 0000000..e37214f --- /dev/null +++ b/src/main/java/de/tavolio/realm/audience/AudienceStrategyEntity.java @@ -0,0 +1,66 @@ +package de.tavolio.realm.audience; + +import de.tavolio.realm.RealmEntity; +import de.tavolio.realm.RealmScoped; +import jakarta.persistence.*; + +@Entity +@Table(name = "audience_strategy") +public class AudienceStrategyEntity implements RealmScoped +{ + @Id + private String id; + + private String strategy; + + private String value; + + @OneToOne + @JoinColumn(name = "realm_id") + private RealmEntity realm; + + public String getId() + { + return id; + } + + public AudienceStrategyEntity setId(String id) + { + this.id = id; + return this; + } + + public String getStrategy() + { + return strategy; + } + + public AudienceStrategyEntity setStrategy(String strategy) + { + this.strategy = strategy; + return this; + } + + public String getValue() + { + return value; + } + + public AudienceStrategyEntity setValue(String value) + { + this.value = value; + return this; + } + + @Override + public RealmEntity getRealm() + { + return realm; + } + + public AudienceStrategyEntity setRealm(RealmEntity realm) + { + this.realm = realm; + return this; + } +} diff --git a/src/main/java/de/tavolio/realm/audience/AudienceStrategyRepo.java b/src/main/java/de/tavolio/realm/audience/AudienceStrategyRepo.java new file mode 100644 index 0000000..c5e6c2e --- /dev/null +++ b/src/main/java/de/tavolio/realm/audience/AudienceStrategyRepo.java @@ -0,0 +1,9 @@ +package de.tavolio.realm.audience; + +import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase; +import jakarta.enterprise.context.ApplicationScoped; + +@ApplicationScoped +public class AudienceStrategyRepo implements PanacheRepositoryBase +{ +} diff --git a/src/main/java/de/tavolio/realm/client/ClientCreation.java b/src/main/java/de/tavolio/realm/client/ClientCreation.java new file mode 100644 index 0000000..92d1f19 --- /dev/null +++ b/src/main/java/de/tavolio/realm/client/ClientCreation.java @@ -0,0 +1,5 @@ +package de.tavolio.realm.client; + +public record ClientCreation(String id, String secret) +{ +} diff --git a/src/main/java/de/tavolio/realm/client/ClientEntity.java b/src/main/java/de/tavolio/realm/client/ClientEntity.java new file mode 100644 index 0000000..ac85d8a --- /dev/null +++ b/src/main/java/de/tavolio/realm/client/ClientEntity.java @@ -0,0 +1,83 @@ +package de.tavolio.realm.client; + +import de.tavolio.realm.RealmEntity; +import de.tavolio.realm.RealmScoped; +import de.tavolio.realm.code.CodeEntity; +import jakarta.persistence.*; + +import java.util.List; + +@Entity +@Table(name = "client") +public class ClientEntity implements RealmScoped +{ + @Id + private String id; + + private String secret; + + @Column(name = "redirect_uri") + private String redirectURI; + + @ManyToOne + @JoinColumn(name = "realm_id") + private RealmEntity realm; + + @OneToMany(mappedBy = "client") + private List codes; + + public String getId() + { + return id; + } + + public ClientEntity setId(String id) + { + this.id = id; + return this; + } + + public String getSecret() + { + return secret; + } + + public ClientEntity setSecret(String secret) + { + this.secret = secret; + return this; + } + + public RealmEntity getRealm() + { + return realm; + } + + public ClientEntity setRealm(RealmEntity realm) + { + this.realm = realm; + return this; + } + + public String getRedirectURI() + { + return redirectURI; + } + + public ClientEntity setRedirectURI(String redirectURI) + { + this.redirectURI = redirectURI; + return this; + } + + public List getCodes() + { + return codes; + } + + public ClientEntity setCodes(List codes) + { + this.codes = codes; + return this; + } +} diff --git a/src/main/java/de/tavolio/realm/client/ClientRepo.java b/src/main/java/de/tavolio/realm/client/ClientRepo.java new file mode 100644 index 0000000..ccbec27 --- /dev/null +++ b/src/main/java/de/tavolio/realm/client/ClientRepo.java @@ -0,0 +1,22 @@ +package de.tavolio.realm.client; + +import de.tavolio.realm.RealmEntity; +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 ClientRepo implements PanacheRepositoryBase +{ + public Optional findByRealmAndIdOptional(RealmEntity realm, String id) + { + return find("realm = :realm AND id = :id", Parameters.with("realm", realm).and("id", id)).firstResultOptional(); + } + + public ClientEntity findByRealmAndId(RealmEntity realm, String id) + { + return find("realm = :realm AND id = :id", Parameters.with("realm", realm).and("id", id)).firstResult(); + } +} diff --git a/src/main/java/de/tavolio/realm/client/ClientService.java b/src/main/java/de/tavolio/realm/client/ClientService.java new file mode 100644 index 0000000..70218ce --- /dev/null +++ b/src/main/java/de/tavolio/realm/client/ClientService.java @@ -0,0 +1,59 @@ +package de.tavolio.realm.client; + +import de.tavolio.bootstrap.model.Client; +import de.tavolio.realm.RealmEntity; +import de.tavolio.realm.RealmRepo; +import io.quarkus.elytron.security.common.BcryptUtil; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; +import jakarta.ws.rs.NotFoundException; +import org.apache.commons.lang3.StringUtils; + +import java.util.Map; + +@ApplicationScoped +public class ClientService +{ + @Inject + ClientRepo clientRepo; + + @Inject + RealmRepo realmRepo; + + public ClientEntity findOrCreate(RealmEntity realm, Map.Entry bootstrap) + { + String secret = null; + if (!StringUtils.isBlank(bootstrap.getValue().clientSecret())) + { + secret = BcryptUtil.bcryptHash(System.getenv(bootstrap.getValue().clientSecret())); + } + return clientRepo.findByRealmAndIdOptional(realm, bootstrap.getKey()).orElse(new ClientEntity().setId(bootstrap.getKey()).setSecret(secret).setRedirectURI(bootstrap.getValue().redirectURI()).setRealm(realm)); + } + + public ClientEntity findByIdAndRealm(String clientId, RealmEntity realm) + { + ClientEntity client = clientRepo.findByRealmAndId(realm, clientId); + if (client != null) + { + return client; + } + throw new NotFoundException(); + } + + @Transactional + public ClientEntity create(String realmKey, ClientCreation client) + { + RealmEntity realm = realmRepo.findByKey(realmKey); + if (realm != null) + { + ClientEntity entity = new ClientEntity(); + entity.setId(client.id()); + entity.setSecret(BcryptUtil.bcryptHash(client.secret())); + entity.setRealm(realm); + clientRepo.persist(entity); + return entity; + } + throw new NotFoundException(); + } +} diff --git a/src/main/java/de/tavolio/realm/code/CodeEntity.java b/src/main/java/de/tavolio/realm/code/CodeEntity.java new file mode 100644 index 0000000..4c987ab --- /dev/null +++ b/src/main/java/de/tavolio/realm/code/CodeEntity.java @@ -0,0 +1,86 @@ +package de.tavolio.realm.code; + +import de.tavolio.realm.RealmScoped; +import de.tavolio.realm.user.UserEntity; +import de.tavolio.realm.RealmEntity; +import de.tavolio.realm.client.ClientEntity; +import jakarta.persistence.*; + +import java.time.ZonedDateTime; + +@Entity +@Table(name = "code") +public class CodeEntity implements RealmScoped +{ + @Id + private String id; + + private ZonedDateTime expiresAt; + + @ManyToOne + @JoinColumn(name = "account_id") + private UserEntity account; + + @ManyToOne + @JoinColumn(name = "realm_id") + private RealmEntity realm; + + @ManyToOne + @JoinColumn(name = "client_id") + private ClientEntity client; + + public String getId() + { + return id; + } + + public CodeEntity setId(String id) + { + this.id = id; + return this; + } + + public ZonedDateTime getExpiresAt() + { + return expiresAt; + } + + public CodeEntity setExpiresAt(ZonedDateTime expiresAt) + { + this.expiresAt = expiresAt; + return this; + } + + public UserEntity getAccount() + { + return account; + } + + public CodeEntity setAccount(UserEntity account) + { + this.account = account; + return this; + } + + public RealmEntity getRealm() + { + return realm; + } + + public CodeEntity setRealm(RealmEntity realm) + { + this.realm = realm; + return this; + } + + public ClientEntity getClient() + { + return client; + } + + public CodeEntity setClient(ClientEntity client) + { + this.client = client; + return this; + } +} diff --git a/src/main/java/de/tavolio/realm/code/CodeRepo.java b/src/main/java/de/tavolio/realm/code/CodeRepo.java new file mode 100644 index 0000000..1396a01 --- /dev/null +++ b/src/main/java/de/tavolio/realm/code/CodeRepo.java @@ -0,0 +1,15 @@ +package de.tavolio.realm.code; + +import de.tavolio.realm.RealmEntity; +import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase; +import io.quarkus.panache.common.Parameters; +import jakarta.enterprise.context.ApplicationScoped; + +@ApplicationScoped +public class CodeRepo implements PanacheRepositoryBase +{ + public CodeEntity findByRealmAndId(RealmEntity realm, String id) + { + return find("realm = :realm AND id = :id", Parameters.with("realm", realm).and("id", id)).firstResult(); + } +} diff --git a/src/main/java/de/tavolio/realm/key/KeypairEntity.java b/src/main/java/de/tavolio/realm/key/KeypairEntity.java new file mode 100644 index 0000000..7048906 --- /dev/null +++ b/src/main/java/de/tavolio/realm/key/KeypairEntity.java @@ -0,0 +1,131 @@ +package de.tavolio.realm.key; + +import de.tavolio.realm.RealmEntity; +import de.tavolio.realm.RealmScoped; +import jakarta.persistence.*; + +@Entity +@Table(name = "keypair") +public class KeypairEntity implements RealmScoped +{ + @Id + private String id; + + @Column(columnDefinition = "bytea") + private byte[] privateKey; + + private String type; + + private String use; + + private String alg; + + private String crv; + + private String x; + + private String y; + + @ManyToOne + @JoinColumn(name = "realm_id") + private RealmEntity realm; + + public String getId() + { + return id; + } + + public KeypairEntity setId(String id) + { + this.id = id; + return this; + } + + public byte[] getPrivateKey() + { + return privateKey; + } + + public KeypairEntity setPrivateKey(byte[] privateKey) + { + this.privateKey = privateKey; + return this; + } + + public RealmEntity getRealm() + { + return realm; + } + + public KeypairEntity setRealm(RealmEntity realm) + { + this.realm = realm; + return this; + } + + public String getType() + { + return type; + } + + public KeypairEntity setType(String type) + { + this.type = type; + return this; + } + + public String getUse() + { + return use; + } + + public KeypairEntity setUse(String use) + { + this.use = use; + return this; + } + + public String getAlg() + { + return alg; + } + + public KeypairEntity setAlg(String alg) + { + this.alg = alg; + return this; + } + + public String getCrv() + { + return crv; + } + + public KeypairEntity setCrv(String crv) + { + this.crv = crv; + return this; + } + + public String getX() + { + return x; + } + + public KeypairEntity setX(String x) + { + this.x = x; + return this; + } + + public String getY() + { + return y; + } + + public KeypairEntity setY(String y) + { + this.y = y; + return this; + } +} diff --git a/src/main/java/de/tavolio/realm/key/KeypairRepo.java b/src/main/java/de/tavolio/realm/key/KeypairRepo.java new file mode 100644 index 0000000..f7e6967 --- /dev/null +++ b/src/main/java/de/tavolio/realm/key/KeypairRepo.java @@ -0,0 +1,9 @@ +package de.tavolio.realm.key; + +import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase; +import jakarta.enterprise.context.ApplicationScoped; + +@ApplicationScoped +public class KeypairRepo implements PanacheRepositoryBase +{ +} diff --git a/src/main/java/de/tavolio/realm/key/KeypairService.java b/src/main/java/de/tavolio/realm/key/KeypairService.java new file mode 100644 index 0000000..bd1405d --- /dev/null +++ b/src/main/java/de/tavolio/realm/key/KeypairService.java @@ -0,0 +1,82 @@ +package de.tavolio.realm.key; + +import de.tavolio.realm.RealmEntity; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.transaction.Transactional; + +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.interfaces.ECPublicKey; +import java.security.spec.ECGenParameterSpec; +import java.util.Base64; +import java.util.NoSuchElementException; +import java.util.UUID; + +@ApplicationScoped +public class KeypairService +{ + public static final String EC = "EC"; + public static final String P256 = "secp256r1"; + + @Inject + KeypairRepo keypairRepo; + + @Transactional + public void create(RealmEntity realm, String type, String alg) + { + if ("EC".equals(type)) + { + KeypairEntity keypair = getKeypair(realm); + keypair.setRealm(realm); + realm.getKeys().add(keypair); + keypairRepo.persist(keypair); + return; + } + throw new NoSuchElementException(); + } + + + private KeypairEntity getKeypair(RealmEntity realm) + { + KeyPair pair = generate(); + ECPublicKey publicKey = (ECPublicKey) pair.getPublic(); + byte[] xBytes = toFixedLength(publicKey.getW().getAffineX().toByteArray()); + byte[] yBytes = toFixedLength(publicKey.getW().getAffineY().toByteArray()); + return new KeypairEntity() + .setId(UUID.randomUUID().toString()) + .setRealm(realm) + .setPrivateKey(pair.getPrivate().getEncoded()) + .setType("EC") + .setUse("sig") + .setAlg("ES256") + .setCrv("P-256") + .setX(Base64.getUrlEncoder().withoutPadding().encodeToString(xBytes)) + .setY(Base64.getUrlEncoder().withoutPadding().encodeToString(yBytes)); + } + + private KeyPair generate() + { + try + { + KeyPairGenerator generator = KeyPairGenerator.getInstance(EC); + generator.initialize(new ECGenParameterSpec(P256)); + return generator.generateKeyPair(); + } + catch (Exception e) + { + throw new RuntimeException(); + } + } + + private byte[] toFixedLength(byte[] input) + { + if (input.length > 32) + { + byte[] result = new byte[32]; + System.arraycopy(input, input.length - 32, result, 0, 32); + return result; + } + return input; + } +} diff --git a/src/main/java/de/tavolio/realm/role/RoleEntity.java b/src/main/java/de/tavolio/realm/role/RoleEntity.java new file mode 100644 index 0000000..8039d75 --- /dev/null +++ b/src/main/java/de/tavolio/realm/role/RoleEntity.java @@ -0,0 +1,87 @@ +package de.tavolio.realm.role; + +import de.tavolio.realm.RealmEntity; +import de.tavolio.realm.RealmScoped; +import jakarta.persistence.*; + +import java.util.ArrayList; +import java.util.List; + +@Entity +@Table(name = "role") +public class RoleEntity implements RealmScoped +{ + @Id + private String id; + + @Column(name = "role_name") + private String name; + + @ManyToOne + @JoinColumn(name = "realm_id") + private RealmEntity realm; + + @ElementCollection + @CollectionTable( + name = "permission", + joinColumns = @JoinColumn(name = "role_id") + ) + @Column(name = "permission") + private List permissions = new ArrayList<>(); + + public String getId() + { + return id; + } + + public RoleEntity setId(String id) + { + this.id = id; + return this; + } + + public String getName() + { + return name; + } + + public RoleEntity setName(String name) + { + this.name = name; + return this; + } + + public List getPermissions() + { + return permissions; + } + + public RoleEntity setPermissions(List permissions) + { + this.permissions = permissions; + return this; + } + + public RealmEntity getRealm() + { + return realm; + } + + public RoleEntity setRealm(RealmEntity realm) + { + this.realm = realm; + return this; + } + + public boolean hasPermission(String permission) + { + for (String value : permissions) + { + if (permission.equals(value)) + { + return true; + } + } + return false; + } +} diff --git a/src/main/java/de/tavolio/realm/role/RoleRepo.java b/src/main/java/de/tavolio/realm/role/RoleRepo.java new file mode 100644 index 0000000..7650ba4 --- /dev/null +++ b/src/main/java/de/tavolio/realm/role/RoleRepo.java @@ -0,0 +1,17 @@ +package de.tavolio.realm.role; + +import de.tavolio.realm.RealmEntity; +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 RoleRepo implements PanacheRepositoryBase +{ + public Optional findByNameAndRealmOptional(String name, RealmEntity realm) + { + return find("realm = :realm AND name = :name", Parameters.with("realm", realm).and("name", name)).firstResultOptional(); + } +} diff --git a/src/main/java/de/tavolio/realm/user/UserEntity.java b/src/main/java/de/tavolio/realm/user/UserEntity.java new file mode 100644 index 0000000..d09908f --- /dev/null +++ b/src/main/java/de/tavolio/realm/user/UserEntity.java @@ -0,0 +1,129 @@ +package de.tavolio.realm.user; + +import de.tavolio.realm.RealmScoped; +import de.tavolio.realm.code.CodeEntity; +import de.tavolio.realm.RealmEntity; +import jakarta.persistence.*; + +import java.util.List; +import java.util.UUID; + +@Entity +@Table(name = "user_regular") +public class UserEntity implements RealmScoped +{ + @Id + private String id; + + private String firstname; + + private String lastname; + + private String email; + + @Column(name = "account_password") + private String password; + + @Enumerated(EnumType.STRING) + private UserStatus status; + + @ManyToOne + @JoinColumn(name = "realm_id") + private RealmEntity realm; + + @OneToMany(mappedBy = "account") + private List codes; + + public static UserEntity init() + { + return new UserEntity().setId(UUID.randomUUID().toString()); + } + + public String getId() + { + return id; + } + + public UserEntity setId(String id) + { + this.id = id; + return this; + } + + public String getFirstname() + { + return firstname; + } + + public UserEntity setFirstname(String firstname) + { + this.firstname = firstname; + return this; + } + + public String getLastname() + { + return lastname; + } + + public UserEntity setLastname(String lastname) + { + this.lastname = lastname; + return this; + } + + public String getEmail() + { + return email; + } + + public UserEntity setEmail(String email) + { + this.email = email; + return this; + } + + public String getPassword() + { + return password; + } + + public UserEntity setPassword(String password) + { + this.password = password; + return this; + } + + public UserStatus getStatus() + { + return status; + } + + public UserEntity setStatus(UserStatus status) + { + this.status = status; + return this; + } + + public RealmEntity getRealm() + { + return realm; + } + + public UserEntity setRealm(RealmEntity realm) + { + this.realm = realm; + return this; + } + + public List getCodes() + { + return codes; + } + + public UserEntity setCodes(List codes) + { + this.codes = codes; + return this; + } +} diff --git a/src/main/java/de/tavolio/realm/user/UserMapper.java b/src/main/java/de/tavolio/realm/user/UserMapper.java new file mode 100644 index 0000000..e9c2bf6 --- /dev/null +++ b/src/main/java/de/tavolio/realm/user/UserMapper.java @@ -0,0 +1,20 @@ +package de.tavolio.realm.user; + +import de.tavolio.realm.user.dto.User; +import jakarta.enterprise.context.ApplicationScoped; + +import java.util.List; + +@ApplicationScoped +public class UserMapper +{ + public List map(List accountEntities) + { + return accountEntities.stream().map(this::map).toList(); + } + + public User map(UserEntity userEntity) + { + return new User(userEntity.getId(), userEntity.getFirstname(), userEntity.getLastname(), userEntity.getEmail(), userEntity.getStatus()); + } +} diff --git a/src/main/java/de/tavolio/realm/user/UserRepo.java b/src/main/java/de/tavolio/realm/user/UserRepo.java new file mode 100644 index 0000000..ca81a37 --- /dev/null +++ b/src/main/java/de/tavolio/realm/user/UserRepo.java @@ -0,0 +1,23 @@ +package de.tavolio.realm.user; + +import de.tavolio.realm.RealmEntity; +import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase; +import io.quarkus.panache.common.Parameters; +import jakarta.enterprise.context.ApplicationScoped; + +import java.util.List; +import java.util.Optional; + +@ApplicationScoped +public class UserRepo implements PanacheRepositoryBase +{ + public Optional findOptionalByRealmAndEmail(RealmEntity realm, String email) + { + return find("realm = :realm AND email = :email", Parameters.with("realm", realm).and("email", email)).firstResultOptional(); + } + + public List findByIds(List ids) + { + return list("id IN :ids", Parameters.with("ids", ids)); + } +} diff --git a/src/main/java/de/tavolio/realm/user/UserResource.java b/src/main/java/de/tavolio/realm/user/UserResource.java new file mode 100644 index 0000000..30867bd --- /dev/null +++ b/src/main/java/de/tavolio/realm/user/UserResource.java @@ -0,0 +1,53 @@ +package de.tavolio.realm.user; + +import de.tavolio.realm.user.dto.User; +import de.tavolio.realm.user.dto.UserCreation; +import jakarta.enterprise.context.RequestScoped; +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; + +import java.util.List; +import java.util.Map; + +@RequestScoped +public class UserResource +{ + @Inject + Logger LOG; + + @Inject + UserService userService; + + @POST + public User post(@Valid UserCreation account) + { + User createdUser = userService.create("", account); + LOG.infof("Created account successfully: %s", account.email()); + return createdUser; + } + + @GET + public User get(@PathParam("id") String id) + { + return userService.getUser(id); + } + + @GET + @Path("/{id}") + public User getById(@PathParam("id") String id) + { + return userService.getUser(id); + } + + @POST + @Path("/search") + public Map get(List ids) + { + return userService.findByIds(ids); + } +} diff --git a/src/main/java/de/tavolio/realm/user/UserService.java b/src/main/java/de/tavolio/realm/user/UserService.java new file mode 100644 index 0000000..0a4cdd2 --- /dev/null +++ b/src/main/java/de/tavolio/realm/user/UserService.java @@ -0,0 +1,83 @@ +package de.tavolio.realm.user; + +import de.tavolio.AuthenticationService; +import de.tavolio.realm.user.dto.User; +import de.tavolio.realm.user.dto.UserCreation; +import de.tavolio.realm.RealmEntity; +import de.tavolio.realm.RealmRepo; +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 jakarta.ws.rs.NotFoundException; +import org.jboss.logging.Logger; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@ApplicationScoped +public class UserService +{ + @Inject + Logger LOG; + + @Inject + UserRepo userRepo; + + @Inject + UserMapper userMapper; + + @Inject + AuthenticationService authenticationService; + + @Inject + RealmRepo realmRepo; + + @Transactional + public User create(String realmId, UserCreation account) + { + RealmEntity realm = realmRepo.findByKey(realmId); + if (realm != null) + { + UserEntity userEntity = UserEntity.init(); + userEntity.setEmail(account.email()) + .setFirstname(account.firstname()) + .setLastname(account.lastname()) + .setPassword(BcryptUtil.bcryptHash(account.password())) + .setRealm(realm) + .setStatus(UserStatus.INIT); + userRepo.persist(userEntity); + return userMapper.map(userEntity); + } + throw new NotFoundException(); + } + + public List get() + { + return userMapper.map(userRepo.listAll()); + } + + public User getUser(String id) + { + UserEntity account = authenticationService.requireUser(); + UserEntity requestedAccount = userRepo.findById(id); + if (requestedAccount != null && requestedAccount.getId().equals(account.getId())) + { + return userMapper.map(authenticationService.requireUser()); + } + LOG.errorf("Cannot access account"); + throw new UnauthorizedException(); + } + + public Map findByIds(List ids) + { + Map accounts = new HashMap<>(); + for (UserEntity userEntity : userRepo.findByIds(ids)) + { + accounts.put(userEntity.getId(), userMapper.map(userEntity)); + } + return accounts; + } +} diff --git a/src/main/java/de/tavolio/realm/user/UserStatus.java b/src/main/java/de/tavolio/realm/user/UserStatus.java new file mode 100644 index 0000000..e29edc3 --- /dev/null +++ b/src/main/java/de/tavolio/realm/user/UserStatus.java @@ -0,0 +1,6 @@ +package de.tavolio.realm.user; + +public enum UserStatus +{ + INIT, REGISTERED +} diff --git a/src/main/java/de/tavolio/realm/user/dto/User.java b/src/main/java/de/tavolio/realm/user/dto/User.java new file mode 100644 index 0000000..dc543ea --- /dev/null +++ b/src/main/java/de/tavolio/realm/user/dto/User.java @@ -0,0 +1,7 @@ +package de.tavolio.realm.user.dto; + +import de.tavolio.realm.user.UserStatus; + +public record User(String id, String firstname, String lastname, String email, UserStatus status) +{ +} diff --git a/src/main/java/de/tavolio/account/dto/AccountCreation.java b/src/main/java/de/tavolio/realm/user/dto/UserCreation.java similarity index 77% rename from src/main/java/de/tavolio/account/dto/AccountCreation.java rename to src/main/java/de/tavolio/realm/user/dto/UserCreation.java index b9049dd..214e8fd 100644 --- a/src/main/java/de/tavolio/account/dto/AccountCreation.java +++ b/src/main/java/de/tavolio/realm/user/dto/UserCreation.java @@ -1,9 +1,9 @@ -package de.tavolio.account.dto; +package de.tavolio.realm.user.dto; import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; -public record AccountCreation( +public record UserCreation( @NotBlank String firstname, @NotBlank String lastname, @Email String email, diff --git a/src/main/java/de/tavolio/session/SessionResource.java b/src/main/java/de/tavolio/session/SessionResource.java deleted file mode 100644 index df4f768..0000000 --- a/src/main/java/de/tavolio/session/SessionResource.java +++ /dev/null @@ -1,26 +0,0 @@ -package de.tavolio.session; - -import de.tavolio.session.dto.SessionCreation; -import jakarta.inject.Inject; -import jakarta.validation.Valid; -import jakarta.ws.rs.POST; -import jakarta.ws.rs.Path; -import org.jboss.logging.Logger; - -@Path("/sessions") -public class SessionResource -{ - @Inject - Logger LOG; - - @Inject - SessionService sessionService; - - @POST - public String get(@Valid SessionCreation sessionCreation) - { - String token = sessionService.generateBySessionCreation(sessionCreation); - LOG.infof("Generated token for email %s", sessionCreation.email()); - return token; - } -} diff --git a/src/main/java/de/tavolio/session/SessionService.java b/src/main/java/de/tavolio/session/SessionService.java deleted file mode 100644 index 24500be..0000000 --- a/src/main/java/de/tavolio/session/SessionService.java +++ /dev/null @@ -1,39 +0,0 @@ -package de.tavolio.session; - -import de.tavolio.account.AccountEntity; -import de.tavolio.account.AccountRepo; -import de.tavolio.session.dto.SessionCreation; -import io.quarkus.elytron.security.common.BcryptUtil; -import io.smallrye.jwt.build.Jwt; -import jakarta.enterprise.context.ApplicationScoped; -import jakarta.inject.Inject; -import jakarta.ws.rs.NotFoundException; - -import java.time.ZonedDateTime; -import java.util.Optional; - -@ApplicationScoped -public class SessionService -{ - @Inject - AccountRepo accountRepo; - - public String generateBySessionCreation(SessionCreation sessionCreation) - { - Optional accountEntityOptional = accountRepo.findOptionalByEmail(sessionCreation.email()); - if (accountEntityOptional.isPresent()) - { - AccountEntity accountEntity = accountEntityOptional.get(); - if (BcryptUtil.matches(sessionCreation.password(), accountEntity.getPassword())) - { - return generateToken(accountEntity.getId()); - } - } - throw new NotFoundException(); - } - - private String generateToken(String upn) - { - return Jwt.upn(upn).expiresAt(ZonedDateTime.now().plusYears(1).toInstant()).issuer("https://tavolio.de").sign(); - } -} diff --git a/src/main/java/de/tavolio/superuser/SuperuserEntity.java b/src/main/java/de/tavolio/superuser/SuperuserEntity.java new file mode 100644 index 0000000..ef6dd24 --- /dev/null +++ b/src/main/java/de/tavolio/superuser/SuperuserEntity.java @@ -0,0 +1,37 @@ +package de.tavolio.superuser; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +@Entity +@Table(name = "user_super") +public class SuperuserEntity +{ + @Id + private String id; + + private String password; + + public String getId() + { + return id; + } + + public SuperuserEntity setId(String id) + { + this.id = id; + return this; + } + + public String getPassword() + { + return password; + } + + public SuperuserEntity setPassword(String password) + { + this.password = password; + return this; + } +} \ No newline at end of file diff --git a/src/main/java/de/tavolio/superuser/SuperuserRepo.java b/src/main/java/de/tavolio/superuser/SuperuserRepo.java new file mode 100644 index 0000000..c334057 --- /dev/null +++ b/src/main/java/de/tavolio/superuser/SuperuserRepo.java @@ -0,0 +1,9 @@ +package de.tavolio.superuser; + +import io.quarkus.hibernate.orm.panache.PanacheRepositoryBase; +import jakarta.enterprise.context.ApplicationScoped; + +@ApplicationScoped +public class SuperuserRepo implements PanacheRepositoryBase +{ +} diff --git a/src/main/java/de/tavolio/verify/JwksService.java b/src/main/java/de/tavolio/verify/JwksService.java new file mode 100644 index 0000000..0f130b5 --- /dev/null +++ b/src/main/java/de/tavolio/verify/JwksService.java @@ -0,0 +1,65 @@ +package de.tavolio.verify; + +import de.tavolio.realm.key.KeypairEntity; +import de.tavolio.realm.key.KeypairRepo; +import de.tavolio.realm.RealmEntity; +import de.tavolio.verify.jwks.EcPublicKey; +import de.tavolio.verify.jwks.JwksKey; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; + +import java.util.LinkedList; +import java.util.List; +import java.util.Optional; + +@ApplicationScoped +public class JwksService +{ + @Inject + KeypairRepo keypairRepo; + + public Optional findByKid(String kid) + { + KeypairEntity keypair = keypairRepo.findById(kid); + if (keypair != null) + { + switch (keypair.getType()) + { + case "EC" -> + { + return Optional.of(constructPublicKey(keypair)); + } + case "RSA" -> + { + throw new IllegalArgumentException(); + } + } + } + return Optional.empty(); + } + + public List findByRealm(RealmEntity realm) + { + List result = new LinkedList<>(); + for (KeypairEntity keypair : realm.getKeys()) + { + switch (keypair.getType()) + { + case "EC" -> + { + result.add(constructPublicKey(keypair)); + } + case "RSA" -> + { + throw new IllegalArgumentException(); + } + } + } + return result; + } + + private EcPublicKey constructPublicKey(KeypairEntity entity) + { + return new EcPublicKey(entity.getType(), entity.getId(), entity.getUse(), entity.getUse(), entity.getCrv(), entity.getX(), entity.getY()); + } +} diff --git a/src/main/java/de/tavolio/verify/TokenVerifier.java b/src/main/java/de/tavolio/verify/TokenVerifier.java new file mode 100644 index 0000000..1ea9751 --- /dev/null +++ b/src/main/java/de/tavolio/verify/TokenVerifier.java @@ -0,0 +1,81 @@ +package de.tavolio.verify; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import de.tavolio.verify.jwks.JwksKey; +import io.quarkus.security.AuthenticationFailedException; +import io.quarkus.security.identity.AuthenticationRequestContext; +import io.quarkus.security.identity.IdentityProvider; +import io.quarkus.security.identity.SecurityIdentity; +import io.quarkus.security.identity.request.TokenAuthenticationRequest; +import io.quarkus.security.runtime.QuarkusPrincipal; +import io.quarkus.security.runtime.QuarkusSecurityIdentity; +import io.smallrye.mutiny.Uni; +import io.smallrye.mutiny.infrastructure.Infrastructure; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.context.control.ActivateRequestContext; +import jakarta.inject.Inject; + +import java.security.PublicKey; +import java.util.Base64; +import java.util.Map; +import java.util.Optional; + +@ApplicationScoped +public class TokenVerifier implements IdentityProvider +{ + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + @Inject + JwksService jwksService; + + @Override + public Class getRequestType() + { + return TokenAuthenticationRequest.class; + } + + @Override + @ActivateRequestContext + public Uni authenticate(TokenAuthenticationRequest tokenAuthenticationRequest, AuthenticationRequestContext authenticationRequestContext) + { + String raw = tokenAuthenticationRequest.getToken().getToken(); + try + { + Object kid = getHeader(raw).get("kid"); + if (kid instanceof String keyId) + { + return Uni.createFrom().item(() -> { + Optional keypair = jwksService.findByKid(keyId); + if (keypair.isPresent()) + { + PublicKey publicKey = keypair.get().toPublicKey(); + return (SecurityIdentity) QuarkusSecurityIdentity.builder().setPrincipal(new QuarkusPrincipal("")).build(); + } + throw new AuthenticationFailedException(); + }).runSubscriptionOn(Infrastructure.getDefaultWorkerPool()); + } + return Uni.createFrom().nullItem(); + } + catch (JsonProcessingException e) + { + return Uni.createFrom().failure(new AuthenticationFailedException()); + } + } + + private Map getHeader(String raw) throws JsonProcessingException + { + return OBJECT_MAPPER.readValue(new String(Base64.getDecoder().decode(section(raw))), new TypeReference>(){}); + } + + private String section(String raw) + { + String[] sections = raw.split("\\."); + if (sections.length == 3) + { + return sections[0]; + } + throw new RuntimeException(); + } +} \ No newline at end of file diff --git a/src/main/java/de/tavolio/verify/jwks/EcPublicKey.java b/src/main/java/de/tavolio/verify/jwks/EcPublicKey.java new file mode 100644 index 0000000..2ffc687 --- /dev/null +++ b/src/main/java/de/tavolio/verify/jwks/EcPublicKey.java @@ -0,0 +1,64 @@ +package de.tavolio.verify.jwks; + +import java.math.BigInteger; +import java.security.AlgorithmParameters; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.spec.*; +import java.util.Base64; + +public class EcPublicKey extends JwksKey +{ + private final String crv; + private final String x; + private final String y; + + public EcPublicKey(String kty, String kid, String use, String alg, String crv, String x, String y) + { + super(kty, kid, use, alg); + this.crv = crv; + this.x = x; + this.y = y; + } + + public String getCrv() + { + return crv; + } + + public String getX() + { + return x; + } + + public String getY() + { + return y; + } + + public PublicKey toPublicKey() + { + byte[] xBytes = Base64.getDecoder().decode(this.x); + byte[] yBytes = Base64.getDecoder().decode(this.y); + + BigInteger x = new BigInteger(1, xBytes); + BigInteger y = new BigInteger(1, yBytes); + + ECPoint point = new ECPoint(x, y); + + try + { + AlgorithmParameters params = AlgorithmParameters.getInstance("EC"); + params.init(new ECGenParameterSpec("secp256r1")); + ECParameterSpec spec = params.getParameterSpec(ECParameterSpec.class); + + ECPublicKeySpec keySpec = new ECPublicKeySpec(point, spec); + return KeyFactory.getInstance("EC").generatePublic(keySpec); + } + catch (NoSuchAlgorithmException | InvalidParameterSpecException | InvalidKeySpecException e) + { + throw new RuntimeException(e); + } + } +} diff --git a/src/main/java/de/tavolio/verify/jwks/JwksKey.java b/src/main/java/de/tavolio/verify/jwks/JwksKey.java new file mode 100644 index 0000000..85ee8f9 --- /dev/null +++ b/src/main/java/de/tavolio/verify/jwks/JwksKey.java @@ -0,0 +1,41 @@ +package de.tavolio.verify.jwks; + +import java.security.PublicKey; + +public abstract class JwksKey +{ + private final String kty; + private final String kid; + private final String use; + private final String alg; + + public JwksKey(String kty, String kid, String use, String alg) + { + this.kty = kty; + this.kid = kid; + this.use = use; + this.alg = alg; + } + + public String getKty() + { + return kty; + } + + public String getKid() + { + return kid; + } + + public String getUse() + { + return use; + } + + public String getAlg() + { + return alg; + } + + public abstract PublicKey toPublicKey(); +} diff --git a/src/main/java/de/tavolio/verify/jwks/RsaPublicKey.java b/src/main/java/de/tavolio/verify/jwks/RsaPublicKey.java new file mode 100644 index 0000000..876941f --- /dev/null +++ b/src/main/java/de/tavolio/verify/jwks/RsaPublicKey.java @@ -0,0 +1,5 @@ +package de.tavolio.verify.jwks; + +public class RsaPublicKey +{ +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 2ce6fe0..149839a 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -3,33 +3,50 @@ quarkus.http.port=8089 quarkus.http.test-port=9089 %dev.quarkus.http.host=0.0.0.0 +quarkus.http.cors.enabled=true +%dev.quarkus.http.cors.origins=/.*/ + # JWT %prod.smallrye.jwt.sign.key.location=${PRIVATE_KEY_LOCATION} %prod.mp.jwt.verify.publickey.location=${PUBLIC_KEY_LOCATION} -%dev.smallrye.jwt.sign.key.location=private.key -%dev.mp.jwt.verify.publickey.location=public.crt mp.jwt.verify.issuer=https://tavolio.de +%dev.smallrye.jwt.verify.key.location=/home/andreas/Documents/dev/publicKey.pem +%dev.smallrye.jwt.sign.key.location=/home/andreas/Documents/dev/privateKey.pem + # Postgres prod.quarkus.hibernate-orm.validate-in-dev-mode=false quarkus.hibernate-orm.schema-management.strategy=none +%test,dev.quarkus.hibernate-orm.schema-management.strategy=drop-and-create quarkus.datasource.db-kind=postgresql -%dev.quarkus.datasource.username=postgres -%dev.quarkus.datasource.password=${DB_PASSWORD} -%dev.quarkus.datasource.jdbc.url=jdbc:postgresql://localhost:5432/postgres?currentSchema=iam +%dev,test.quarkus.datasource.username=postgres +%dev,test.quarkus.datasource.password=postgres +%dev,test.quarkus.datasource.jdbc.url=jdbc:postgresql://localhost:5432/postgres?currentSchema=auth %prod.quarkus.datasource.username=${DB_USER} %prod.quarkus.datasource.password=${DB_PASSWORD} %prod.quarkus.datasource.jdbc.url=jdbc:postgresql://${DB_HOST}:${DB_PORT}/${DB_DATABASE}?currentSchema=${DB_SCHEMA} # Flyway +quarkus.flyway.enabled=false %test.quarkus.flyway.clean-at-start=true %dev.quarkus.flyway.clean-at-start=true %dev.quarkus.flyway.locations=db/migration,db/dev +%test,dev.quarkus.flyway.migrate-at-start=false quarkus.flyway.migrate-at-start=true # IAM Superuser %test,dev.iam.user.name=tavolio -%test,dev.iam.user.password=tavolio \ No newline at end of file +%test,dev.iam.user.password=tavolio + + +quarkus.http.access-log.enabled=true +quarkus.http.auth.basic=true +dev.dinauer.idp.origin=http://localhost:8089 + +%dev.dev.dinauer.idp.superuser.username=admin +%dev.dev.dinauer.idp.superuser.password=pw + +quarkus.log.level=DEBUG \ No newline at end of file diff --git a/src/main/resources/bootstrap.yaml b/src/main/resources/bootstrap.yaml new file mode 100644 index 0000000..d583b11 --- /dev/null +++ b/src/main/resources/bootstrap.yaml @@ -0,0 +1,22 @@ +realms: + maven: + name: My Bootstrap Realm + audience: + strategy: realm + key: + type: EC + alg: P256 + clients: + backend: + client-secret: MY_SECRET_ENV + permissions: + - USER:VIEW + frontend: + redirect-uri: http://localhost:8080/callback + accounts: + - email: andreas.j.dinauer@gmail.com + first-name: Andreas + last-name: Dinauer + password-plain: pw + roles: + - USER:VIEW \ No newline at end of file diff --git a/src/main/resources/db/dev/V9999__init.sql b/src/main/resources/db/dev/V9999__init.sql index 2d0de2c..a1a13e0 100755 --- a/src/main/resources/db/dev/V9999__init.sql +++ b/src/main/resources/db/dev/V9999__init.sql @@ -1,5 +1,2 @@ INSERT INTO account (id, firstname, lastname, email, account_password, status) -VALUES ('66b261fe-4c5a-4728-9857-67717f02d4e1', 'Andreas', 'Dinauer', 'andreas.j.dinauer@gmail.com', '$2a$12$cdrzIY4sMFAXiz29uo9Ul.MPy0RN0FGS2yjVzb5BTe6bSijn4eGQy', 'REGISTERED'); - -INSERT INTO membership(id, tenant_type, tenant_id, member_role, member_since, account_id) -VALUES ('cd20d271-6e76-48c4-9cfb-a67f8892d46c', 'ORGANISATION', 'b3912be6-7503-4a13-b8ab-5d65af036742', 'OWNER', '2025-08-20 15:30:12.123456+02', '66b261fe-4c5a-4728-9857-67717f02d4e1'); \ No newline at end of file +VALUES ('66b261fe-4c5a-4728-9857-67717f02d4e1', 'Andreas', 'Dinauer', 'andreas.j.dinauer@gmail.com', '$2a$12$cdrzIY4sMFAXiz29uo9Ul.MPy0RN0FGS2yjVzb5BTe6bSijn4eGQy', 'REGISTERED'); \ No newline at end of file diff --git a/src/main/resources/db/migration/V1.0.1__init.sql b/src/main/resources/db/migration/V1.0.1__init.sql index 2c66664..1fff205 100755 --- a/src/main/resources/db/migration/V1.0.1__init.sql +++ b/src/main/resources/db/migration/V1.0.1__init.sql @@ -7,17 +7,4 @@ CREATE TABLE account ( status VARCHAR(255) NULL, CONSTRAINT account_pkey PRIMARY KEY (id), CONSTRAINT account_status_check CHECK (((status)::text = ANY ((ARRAY['INIT'::character varying, 'REGISTERED'::character varying])::text[]))) -); - -CREATE TABLE membership ( - member_since TIMESTAMP(6) WITH TIME ZONE NOT NULL, - account_id VARCHAR(255) NOT NULL, - id VARCHAR(255) NOT NULL, - member_role VARCHAR(255) NOT NULL, - tenant_id VARCHAR(255) NOT NULL, - tenant_type VARCHAR(255) NOT NULL, - CONSTRAINT membership_member_role_check CHECK (((member_role)::text = ANY ((ARRAY['OWNER'::character varying, 'ADMIN'::character varying, 'MEMBER'::character varying])::text[]))), - CONSTRAINT membership_pkey PRIMARY KEY (id), - CONSTRAINT membership_tenant_type_check CHECK (((tenant_type)::text = ANY ((ARRAY['ORGANISATION'::character varying, 'RESTAURANT'::character varying])::text[]))), - CONSTRAINT fkd1yliqdvipm4yvq2tulbktpg6 FOREIGN KEY (account_id) REFERENCES account(id) ); \ No newline at end of file diff --git a/src/main/resources/import.sql b/src/main/resources/import.sql new file mode 100644 index 0000000..e69de29 diff --git a/src/test/java/de/tavolio/account/AccountResourceTest.java b/src/test/java/de/tavolio/account/UserResourceTest.java similarity index 88% rename from src/test/java/de/tavolio/account/AccountResourceTest.java rename to src/test/java/de/tavolio/account/UserResourceTest.java index c5d351f..85314a5 100644 --- a/src/test/java/de/tavolio/account/AccountResourceTest.java +++ b/src/test/java/de/tavolio/account/UserResourceTest.java @@ -1,5 +1,7 @@ package de.tavolio.account; +import de.tavolio.realm.user.UserEntity; +import de.tavolio.realm.user.UserRepo; import de.tavolio.utils.Database; import io.quarkus.elytron.security.common.BcryptUtil; import io.quarkus.test.junit.QuarkusTest; @@ -15,13 +17,13 @@ import static io.restassured.RestAssured.when; import static org.junit.jupiter.api.Assertions.*; @QuarkusTest -public class AccountResourceTest +public class UserResourceTest { @Inject Database database; @Inject - AccountRepo accountRepo; + UserRepo userRepo; @AfterEach void afterEach() @@ -49,7 +51,7 @@ public class AccountResourceTest .then() .statusCode(200); - assertEquals(1, accountRepo.count()); + assertEquals(1, userRepo.count()); } @Test @@ -58,13 +60,13 @@ public class AccountResourceTest { // given database.setup(() -> { - AccountEntity account = new AccountEntity() + UserEntity account = new UserEntity() .setId("66609092-6c98-4466-af52-9a3e9d633108") .setFirstname("Andreas") .setLastname("Dinauer") .setEmail("andreas.j.dinauer@gmail.com") .setPassword(BcryptUtil.bcryptHash("pw")); - accountRepo.persist(account); + userRepo.persist(account); }); // when diff --git a/src/test/java/de/tavolio/realm/RealmServiceTest.java b/src/test/java/de/tavolio/realm/RealmServiceTest.java new file mode 100644 index 0000000..4e3e632 --- /dev/null +++ b/src/test/java/de/tavolio/realm/RealmServiceTest.java @@ -0,0 +1,19 @@ +package de.tavolio.realm; + +import io.quarkus.test.junit.QuarkusTest; +import jakarta.inject.Inject; +import org.junit.jupiter.api.Test; + +@QuarkusTest +public class RealmServiceTest +{ + @Inject + RealmService realmService; + + @Test + void test() + { + realmService.create(new RealmCreation("Test Realm", "test-realm")); + System.out.println("Press any key to continue..."); + } +}