🚧 Changes

This commit is contained in:
Andreas Dinauer 2026-04-04 19:13:40 +02:00
parent cf0b994eed
commit 483d34457f
43 changed files with 778 additions and 210 deletions

View File

@ -0,0 +1,79 @@
package de.tavolio.auth.httpmechanism;
import de.tavolio.auth.request.JWTRequest;
import de.tavolio.auth.request.UsernamePasswordRequest;
import io.quarkus.security.AuthenticationFailedException;
import io.quarkus.security.identity.IdentityProviderManager;
import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.vertx.http.runtime.security.ChallengeData;
import io.quarkus.vertx.http.runtime.security.HttpAuthenticationMechanism;
import io.quarkus.vertx.http.runtime.security.HttpSecurityUtils;
import io.smallrye.mutiny.Uni;
import io.vertx.core.http.HttpHeaders;
import io.vertx.ext.web.RoutingContext;
import jakarta.annotation.Priority;
import jakarta.enterprise.context.ApplicationScoped;
import org.apache.commons.lang3.Strings;
import java.util.Base64;
@Priority(50)
@ApplicationScoped
public class CustomHttpAuthenticationMechanism implements HttpAuthenticationMechanism
{
private static final String BASIC = "Basic ";
private static final String BEARER = "Bearer ";
@Override
public Uni<SecurityIdentity> authenticate(RoutingContext context, IdentityProviderManager identityProviderManager)
{
String realmKey = getRealmKey(context.request().uri());
context.put(CustomHttpAuthenticationMechanism.class.getName(), this);
String auth = context.request().headers().get(HttpHeaders.AUTHORIZATION);
if (Strings.CI.startsWith(auth, BASIC))
{
String decoded = new String(Base64.getDecoder().decode(Strings.CI.removeStart(auth, BASIC)));
String[] sections = decoded.split(":");
if (sections.length == 2)
{
UsernamePasswordRequest request = new UsernamePasswordRequest(realmKey, sections[0], sections[1]);
HttpSecurityUtils.setRoutingContextAttribute(request, context);
return identityProviderManager.authenticate(request);
}
return Uni.createFrom().failure(new AuthenticationFailedException());
}
if (Strings.CI.startsWith(auth, BEARER))
{
JWTRequest request = new JWTRequest(realmKey, Strings.CI.removeStart(auth, BEARER));
HttpSecurityUtils.setRoutingContextAttribute(request, context);
return identityProviderManager.authenticate(request);
}
return Uni.createFrom().nullItem();
}
private String getRealmKey(String uri)
{
String[] sections = uri.split("/");
for (int i = 0; i < sections.length; i++)
{
if (sections[i].equals("realms"))
{
try
{
return sections[i + 1];
}
catch (Exception e)
{
return null;
}
}
}
return null;
}
@Override
public Uni<ChallengeData> getChallenge(RoutingContext context)
{
return null;
}
}

View File

