From 214243e465c5f76356dfff280f543669ec88e3f6 Mon Sep 17 00:00:00 2001 From: Walter Oggioni Date: Thu, 7 Sep 2023 08:15:02 +0800 Subject: [PATCH] added oidc authentication --- jpacrepo-api/src/main/java/module-info.java | 1 + .../woggioni/jpacrepo/api/security/Roles.java | 9 ++++ .../api/service/PacmanServiceLocal.java | 2 + .../api/service/PacmanServiceRemote.java | 6 +++ .../jpacrepo/service/PacmanServiceEJB.java | 1 + .../jpacrepo/service/PacmanWebService.java | 47 +++++++++++-------- .../jpacrepo/service/WxRsApplication.java | 5 ++ .../exception/EJBAccessExceptionMapper.java | 12 +++++ .../jpacrepo/service/jpa/QueryBuilder.java | 35 -------------- src/main/webapp/WEB-INF/oidc.json | 14 ++++++ src/main/webapp/WEB-INF/web.xml | 21 +++++++++ 11 files changed, 99 insertions(+), 54 deletions(-) create mode 100644 jpacrepo-api/src/main/java/net/woggioni/jpacrepo/api/security/Roles.java create mode 100644 src/main/java/net/woggioni/jpacrepo/service/exception/EJBAccessExceptionMapper.java delete mode 100644 src/main/java/net/woggioni/jpacrepo/service/jpa/QueryBuilder.java create mode 100644 src/main/webapp/WEB-INF/oidc.json create mode 100644 src/main/webapp/WEB-INF/web.xml diff --git a/jpacrepo-api/src/main/java/module-info.java b/jpacrepo-api/src/main/java/module-info.java index 95de5bc..a927d19 100644 --- a/jpacrepo-api/src/main/java/module-info.java +++ b/jpacrepo-api/src/main/java/module-info.java @@ -9,4 +9,5 @@ module net.woggioni.jpacrepo.api { exports net.woggioni.jpacrepo.api.model; exports net.woggioni.jpacrepo.api.service; exports net.woggioni.jpacrepo.api.wire; + exports net.woggioni.jpacrepo.api.security; } \ No newline at end of file diff --git a/jpacrepo-api/src/main/java/net/woggioni/jpacrepo/api/security/Roles.java b/jpacrepo-api/src/main/java/net/woggioni/jpacrepo/api/security/Roles.java new file mode 100644 index 0000000..77fbed0 --- /dev/null +++ b/jpacrepo-api/src/main/java/net/woggioni/jpacrepo/api/security/Roles.java @@ -0,0 +1,9 @@ +package net.woggioni.jpacrepo.api.security; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class Roles { + public static final String WRITER = "jpacrepo"; +} diff --git a/jpacrepo-api/src/main/java/net/woggioni/jpacrepo/api/service/PacmanServiceLocal.java b/jpacrepo-api/src/main/java/net/woggioni/jpacrepo/api/service/PacmanServiceLocal.java index c5d7693..6f234d4 100644 --- a/jpacrepo-api/src/main/java/net/woggioni/jpacrepo/api/service/PacmanServiceLocal.java +++ b/jpacrepo-api/src/main/java/net/woggioni/jpacrepo/api/service/PacmanServiceLocal.java @@ -1,5 +1,6 @@ package net.woggioni.jpacrepo.api.service; +import jakarta.annotation.security.PermitAll; import jakarta.ejb.Local; import net.woggioni.jpacrepo.api.model.CompressionFormat; import net.woggioni.jpacrepo.api.model.PkgData; @@ -7,6 +8,7 @@ import net.woggioni.jpacrepo.api.model.PkgData; import java.util.List; @Local +@PermitAll public interface PacmanServiceLocal extends PacmanServiceRemote { long countResults(String name, String version, String arch); List searchPackage(String name, diff --git a/jpacrepo-api/src/main/java/net/woggioni/jpacrepo/api/service/PacmanServiceRemote.java b/jpacrepo-api/src/main/java/net/woggioni/jpacrepo/api/service/PacmanServiceRemote.java index 6244aa7..f1b695d 100644 --- a/jpacrepo-api/src/main/java/net/woggioni/jpacrepo/api/service/PacmanServiceRemote.java +++ b/jpacrepo-api/src/main/java/net/woggioni/jpacrepo/api/service/PacmanServiceRemote.java @@ -2,10 +2,13 @@ package net.woggioni.jpacrepo.api.service; import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; +import jakarta.annotation.security.PermitAll; +import jakarta.annotation.security.RolesAllowed; import jakarta.ejb.Remote; import net.woggioni.jpacrepo.api.model.CompressionFormat; import net.woggioni.jpacrepo.api.model.PkgData; import net.woggioni.jpacrepo.api.model.PkgId; +import net.woggioni.jpacrepo.api.security.Roles; import net.woggioni.jpacrepo.api.wire.PkgTuple; import java.io.InputStream; @@ -15,6 +18,7 @@ import java.util.NavigableMap; import java.util.Set; @Remote +@PermitAll public interface PacmanServiceRemote { void syncDB(); @@ -42,9 +46,11 @@ public interface PacmanServiceRemote { @Nullable Long getFileSize(String fileName); + @RolesAllowed(Roles.WRITER) Set missingFiles(Collection fileNames); NavigableMap getPkgMap(); + @RolesAllowed(Roles.WRITER) boolean addPackage(String fileName, InputStream inputStream); } \ No newline at end of file diff --git a/src/main/java/net/woggioni/jpacrepo/service/PacmanServiceEJB.java b/src/main/java/net/woggioni/jpacrepo/service/PacmanServiceEJB.java index 0f47339..90ff1c9 100644 --- a/src/main/java/net/woggioni/jpacrepo/service/PacmanServiceEJB.java +++ b/src/main/java/net/woggioni/jpacrepo/service/PacmanServiceEJB.java @@ -70,6 +70,7 @@ import java.util.stream.Stream; @Local({PacmanServiceLocal.class}) @Remote({PacmanServiceRemote.class}) public class PacmanServiceEJB implements PacmanServiceLocal { + @PersistenceContext private EntityManager em; diff --git a/src/main/java/net/woggioni/jpacrepo/service/PacmanWebService.java b/src/main/java/net/woggioni/jpacrepo/service/PacmanWebService.java index e01579b..6a1cd7c 100644 --- a/src/main/java/net/woggioni/jpacrepo/service/PacmanWebService.java +++ b/src/main/java/net/woggioni/jpacrepo/service/PacmanWebService.java @@ -1,5 +1,6 @@ package net.woggioni.jpacrepo.service; +import jakarta.annotation.security.PermitAll; import jakarta.ejb.ConcurrencyManagement; import jakarta.ejb.ConcurrencyManagementType; import jakarta.ejb.TransactionAttribute; @@ -7,11 +8,8 @@ import jakarta.ejb.TransactionAttributeType; import jakarta.ejb.TransactionManagement; import jakarta.ejb.TransactionManagementType; import jakarta.inject.Inject; -import jakarta.persistence.EntityManager; -import jakarta.persistence.EntityManagerFactory; import jakarta.persistence.NoResultException; import jakarta.persistence.NonUniqueResultException; -import jakarta.persistence.TypedQuery; import jakarta.ws.rs.BadRequestException; import jakarta.ws.rs.Consumes; import jakarta.ws.rs.FormParam; @@ -31,25 +29,22 @@ import jakarta.ws.rs.core.EntityTag; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Request; import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.SecurityContext; import jakarta.ws.rs.core.StreamingOutput; import jakarta.ws.rs.core.UriInfo; import lombok.SneakyThrows; import net.woggioni.jpacrepo.api.model.CompressionFormat; import net.woggioni.jpacrepo.api.model.PkgData; import net.woggioni.jpacrepo.api.model.PkgId; +import net.woggioni.jpacrepo.api.security.Roles; import net.woggioni.jpacrepo.api.service.PacmanServiceLocal; -import net.woggioni.jpacrepo.config.AppConfig; -import net.woggioni.jpacrepo.impl.model.CompressionFormatImpl; -import net.woggioni.jpacrepo.impl.model.PkgDataImpl; import net.woggioni.jpacrepo.api.wire.PkgDataList; import net.woggioni.jpacrepo.api.wire.PkgTuple; import net.woggioni.jpacrepo.api.wire.StringList; -import net.woggioni.jpacrepo.version.PkgIdComparator; +import net.woggioni.jpacrepo.config.AppConfig; import net.woggioni.jpacrepo.version.VersionComparator; import net.woggioni.jwo.CollectionUtils; -import net.woggioni.jwo.Con; import net.woggioni.jwo.JWO; -import net.woggioni.jwo.Tuple2; import org.apache.commons.compress.archivers.tar.TarArchiveEntry; import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream; import org.slf4j.Logger; @@ -60,10 +55,9 @@ import java.net.URI; import java.net.URLDecoder; import java.nio.charset.StandardCharsets; import java.nio.file.Files; -import java.nio.file.Paths; -import java.nio.file.StandardCopyOption; import java.util.Arrays; import java.util.Comparator; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.NavigableMap; @@ -72,14 +66,15 @@ import java.util.Optional; import java.util.TreeMap; import java.util.stream.Collectors; +import static net.woggioni.jpacrepo.api.security.Roles.WRITER; + @Path("/pkg") +@PermitAll @ConcurrencyManagement(ConcurrencyManagementType.BEAN) @Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON}) @Consumes({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON}) @TransactionManagement(TransactionManagementType.CONTAINER) public class PacmanWebService { - @Inject - private EntityManagerFactory emf; @Inject private Logger log; @@ -139,7 +134,7 @@ public class PacmanWebService { cc.setNoCache(true); NavigableMap cachedMap = service.getPkgMap(); - EntityTag etag = new EntityTag(Integer.toString(cachedMap.hashCode()), false); + EntityTag etag = new EntityTag(Integer.toString(cachedMap.hashCode()), true); Response.ResponseBuilder builder = request.evaluatePreconditions(etag); if (builder == null) { @@ -238,8 +233,10 @@ public class PacmanWebService { @POST @Path("/doYouWantAny") @Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON}) - public Response doYouWantAny(List fileNames) { - if (fileNames.isEmpty()) { + public Response doYouWantAny(List fileNames, @Context SecurityContext sctx) { + if(!sctx.isUserInRole(WRITER)) { + return Response.status(Response.Status.UNAUTHORIZED).build(); + } else if (fileNames.isEmpty()) { return Response.ok(fileNames).build(); } else { @@ -255,9 +252,11 @@ public class PacmanWebService { public Response createPackage( InputStream input, @MatrixParam("filename") String filename, - @Context UriInfo uriInfo) { - EntityManager em = emf.createEntityManager(); - if (filename == null) throw new BadRequestException(); + @Context UriInfo uriInfo, + @Context SecurityContext sctx) { + if(!sctx.isUserInRole(Roles.WRITER)) { + return Response.status(Response.Status.UNAUTHORIZED).build(); + } else if (filename == null) throw new BadRequestException(); Response result; boolean added = service.addPackage(filename, input); if(!added) { @@ -334,4 +333,14 @@ public class PacmanWebService { }; return Response.ok(stream).header("Content-Disposition", "attachment; filename=pkgs.tar").build(); } + + @GET + @Path("whoAmI") + @SneakyThrows + public Response whoAmI(@Context SecurityContext securityContext) { + Map result = new HashMap<>(); + result.put("user", securityContext.getUserPrincipal().getName()); + result.put("jpacrepo", Boolean.toString(securityContext.isUserInRole("jpacrepo"))); + return Response.ok(result).build(); + } } diff --git a/src/main/java/net/woggioni/jpacrepo/service/WxRsApplication.java b/src/main/java/net/woggioni/jpacrepo/service/WxRsApplication.java index 4499e49..5e4ef4d 100644 --- a/src/main/java/net/woggioni/jpacrepo/service/WxRsApplication.java +++ b/src/main/java/net/woggioni/jpacrepo/service/WxRsApplication.java @@ -1,18 +1,23 @@ package net.woggioni.jpacrepo.service; +import jakarta.annotation.security.DeclareRoles; import jakarta.ws.rs.ApplicationPath; import jakarta.ws.rs.core.Application; +import net.woggioni.jpacrepo.api.security.Roles; +import net.woggioni.jpacrepo.service.exception.EJBAccessExceptionMapper; import java.util.HashSet; import java.util.Set; @ApplicationPath("api") +@DeclareRoles({Roles.WRITER}) public class WxRsApplication extends Application { @Override public Set> getClasses() { Set> result = new HashSet<>(); result.add(PacmanWebService.class); + result.add(EJBAccessExceptionMapper.class); return result; } } diff --git a/src/main/java/net/woggioni/jpacrepo/service/exception/EJBAccessExceptionMapper.java b/src/main/java/net/woggioni/jpacrepo/service/exception/EJBAccessExceptionMapper.java new file mode 100644 index 0000000..4ade161 --- /dev/null +++ b/src/main/java/net/woggioni/jpacrepo/service/exception/EJBAccessExceptionMapper.java @@ -0,0 +1,12 @@ +package net.woggioni.jpacrepo.service.exception; + +import jakarta.ejb.EJBAccessException; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.ext.ExceptionMapper; + +public class EJBAccessExceptionMapper implements ExceptionMapper { + @Override + public Response toResponse(EJBAccessException exception) { + return Response.status(Response.Status.UNAUTHORIZED).build(); + } +} diff --git a/src/main/java/net/woggioni/jpacrepo/service/jpa/QueryBuilder.java b/src/main/java/net/woggioni/jpacrepo/service/jpa/QueryBuilder.java deleted file mode 100644 index 93cf729..0000000 --- a/src/main/java/net/woggioni/jpacrepo/service/jpa/QueryBuilder.java +++ /dev/null @@ -1,35 +0,0 @@ -package net.woggioni.jpacrepo.service.jpa; - -import jakarta.persistence.EntityManager; -import jakarta.persistence.TypedQuery; -import jakarta.persistence.criteria.CriteriaBuilder; -import jakarta.persistence.criteria.CriteriaQuery; -import jakarta.persistence.criteria.Root; -import jakarta.persistence.criteria.Selection; -import jakarta.persistence.metamodel.Metamodel; - -public class QueryBuilder { - private final EntityManager em; - private final CriteriaBuilder cb; - private final CriteriaQuery criteriaQuery; - private final Metamodel metamodel; - public QueryBuilder(EntityManager em, Class cls) { - this.em = em; - this.cb = em.getCriteriaBuilder(); - this.criteriaQuery = cb.createQuery(cls); - this.metamodel = em.getMetamodel(); - } - - public QueryBuilder select(Selection a) { - criteriaQuery.select(a); - return this; - } - - public Root from(Class cls) { - return criteriaQuery.from(metamodel.entity(cls)); - } - - public TypedQuery build() { - return em.createQuery(criteriaQuery); - } -} diff --git a/src/main/webapp/WEB-INF/oidc.json b/src/main/webapp/WEB-INF/oidc.json new file mode 100644 index 0000000..9547b51 --- /dev/null +++ b/src/main/webapp/WEB-INF/oidc.json @@ -0,0 +1,14 @@ +{ + "realm": "woggioni.net", + "auth-server-url": "https://woggioni.net/auth", + "public-client": false, + "ssl-required": "all", + "resource": "jpacrepo", + "credentials": { + "secret": "${env.JPACREPO_OIDC_CLIENT_SECRET}" + }, + "verify-token-audience": true, + "confidential-port": 443, + "token-signature-algorithm": "ES512", + "use-resource-role-mappings": true +} diff --git a/src/main/webapp/WEB-INF/web.xml b/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 0000000..6fc05dc --- /dev/null +++ b/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,21 @@ + + + OIDC + + + + login2 + /api/pkg/doYouWantAny + /api/pkg/upload + + + jpacrepo + + + + jpacrepo + + \ No newline at end of file