@ -1,7 +1,8 @@
package de.tavolio.oidc.identityproviders; package de.tavolio.auth.identityproviders;
import de.tavolio.Role; import de.tavolio.Role;
import de.tavolio.realm.user.UserRepo; import de.tavolio.auth.request.UsernamePasswordRequest;
import de.tavolio.realm.RealmService;
import de.tavolio.realm.client.ClientEntity; import de.tavolio.realm.client.ClientEntity;
import de.tavolio.realm.client.ClientRepo; import de.tavolio.realm.client.ClientRepo;
import de.tavolio.superuser.SuperuserEntity; import de.tavolio.superuser.SuperuserEntity;
@ -11,7 +12,6 @@ import io.quarkus.security.AuthenticationFailedException;
import io.quarkus.security.identity.AuthenticationRequestContext; import io.quarkus.security.identity.AuthenticationRequestContext;
import io.quarkus.security.identity.IdentityProvider; import io.quarkus.security.identity.IdentityProvider;
import io.quarkus.security.identity.SecurityIdentity; import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.security.identity.request.UsernamePasswordAuthenticationRequest;
import io.quarkus.security.runtime.QuarkusPrincipal; import io.quarkus.security.runtime.QuarkusPrincipal;
import io.quarkus.security.runtime.QuarkusSecurityIdentity; import io.quarkus.security.runtime.QuarkusSecurityIdentity;
import io.smallrye.mutiny.Uni; import io.smallrye.mutiny.Uni;
@ -23,7 +23,7 @@ import jakarta.inject.Inject;
@ApplicationScoped @ApplicationScoped
@Priority(1) @Priority(1)
public class BasicAuthIdentityProvider implements IdentityProvider<UsernamePasswordAuthenticationRequest> public class BasicAuthIdentityProvider implements IdentityProvider<UsernamePasswordRequest>
{ {
@Inject @Inject
ClientRepo clientRepo; ClientRepo clientRepo;
@ -31,19 +31,22 @@ public class BasicAuthIdentityProvider implements IdentityProvider<UsernamePassw
@Inject @Inject
SuperuserRepo superuserRepo; SuperuserRepo superuserRepo;
@Inject
RealmService realmService;
@Override @Override
public Class<UsernamePasswordAuthenticationRequest> getRequestType() public Class<UsernamePasswordRequest> getRequestType()
{ {
return UsernamePasswordAuthenticationRequest.class; return UsernamePasswordRequest.class;
} }
@Override @Override
@ActivateRequestContext @ActivateRequestContext
public Uni<SecurityIdentity> authenticate(UsernamePasswordAuthenticationRequest usernamePasswordAuthenticationRequest, AuthenticationRequestContext authenticationRequestContext) public Uni<SecurityIdentity> authenticate(UsernamePasswordRequest usernamePasswordAuthenticationRequest, AuthenticationRequestContext authenticationRequestContext)
{ {
return Uni.createFrom().item(() -> { return Uni.createFrom().item(() -> {
String username = usernamePasswordAuthenticationRequest.getUsername(); String username = usernamePasswordAuthenticationRequest.getUsername();
String credential = new String(usernamePasswordAuthenticationRequest.getPassword().getPassword()); String credential = usernamePasswordAuthenticationRequest.getPassword();
SuperuserEntity superuser = superuserRepo.findById(username); SuperuserEntity superuser = superuserRepo.findById(username);
if (superuser != null) if (superuser != null)
{ {
@ -52,7 +55,7 @@ public class BasicAuthIdentityProvider implements IdentityProvider<UsernamePassw
return (SecurityIdentity) QuarkusSecurityIdentity.builder().setPrincipal(new QuarkusPrincipal(username)).addRole(Role.ROOT.toString()).build(); return (SecurityIdentity) QuarkusSecurityIdentity.builder().setPrincipal(new QuarkusPrincipal(username)).addRole(Role.ROOT.toString()).build();
} }
} }
ClientEntity client = clientRepo.findById(username); ClientEntity client = clientRepo.findByRealmAndName(realmService.requireByKey(usernamePasswordAuthenticationRequest.getRealmKey()), username);
if (client != null) if (client != null)
{ {
if (BcryptUtil.matches(credential, client.getSecret())) if (BcryptUtil.matches(credential, client.getSecret()))

View File

@ -0,0 +1,101 @@
package de.tavolio.auth.identityproviders;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import de.tavolio.Role;
import de.tavolio.auth.utils.JwtUtils;
import de.tavolio.oidc.IssuerService;
import de.tavolio.auth.request.JWTRequest;
import de.tavolio.realm.RealmEntity;
import de.tavolio.realm.client.ClientEntity;
import de.tavolio.realm.client.ClientService;
import de.tavolio.realm.key.KeypairEntity;
import de.tavolio.realm.key.KeypairRepo;
import de.tavolio.realm.user.Permission;
import de.tavolio.verify.JwksService;
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.runtime.QuarkusPrincipal;
import io.quarkus.security.runtime.QuarkusSecurityIdentity;
import io.smallrye.jwt.auth.principal.DefaultJWTParser;
import io.smallrye.jwt.auth.principal.JWTAuthContextInfo;
import io.smallrye.jwt.auth.principal.ParseException;
import io.smallrye.mutiny.Uni;
import io.smallrye.mutiny.infrastructure.Infrastructure;
import jakarta.annotation.Priority;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.context.control.ActivateRequestContext;
import jakarta.inject.Inject;
import org.eclipse.microprofile.jwt.JsonWebToken;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.security.PublicKey;
import java.util.*;
@ApplicationScoped
@Priority(1)
public class ClientIdentityProvider implements IdentityProvider<JWTRequest>
{
private static final Logger LOG = LoggerFactory.getLogger(ClientIdentityProvider.class);
@Inject
JwksService jwksService;
@Inject
KeypairRepo keypairRepo;
@Inject
IssuerService issuerService;
@Inject
ClientService clientService;
@Override
public Class<JWTRequest> getRequestType()
{
return JWTRequest.class;
}
@Override
@ActivateRequestContext
public Uni<SecurityIdentity> authenticate(JWTRequest tokenAuthenticationRequest, AuthenticationRequestContext authenticationRequestContext)
{
String raw = tokenAuthenticationRequest.getToken();
return Uni.createFrom().item(() -> {
String kid = JwtUtils.parseHeader(raw).getKid();
KeypairEntity keypairEntity = keypairRepo.findById(kid);
if (keypairEntity != null)
{
PublicKey publicKey = jwksService.findByKid(keypairEntity).toPublicKey();
RealmEntity realm = keypairEntity.getRealm();
try
{
JsonWebToken token = new DefaultJWTParser(getContextForRealm(realm.getKey())).verify(raw, publicKey);
ClientEntity client = clientService.requireByNameAndRealm(token.getName(), realm);
if (client != null)
{
Set<Permission> permissions = new HashSet<>(client.getPermissions());
return (SecurityIdentity) QuarkusSecurityIdentity.builder().setPrincipal(new QuarkusPrincipal(client.getId())).addRole(Role.CLIENT.toString()).addAttribute("permissions", permissions).build();
}
}
catch (ParseException e)
{
throw new RuntimeException(e);
}
}
LOG.error("Cannot find key with id {}", kid);
throw new AuthenticationFailedException();
}).runSubscriptionOn(Infrastructure.getDefaultWorkerPool());
}
private JWTAuthContextInfo getContextForRealm(String realmKey)
{
JWTAuthContextInfo info = new JWTAuthContextInfo();
info.setIssuedBy(issuerService.getIssuer(realmKey));
return info;
}
}

View File

@ -0,0 +1,39 @@
package de.tavolio.auth.request;
import io.quarkus.security.identity.request.AuthenticationRequest;
import java.util.Map;
public class JWTRequest extends RealmScoped implements AuthenticationRequest
{
private final String token;
public JWTRequest(String realmKey, String token)
{
super(realmKey);
this.token = token;
}
public String getToken()
{
return token;
}
@Override
public <T> T getAttribute(String name)
{
return null;
}
@Override
public void setAttribute(String name, Object value)
{
}
@Override
public Map<String, Object> getAttributes()
{
return Map.of();
}
}

View File

@ -0,0 +1,16 @@
package de.tavolio.auth.request;
public class RealmScoped
{
private final String realmKey;
public RealmScoped(String realmKey)
{
this.realmKey = realmKey;
}
public String getRealmKey()
{
return realmKey;
}
}

View File

@ -0,0 +1,46 @@
package de.tavolio.auth.request;
import io.quarkus.security.identity.request.AuthenticationRequest;
import java.util.Map;
public class UsernamePasswordRequest extends RealmScoped implements AuthenticationRequest
{
private final String username;
private final String password;
public UsernamePasswordRequest(String realmKey, String username, String password)
{
super(realmKey);
this.username = username;
this.password = password;
}
public String getUsername()
{
return username;
}
public String getPassword()
{
return password;
}
@Override
public <T> T getAttribute(String name)
{
return null;
}
@Override
public void setAttribute(String name, Object value)
{
}
@Override
public Map<String, Object> getAttributes()
{
return Map.of();
}
}

View File

@ -0,0 +1,17 @@
package de.tavolio.auth.utils;
public class JwtHeader
{
public String kid;
public String getKid()
{
return kid;
}
public JwtHeader setKid(String kid)
{
this.kid = kid;
return this;
}
}

View File

@ -0,0 +1,32 @@
package de.tavolio.auth.utils;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.quarkus.security.AuthenticationFailedException;
import java.util.Base64;
import java.util.Map;
public class JwtUtils
{
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
public static JwtHeader parseHeader(String token)
{
String[] sections = token.split("\\.");
if (sections.length == 3)
{
try
{
Map<String, Object> result = OBJECT_MAPPER.readValue(new String(Base64.getUrlDecoder().decode(sections[0])), new TypeReference<Map<String, Object>>(){});
return new JwtHeader().setKid((String) result.get("kid"));
}
catch (JsonProcessingException e)
{
throw new RuntimeException(e);
}
}
throw new AuthenticationFailedException();
}
}

View File

@ -0,0 +1,6 @@
package de.tavolio.bootstrap;
public interface Bootstrapper<O>
{
void run(String key, O object);
}

View File

@ -39,7 +39,8 @@ public class ClientBootstrapper
.setSecret(Credentials.resolve(client.secret())) .setSecret(Credentials.resolve(client.secret()))
.setRedirectURI(client.redirectURI()) .setRedirectURI(client.redirectURI())
.setRealm(realm) .setRealm(realm)
.setPermissions(client.permissions()); .setPermissions(client.permissions())
.setAllowedGrants(client.allowedGrants());
clientRepo.persist(entity); clientRepo.persist(entity);
} }
} }

View File

@ -4,7 +4,6 @@ import de.tavolio.bootstrap.model.Audience;
import de.tavolio.bootstrap.model.Realm; import de.tavolio.bootstrap.model.Realm;
import de.tavolio.realm.RealmEntity; import de.tavolio.realm.RealmEntity;
import de.tavolio.realm.RealmRepo; import de.tavolio.realm.RealmRepo;
import de.tavolio.realm.RealmService;
import de.tavolio.realm.audience.AudienceStrategyEntity; import de.tavolio.realm.audience.AudienceStrategyEntity;
import de.tavolio.realm.audience.AudienceStrategyRepo; import de.tavolio.realm.audience.AudienceStrategyRepo;
import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.context.ApplicationScoped;
@ -17,9 +16,6 @@ import java.util.UUID;
@ApplicationScoped @ApplicationScoped
public class RealmBootstrapService public class RealmBootstrapService
{ {
@Inject
RealmService realmService;
@Inject @Inject
KeyBootstrapper keyBootstrapper; KeyBootstrapper keyBootstrapper;
@ -34,6 +30,8 @@ public class RealmBootstrapService
@Inject @Inject
AudienceStrategyRepo audienceStrategyRepo; AudienceStrategyRepo audienceStrategyRepo;
@Inject
RoleBootstrapper roleBootstrapper;
public void bootstrap(Map.Entry<String, Realm> realmEntry) public void bootstrap(Map.Entry<String, Realm> realmEntry)
{ {
@ -42,6 +40,7 @@ public class RealmBootstrapService
keyBootstrapper.bootstrap(realm, realmValue.key()); keyBootstrapper.bootstrap(realm, realmValue.key());
clientBootstrapper.bootstrap(realm, realmValue.clients()); clientBootstrapper.bootstrap(realm, realmValue.clients());
userBootstrapper.bootstrap(realm, realmValue.users()); userBootstrapper.bootstrap(realm, realmValue.users());
roleBootstrapper.run(realm, realmValue.roles());
} }
public RealmEntity run(String key, Realm realm) public RealmEntity run(String key, Realm realm)
@ -53,7 +52,7 @@ public class RealmBootstrapService
} }
else else
{ {
RealmEntity newRealm = new RealmEntity().setKey(key).setName(realm.name()); RealmEntity newRealm = new RealmEntity().setKey(key).setName(realm.name()).setLifetime(Optional.ofNullable(realm.lifetime()).orElse(60 * 30));
realmRepo.persist(newRealm); realmRepo.persist(newRealm);
bootstrapAudience(newRealm, realm.audience()); bootstrapAudience(newRealm, realm.audience());
return newRealm; return newRealm;

View File

@ -0,0 +1,38 @@
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.List;
import java.util.UUID;
@ApplicationScoped
public class RoleBootstrapper
{
@Inject
RoleRepo roleRepo;
public void run(RealmEntity realm, List<Role> roles)
{
for (Role role : roles)
{
run(realm, role);
}
}
private void run(RealmEntity realm, Role role)
{
RoleEntity existingRole = roleRepo.findByRealmAndName(realm, role.name());
if (existingRole == null)
{
RoleEntity newRole = new RoleEntity().setId(UUID.randomUUID().toString());
newRole.setName(role.name());
newRole.setRealm(realm);
roleRepo.persist(newRole);
}
}
}

View File

@ -1,12 +1,13 @@
package de.tavolio.bootstrap.model; package de.tavolio.bootstrap.model;
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty;
import de.tavolio.realm.client.Grant;
import de.tavolio.realm.user.Permission; import de.tavolio.realm.user.Permission;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
public record Client(@JsonProperty("secret") Credential secret, @JsonProperty("redirect-uri") String redirectURI, @JsonProperty("permissions") Set<String> permissionList) public record Client(@JsonProperty("secret") Credential secret, @JsonProperty("redirect-uri") String redirectURI, @JsonProperty("permissions") Set<String> permissionList, @JsonProperty("allowed-grants") Set<Grant> allowedGrants)
{ {
public Set<Permission> permissions() public Set<Permission> permissions()
{ {

View File

@ -3,6 +3,6 @@ package de.tavolio.bootstrap.model;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
public record Realm(String name, Key key, Audience audience, Map<String, Client> clients, Map<String, Role> roles, List<String> permissions, List<User> users) public record Realm(String name, Key key, Integer lifetime, Audience audience, Map<String, Client> clients, List<Role> roles, List<String> permissions, List<User> users)
{ {
} }

View File

@ -1,7 +1,5 @@
package de.tavolio.bootstrap.model; package de.tavolio.bootstrap.model;
import java.util.List; public record Role(String name)
public record Role(List<String> permissions)
{ {
} }

View File

@ -1,30 +0,0 @@
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);
}
}

View File

@ -9,8 +9,9 @@ import de.tavolio.realm.RealmEntity;
import de.tavolio.realm.RealmService; import de.tavolio.realm.RealmService;
import de.tavolio.realm.client.ClientEntity; import de.tavolio.realm.client.ClientEntity;
import de.tavolio.realm.client.ClientService; import de.tavolio.realm.client.ClientService;
import de.tavolio.realm.client.Grant;
import io.quarkus.security.Authenticated; import io.quarkus.security.Authenticated;
import jakarta.annotation.security.RolesAllowed; import io.quarkus.security.identity.SecurityIdentity;
import jakarta.enterprise.context.RequestScoped; import jakarta.enterprise.context.RequestScoped;
import jakarta.inject.Inject; import jakarta.inject.Inject;
import jakarta.transaction.Transactional; import jakarta.transaction.Transactional;
@ -46,6 +47,9 @@ public class OidcResource
@Inject @Inject
RealmService realmService; RealmService realmService;
@Inject
SecurityIdentity identity;
@GET @GET
@Path("/certs") @Path("/certs")
public Map<String, Object> certs() public Map<String, Object> certs()
@ -58,7 +62,7 @@ public class OidcResource
public Response auth(@QueryParam("client_id") String clientId, @FormParam("email") String email, @FormParam("password") String password) public Response auth(@QueryParam("client_id") String clientId, @FormParam("email") String email, @FormParam("password") String password)
{ {
RealmEntity realm = realmService.requireByKey(realmKey); RealmEntity realm = realmService.requireByKey(realmKey);
ClientEntity client = clientService.requireByIdAndRealm(clientId, realm); ClientEntity client = clientService.requireByNameAndRealm(clientId, realm);
if (client != null) if (client != null)
{ {
String code = authorizationService.generateBySessionCreation(realmKey, clientId, new AuthorizationCreation(email, password)); String code = authorizationService.generateBySessionCreation(realmKey, clientId, new AuthorizationCreation(email, password));
@ -73,14 +77,36 @@ public class OidcResource
@Transactional @Transactional
public TokenResponse token(@FormParam("grant_type") String grantType, @FormParam("code") String code) throws NoSuchAlgorithmException, InvalidKeySpecException public TokenResponse token(@FormParam("grant_type") String grantType, @FormParam("code") String code) throws NoSuchAlgorithmException, InvalidKeySpecException
{ {
if (GrantType.AUTH_CODE.equals(GrantType.fromValue(grantType))) Grant grant = getGrant(grantType);
RealmEntity realm = realmService.getCurrent();
ClientEntity client = clientService.requireByNameAndRealm(identity.getPrincipal().getName(), realm);
if (!client.getAllowedGrants().contains(grant))
{
throw new ForbiddenException();
}
if (Grant.AUTHORIZATION_CODE.equals(grant))
{ {
return userTokenService.getToken(realmKey, code); return userTokenService.getToken(realmKey, code);
} }
if (GrantType.CLIENT_CREDENTIALS.equals(GrantType.fromValue(grantType))) if (Grant.CLIENT_CREDENTIALS.equals(grant))
{ {
return clientTokenService.getToken(realmKey); return clientTokenService.getToken(realmKey);
} }
throw new RuntimeException(); throw new RuntimeException();
} }
private Grant getGrant(String grantType)
{
if ("authorization_code".equals(grantType))
{
return Grant.AUTHORIZATION_CODE;
}
if ("client_credentials".equals(grantType))
{
return Grant.CLIENT_CREDENTIALS;
}
throw new RuntimeException();
}
} }

View File

@ -39,7 +39,7 @@ public class AuthorizationService
{ {
RealmEntity realm = realmService.requireByKey(realmKey); RealmEntity realm = realmService.requireByKey(realmKey);
ClientEntity client = clientService.requireByIdAndRealm(clientId, realm); ClientEntity client = clientService.requireByNameAndRealm(clientId, realm);
Optional<UserEntity> accountEntityOptional = userRepo.findOptionalByRealmAndEmail(realm, authorizationCreation.email()); Optional<UserEntity> accountEntityOptional = userRepo.findOptionalByRealmAndEmail(realm, authorizationCreation.email());
if (accountEntityOptional.isPresent()) if (accountEntityOptional.isPresent())
{ {

View File

@ -1,127 +0,0 @@
package de.tavolio.oidc.identityproviders;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import de.tavolio.Role;
import de.tavolio.oidc.IssuerService;
import de.tavolio.realm.RealmEntity;
import de.tavolio.realm.client.ClientEntity;
import de.tavolio.realm.client.ClientService;
import de.tavolio.realm.key.KeypairEntity;
import de.tavolio.realm.key.KeypairRepo;
import de.tavolio.realm.user.Permission;
import de.tavolio.verify.JwksService;
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.jwt.auth.principal.DefaultJWTParser;
import io.smallrye.jwt.auth.principal.JWTAuthContextInfo;
import io.smallrye.jwt.auth.principal.ParseException;
import io.smallrye.mutiny.Uni;
import io.smallrye.mutiny.infrastructure.Infrastructure;
import jakarta.annotation.Priority;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.context.control.ActivateRequestContext;
import jakarta.inject.Inject;
import org.eclipse.microprofile.jwt.JsonWebToken;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.security.PublicKey;
import java.util.*;
@ApplicationScoped
@Priority(1)
public class ClientIdentityProvider implements IdentityProvider<TokenAuthenticationRequest>
{
private static final Logger LOG = LoggerFactory.getLogger(ClientIdentityProvider.class);
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
@Inject
JwksService jwksService;
@Inject
KeypairRepo keypairRepo;
@Inject
IssuerService issuerService;
@Inject
ClientService clientService;
@Override
public Class<TokenAuthenticationRequest> getRequestType()
{
return TokenAuthenticationRequest.class;
}
@Override
@ActivateRequestContext
public Uni<SecurityIdentity> 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(() -> {
KeypairEntity keypairEntity = keypairRepo.findById(keyId);
if (keypairEntity != null)
{
PublicKey publicKey = jwksService.findByKid(keypairEntity).toPublicKey();
RealmEntity realm = keypairEntity.getRealm();
try
{
JsonWebToken token = new DefaultJWTParser(getContextForRealm(realm.getKey())).verify(raw, publicKey);
ClientEntity client = clientService.requireByIdAndRealm(token.getName(), realm);
if (client != null)
{
Set<Permission> permissions = new HashSet<>(client.getPermissions());
return (SecurityIdentity) QuarkusSecurityIdentity.builder().setPrincipal(new QuarkusPrincipal(client.getId())).addRole(Role.CLIENT.toString()).addAttribute("permissions", permissions).build();
}
}
catch (ParseException e)
{
throw new RuntimeException(e);
}
}
LOG.error("Cannot find key with id {}", kid);
throw new AuthenticationFailedException();
}).runSubscriptionOn(Infrastructure.getDefaultWorkerPool());
}
return Uni.createFrom().nullItem();
}
catch (JsonProcessingException e)
{
return Uni.createFrom().failure(new AuthenticationFailedException());
}
}
private Map<String, Object> getHeader(String raw) throws JsonProcessingException
{
return OBJECT_MAPPER.readValue(new String(Base64.getUrlDecoder().decode(section(raw))), new TypeReference<Map<String, Object>>(){});
}
private String section(String raw)
{
String[] sections = raw.split("\\.");
if (sections.length == 3)
{
return sections[0];
}
throw new RuntimeException();
}
private JWTAuthContextInfo getContextForRealm(String realmKey)
{
JWTAuthContextInfo info = new JWTAuthContextInfo();
info.setIssuedBy(issuerService.getIssuer(realmKey));
return info;
}
}

View File

@ -32,13 +32,13 @@ public class ClientTokenService
public TokenResponse getToken(String realmKey) public TokenResponse getToken(String realmKey)
{ {
RealmEntity realm = realmService.requireByKey(realmKey); RealmEntity realm = realmService.requireByKey(realmKey);
ClientEntity client = clientService.requireByIdAndRealm(identity.getPrincipal().getName(), realm); ClientEntity client = clientService.requireByNameAndRealm(identity.getPrincipal().getName(), realm);
if (client != null) if (client != null)
{ {
KeypairEntity keypair = realm.getKeys().getFirst(); KeypairEntity keypair = realm.getKeys().getFirst();
PrivateKey signingKey = KeypairEntity.toPrivateKey(keypair); PrivateKey signingKey = KeypairEntity.toPrivateKey(keypair);
ZonedDateTime expiresAt = ZonedDateTime.now().plusYears(1); ZonedDateTime expiresAt = ZonedDateTime.now().plusYears(1);
String token = clientTokenGenerator.generateAccessToken(realmKey, client.getId(), expiresAt, signingKey, keypair.getId()); String token = clientTokenGenerator.generateAccessToken(realmKey, client.getName(), expiresAt, signingKey, keypair.getId());
return new TokenResponse().setAccessToken(token).setTokenType("Bearer").setExpiresAt(expiresAt.toInstant().getEpochSecond()); return new TokenResponse().setAccessToken(token).setTokenType("Bearer").setExpiresAt(expiresAt.toInstant().getEpochSecond());
} }
throw new BadRequestException(); throw new BadRequestException();

View File

@ -47,11 +47,11 @@ public class UserTokenService
String principal = identity.getPrincipal().getName(); String principal = identity.getPrincipal().getName();
RealmEntity realm = realmService.requireByKey(realmKey); RealmEntity realm = realmService.requireByKey(realmKey);
CodeEntity entity = codeRepo.findByRealmAndId(realm, code); CodeEntity entity = codeRepo.findByRealmAndId(realm, code);
if (entity != null && !ZonedDateTime.now().isAfter(entity.getExpiresAt()) && principal.equals(entity.getClient().getId())) if (entity != null && !ZonedDateTime.now().isAfter(entity.getExpiresAt()) && principal.equals(entity.getClient().getName()))
{ {
KeypairEntity keypair = realm.getKeys().getFirst(); KeypairEntity keypair = realm.getKeys().getFirst();
PrivateKey signingKey = KeypairEntity.toPrivateKey(keypair); PrivateKey signingKey = KeypairEntity.toPrivateKey(keypair);
ZonedDateTime expiresAt = ZonedDateTime.now().plusYears(1); ZonedDateTime expiresAt = ZonedDateTime.now().plusSeconds(realm.getLifetime());
TokenResponse response = new TokenResponse() TokenResponse response = new TokenResponse()
.setAccessToken(userTokenGenerator.generateAccessToken(realm.getKey(), principal, entity.getAccount().getId(), expiresAt, signingKey, keypair.getId())) .setAccessToken(userTokenGenerator.generateAccessToken(realm.getKey(), principal, entity.getAccount().getId(), expiresAt, signingKey, keypair.getId()))
.setRefreshToken(UUID.randomUUID().toString()) .setRefreshToken(UUID.randomUUID().toString())

View File

@ -1,5 +1,5 @@
package de.tavolio.realm; package de.tavolio.realm;
public record Realm(String key, String name) public record Realm(String key, String name, int lifetime)
{ {
} }

View File

@ -6,6 +6,7 @@ import de.tavolio.realm.user.UserEntity;
import de.tavolio.realm.client.ClientEntity; import de.tavolio.realm.client.ClientEntity;
import de.tavolio.realm.code.CodeEntity; import de.tavolio.realm.code.CodeEntity;
import de.tavolio.realm.role.RoleEntity; import de.tavolio.realm.role.RoleEntity;
import jakarta.enterprise.inject.Vetoed;
import jakarta.persistence.*; import jakarta.persistence.*;
import org.hibernate.annotations.OnDelete; import org.hibernate.annotations.OnDelete;
import org.hibernate.annotations.OnDeleteAction; import org.hibernate.annotations.OnDeleteAction;
@ -15,6 +16,7 @@ import java.util.List;
@Entity @Entity
@Table(name = "realm") @Table(name = "realm")
@Vetoed
public class RealmEntity public class RealmEntity
{ {
@Id @Id
@ -26,6 +28,8 @@ public class RealmEntity
@Column(name = "realm_name") @Column(name = "realm_name")
private String name; private String name;
private int lifetime;
@OneToMany(mappedBy = "realm") @OneToMany(mappedBy = "realm")
@OnDelete(action = OnDeleteAction.CASCADE) @OnDelete(action = OnDeleteAction.CASCADE)
private List<KeypairEntity> keys = new ArrayList<>(); private List<KeypairEntity> keys = new ArrayList<>();
@ -83,6 +87,17 @@ public class RealmEntity
return this; return this;
} }
public int getLifetime()
{
return lifetime;
}
public RealmEntity setLifetime(int lifetime)
{
this.lifetime = lifetime;
return this;
}
public List<KeypairEntity> getKeys() public List<KeypairEntity> getKeys()
{ {
return keys; return keys;

View File

@ -0,0 +1,16 @@
package de.tavolio.realm;
import java.util.List;
public class RealmMapper
{
public static List<Realm> map(List<RealmEntity> entities)
{
return entities.stream().map(RealmMapper::map).toList();
}
public static Realm map(RealmEntity entity)
{
return new Realm(entity.getKey(), entity.getName(), entity.getLifetime());
}
}

View File

@ -3,6 +3,8 @@ package de.tavolio.realm;
import de.tavolio.Role; import de.tavolio.Role;
import de.tavolio.oidc.OidcConfigurationResource; import de.tavolio.oidc.OidcConfigurationResource;
import de.tavolio.oidc.OidcResource; import de.tavolio.oidc.OidcResource;
import de.tavolio.realm.assignment.Assignment;
import de.tavolio.realm.assignment.AssignmentResource;
import de.tavolio.realm.client.ClientResource; import de.tavolio.realm.client.ClientResource;
import de.tavolio.realm.role.RoleResource; import de.tavolio.realm.role.RoleResource;
import de.tavolio.realm.user.UserResource; import de.tavolio.realm.user.UserResource;
@ -11,6 +13,7 @@ import jakarta.annotation.security.RolesAllowed;
import jakarta.enterprise.inject.spi.CDI; import jakarta.enterprise.inject.spi.CDI;
import jakarta.inject.Inject; import jakarta.inject.Inject;
import jakarta.ws.rs.GET; import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path; import jakarta.ws.rs.Path;
import java.util.List; import java.util.List;
@ -21,19 +24,28 @@ public class RealmResource
@Inject @Inject
RealmRepo repo; RealmRepo repo;
@Inject
RealmService realmService;
@Path("/{realm-key}") @Path("/{realm-key}")
public RealmSubResource realms() public RealmSubResource realms()
{ {
return CDI.current().select(RealmSubResource.class).get(); return CDI.current().select(RealmSubResource.class).get();
} }
@RolesAllowed("ROOT") @RolesAllowed({"ROOT", "CLIENT"})
@Path("/{realm-key}/users") @Path("/{realm-key}/users")
public UserResource accounts() public UserResource accounts()
{ {
return CDI.current().select(UserResource.class).get(); return CDI.current().select(UserResource.class).get();
} }
@Path("/{realm-key}/users/{user-id}/role-assignments")
public AssignmentResource assignments()
{
return CDI.current().select(AssignmentResource.class).get();
}
@RolesAllowed("ROOT") @RolesAllowed("ROOT")
@Path("/{realm-key}/clients") @Path("/{realm-key}/clients")
public ClientResource clients() public ClientResource clients()
@ -63,6 +75,13 @@ public class RealmResource
@GET @GET
public List<Realm> get() public List<Realm> get()
{ {
return repo.listAll().stream().map(item -> new Realm(item.getKey(), item.getName())).toList(); return RealmMapper.map(repo.listAll());
}
@POST
@RolesAllowed("ROOT")
public Realm post(RealmCreation realmCreation)
{
return realmService.create(realmCreation);
} }
} }

View File

@ -1,11 +1,13 @@
package de.tavolio.realm; package de.tavolio.realm;
import de.tavolio.bootstrap.model.Realm;
import de.tavolio.realm.key.KeypairEntity; import de.tavolio.realm.key.KeypairEntity;
import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject; import jakarta.inject.Inject;
import jakarta.transaction.Transactional; import jakarta.transaction.Transactional;
import jakarta.ws.rs.InternalServerErrorException;
import jakarta.ws.rs.NotFoundException; import jakarta.ws.rs.NotFoundException;
import jakarta.ws.rs.core.UriInfo;
import org.apache.commons.lang3.StringUtils;
import java.security.KeyPair; import java.security.KeyPair;
import java.security.KeyPairGenerator; import java.security.KeyPairGenerator;
@ -22,6 +24,9 @@ public class RealmService
@Inject @Inject
RealmRepo repo; RealmRepo repo;
@Inject
UriInfo context;
public RealmEntity requireByKey(String key) public RealmEntity requireByKey(String key)
{ {
RealmEntity realm = repo.findByKey(key); RealmEntity realm = repo.findByKey(key);
@ -32,13 +37,23 @@ public class RealmService
throw new NotFoundException(); throw new NotFoundException();
} }
public RealmEntity getCurrent()
{
String realmKey = context.getPathParameters().getFirst("realm-key");
if (!StringUtils.isBlank(realmKey))
{
return requireByKey(realmKey);
}
throw new InternalServerErrorException();
}
@Transactional @Transactional
public RealmEntity create(RealmCreation creation) public Realm create(RealmCreation creation)
{ {
RealmEntity realm = new RealmEntity(); RealmEntity realm = new RealmEntity();
realm.setKey(creation.key()); realm.setKey(creation.key());
realm.setName(creation.name()); realm.setName(creation.name());
repo.persist(realm); repo.persist(realm);
return realm; return RealmMapper.map(realm);
} }
} }

View File

@ -4,16 +4,15 @@ import io.quarkus.security.Authenticated;
import jakarta.annotation.security.RolesAllowed; import jakarta.annotation.security.RolesAllowed;
import jakarta.enterprise.context.RequestScoped; import jakarta.enterprise.context.RequestScoped;
import jakarta.inject.Inject; import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import jakarta.ws.rs.DELETE; import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.GET; import jakarta.ws.rs.GET;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.PathParam; import jakarta.ws.rs.PathParam;
@RequestScoped @RequestScoped
public class RealmSubResource public class RealmSubResource
{ {
@PathParam("realm-key")
String key;
@Inject @Inject
RealmRepo repo; RealmRepo repo;
@ -23,14 +22,14 @@ public class RealmSubResource
@GET @GET
public Realm get() public Realm get()
{ {
RealmEntity realm = service.requireByKey(key); return RealmMapper.map(service.getCurrent());
return new Realm(realm.getKey(), realm.getName());
} }
@RolesAllowed("ROOT") @RolesAllowed("ROOT")
@DELETE @DELETE
@Transactional
public void delete() public void delete()
{ {
repo.delete(service.requireByKey(key)); repo.delete(service.getCurrent());
} }
} }

View File

@ -0,0 +1,9 @@
package de.tavolio.realm.assignment;
import de.tavolio.realm.role.Role;
import java.util.List;
public record Assignment(List<Role> assigned, List<Role> unassigned)
{
}

View File

@ -0,0 +1,54 @@
package de.tavolio.realm.assignment;
import de.tavolio.realm.role.RoleEntity;
import de.tavolio.realm.user.UserEntity;
import jakarta.persistence.*;
@Entity
@Table(name = "assignment")
public class AssignmentEntity
{
@Id
private String id;
@ManyToOne
@JoinColumn(name = "user_id")
private UserEntity user;
@ManyToOne
@JoinColumn(name = "role_id")
private RoleEntity role;
public String getId()
{
return id;
}
public AssignmentEntity setId(String id)
{
this.id = id;
return this;
}
public UserEntity getUser()
{
return user;
}
public AssignmentEntity setUser(UserEntity user)
{
this.user = user;
return this;
}
public RoleEntity getRole()
{
return role;
}
public AssignmentEntity setRole(RoleEntity role)
{
this.role = role;
return this;
}
}

View File

@ -0,0 +1,19 @@
package de.tavolio.realm.assignment;
import de.tavolio.realm.RealmEntity;
import de.tavolio.realm.role.RoleEntity;
import de.tavolio.realm.user.UserEntity;
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 AssignmentRepo implements PanacheRepositoryBase<AssignmentEntity, String>
{
public AssignmentEntity findByRoleAndUser(RoleEntity role, UserEntity user)
{
return find("role = :role AND user = :user", Parameters.with("role", role).and("user", user)).firstResult();
}
}

View File

@ -0,0 +1,7 @@
package de.tavolio.realm.assignment;
import java.util.List;
public record AssignmentRequest(List<String> assign, List<String> unassign)
{
}

View File

@ -0,0 +1,34 @@
package de.tavolio.realm.assignment;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.context.RequestScoped;
import jakarta.inject.Inject;
import jakarta.ws.rs.*;
import java.util.List;
@ApplicationScoped
public class AssignmentResource
{
@Inject
AssignmentService assignmentService;
@GET
public Assignment get(@PathParam("user-id") String userId)
{
return assignmentService.get(userId);
}
@PUT
public void post(@PathParam("user-id") String userId, AssignmentRequest request)
{
if (request.assign() != null)
{
assignmentService.assign(userId, request.assign());
}
if (request.unassign() != null)
{
assignmentService.unassign(userId, request.unassign());
}
}
}

View File

@ -0,0 +1,98 @@
package de.tavolio.realm.assignment;
import de.tavolio.realm.RealmEntity;
import de.tavolio.realm.RealmService;
import de.tavolio.realm.role.Role;
import de.tavolio.realm.role.RoleEntity;
import de.tavolio.realm.role.RoleRepo;
import de.tavolio.realm.role.RoleService;
import de.tavolio.realm.user.UserEntity;
import de.tavolio.realm.user.UserRepo;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;
import java.util.LinkedList;
import java.util.List;
import java.util.UUID;
@ApplicationScoped
public class AssignmentService
{
@Inject
RoleRepo roleRepo;
@Inject
RealmService realmService;
@Inject
UserRepo userRepo;
@Inject
AssignmentRepo assignmentRepo;
public Assignment get(String userId)
{
RealmEntity realm = realmService.getCurrent();
UserEntity user = userRepo.findByRealmAndId(realm, userId);
List<Role> assigned = new LinkedList<>();
List<Role> unassigned = new LinkedList<>();
for (RoleEntity role : roleRepo.findByRealm(realm))
{
if (isAssigned(user, role))
{
assigned.add(new Role(role.getId(), role.getName()));
}
else
{
unassigned.add(new Role(role.getId(), role.getName()));
}
}
return new Assignment(assigned, unassigned);
}
@Transactional
public void assign(String userId, List<String> roles)
{
RealmEntity realm = realmService.getCurrent();
UserEntity user = userRepo.findByRealmAndId(realm, userId);
for (String roleName : roles)
{
RoleEntity role = roleRepo.findByRealmAndName(realm, roleName);
assign(user, role);
}
}
public void assign(UserEntity user, RoleEntity role)
{
AssignmentEntity assignment = new AssignmentEntity().setId(UUID.randomUUID().toString());
assignment.setUser(user);
assignment.setRole(role);
assignmentRepo.persist(assignment);
}
@Transactional
public void unassign(String userId, List<String> roles)
{
RealmEntity realm = realmService.getCurrent();
UserEntity user = userRepo.findByRealmAndId(realm, userId);
for (String roleName : roles)
{
RoleEntity role = roleRepo.findByRealmAndName(realm, roleName);
AssignmentEntity assignment = assignmentRepo.findByRoleAndUser(role, user);
assignmentRepo.delete(assignment);
}
}
private boolean isAssigned(UserEntity user, RoleEntity role)
{
for (AssignmentEntity assignment : role.getAssignments())
{
if (assignment.getUser().getId().equals(user.getId()))
{
return true;
}
}
return false;
}
}

View File

@ -43,7 +43,7 @@ public class ClientEntity implements RealmScoped
@CollectionTable(name = "allowed_grants", joinColumns = @JoinColumn(name = "client_id")) @CollectionTable(name = "allowed_grants", joinColumns = @JoinColumn(name = "client_id"))
@Column(name = "grant_name") @Column(name = "grant_name")
@Enumerated(EnumType.STRING) @Enumerated(EnumType.STRING)
private Set<Grant> allowedGrants = new HashSet<Grant>(); private Set<Grant> allowedGrants = new HashSet<>();
public String getId() public String getId()
{ {

View File

@ -1,13 +1,11 @@
package de.tavolio.realm.client; package de.tavolio.realm.client;
import de.tavolio.realm.RealmEntity; import de.tavolio.realm.RealmEntity;
import de.tavolio.realm.RealmRepo;
import de.tavolio.realm.RealmService; import de.tavolio.realm.RealmService;
import io.quarkus.elytron.security.common.BcryptUtil; import io.quarkus.elytron.security.common.BcryptUtil;
import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject; import jakarta.inject.Inject;
import jakarta.transaction.Transactional; import jakarta.transaction.Transactional;
import jakarta.ws.rs.DELETE;
import jakarta.ws.rs.NotFoundException; import jakarta.ws.rs.NotFoundException;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
@ -22,9 +20,9 @@ public class ClientService
@Inject @Inject
RealmService realmService; RealmService realmService;
public ClientEntity requireByIdAndRealm(String clientId, RealmEntity realm) public ClientEntity requireByNameAndRealm(String clientId, RealmEntity realm)
{ {
ClientEntity client = clientRepo.findByRealmAndId(realm, clientId); ClientEntity client = clientRepo.findByRealmAndName(realm, clientId);
if (client != null) if (client != null)
{ {
return client; return client;

View File

@ -2,5 +2,5 @@ package de.tavolio.realm.client;
public enum Grant public enum Grant
{ {
CLIENT_CREDENTIALS, AUTHORIZATION, PASSWORD CLIENT_CREDENTIALS, AUTHORIZATION_CODE, PASSWORD
} }

View File

@ -2,6 +2,7 @@ package de.tavolio.realm.role;
import de.tavolio.realm.RealmEntity; import de.tavolio.realm.RealmEntity;
import de.tavolio.realm.RealmScoped; import de.tavolio.realm.RealmScoped;
import de.tavolio.realm.assignment.AssignmentEntity;
import jakarta.persistence.*; import jakarta.persistence.*;
import java.util.ArrayList; import java.util.ArrayList;
@ -21,6 +22,9 @@ public class RoleEntity implements RealmScoped
@JoinColumn(name = "realm_id") @JoinColumn(name = "realm_id")
private RealmEntity realm; private RealmEntity realm;
@OneToMany(mappedBy = "role")
private List<AssignmentEntity> assignments;
public String getId() public String getId()
{ {
return id; return id;
@ -53,4 +57,15 @@ public class RoleEntity implements RealmScoped
this.realm = realm; this.realm = realm;
return this; return this;
} }
public List<AssignmentEntity> getAssignments()
{
return assignments;
}
public RoleEntity setAssignments(List<AssignmentEntity> assignments)
{
this.assignments = assignments;
return this;
}
} }

View File

@ -15,4 +15,9 @@ public class RoleRepo implements PanacheRepositoryBase<RoleEntity, String>
{ {
return list("realm = :realm", Parameters.with("realm", realm)); return list("realm = :realm", Parameters.with("realm", realm));
} }
public RoleEntity findByRealmAndName(RealmEntity realm, String name)
{
return find("realm = :realm AND name = :name", Parameters.with("realm", realm).and("name", name)).firstResult();
}
} }

View File

@ -2,5 +2,5 @@ package de.tavolio.realm.user;
public enum Permission public enum Permission
{ {
USER_VIEW, USER_CREATE USER_VIEW, USER_DELETE, USER_CREATE
} }

View File

@ -1,6 +1,7 @@
package de.tavolio.realm.user; package de.tavolio.realm.user;
import de.tavolio.realm.RealmScoped; import de.tavolio.realm.RealmScoped;
import de.tavolio.realm.assignment.AssignmentEntity;
import de.tavolio.realm.code.CodeEntity; import de.tavolio.realm.code.CodeEntity;
import de.tavolio.realm.RealmEntity; import de.tavolio.realm.RealmEntity;
import jakarta.persistence.*; import jakarta.persistence.*;
@ -34,6 +35,9 @@ public class UserEntity implements RealmScoped
@OneToMany(mappedBy = "account") @OneToMany(mappedBy = "account")
private List<CodeEntity> codes; private List<CodeEntity> codes;
@OneToMany(mappedBy = "user")
private List<AssignmentEntity> assignments;
public static UserEntity init() public static UserEntity init()
{ {
return new UserEntity().setId(UUID.randomUUID().toString()); return new UserEntity().setId(UUID.randomUUID().toString());
@ -126,4 +130,15 @@ public class UserEntity implements RealmScoped
this.codes = codes; this.codes = codes;
return this; return this;
} }
public List<AssignmentEntity> getAssignments()
{
return assignments;
}
public UserEntity setAssignments(List<AssignmentEntity> assignments)
{
this.assignments = assignments;
return this;
}
} }

View File

@ -53,7 +53,7 @@ public class UserResource
@Path("/{id}") @Path("/{id}")
public void delete(@PathParam("id") String id) public void delete(@PathParam("id") String id)
{ {
permissionService.hasPermission(Permission.USER_VIEW); permissionService.hasPermission(Permission.USER_DELETE);
userService.delete(realmKey, id); userService.delete(realmKey, id);
} }

View File

@ -25,7 +25,7 @@ quarkus.flyway.enabled=false
quarkus.flyway.migrate-at-start=true quarkus.flyway.migrate-at-start=true
quarkus.http.access-log.enabled=true quarkus.http.access-log.enabled=true
quarkus.http.auth.basic=true quarkus.http.auth.basic=false
%dev.io.verifoo.http.origin=http://localhost:8089 %dev.io.verifoo.http.origin=http://localhost:8089

View File

@ -8,17 +8,22 @@ realms:
key: key:
type: EC type: EC
alg: P256 alg: P256
roles:
- name: ADMIN
- name: USER
clients: clients:
backend: backend:
secret: secret:
bcrypt: $2a$12$1oYS45e/nXP1OeMgdZZAKeEixarRDzbBGZd0xOnEQQMKlOKwVMrX. bcrypt: $2a$12$1oYS45e/nXP1OeMgdZZAKeEixarRDzbBGZd0xOnEQQMKlOKwVMrX.
redirect-uri: http://localhost:8080/callback redirect-uri: http://localhost:8080/auth/callback
permissions: permissions:
- USER_VIEW - USER_VIEW
allowed-grants:
- AUTHORIZATION_CODE
analytics-backend: analytics-backend:
secret: secret:
bcrypt: $2a$12$1oYS45e/nXP1OeMgdZZAKeEixarRDzbBGZd0xOnEQQMKlOKwVMrX. bcrypt: $2a$12$1oYS45e/nXP1OeMgdZZAKeEixarRDzbBGZd0xOnEQQMKlOKwVMrX.
redirect-uri: http://localhost:8080/callback redirect-uri: http://localhost:8080/auth/callback
permissions: permissions:
- USER_VIEW - USER_VIEW
users: users: