added oidc authentication

This commit is contained in:
2023-09-07 08:15:02 +08:00
parent b9851eecaf
commit 214243e465
11 changed files with 99 additions and 54 deletions

View File

@@ -9,4 +9,5 @@ module net.woggioni.jpacrepo.api {
exports net.woggioni.jpacrepo.api.model; exports net.woggioni.jpacrepo.api.model;
exports net.woggioni.jpacrepo.api.service; exports net.woggioni.jpacrepo.api.service;
exports net.woggioni.jpacrepo.api.wire; exports net.woggioni.jpacrepo.api.wire;
exports net.woggioni.jpacrepo.api.security;
} }

View File

@@ -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";
}

View File

@@ -1,5 +1,6 @@
package net.woggioni.jpacrepo.api.service; package net.woggioni.jpacrepo.api.service;
import jakarta.annotation.security.PermitAll;
import jakarta.ejb.Local; import jakarta.ejb.Local;
import net.woggioni.jpacrepo.api.model.CompressionFormat; import net.woggioni.jpacrepo.api.model.CompressionFormat;
import net.woggioni.jpacrepo.api.model.PkgData; import net.woggioni.jpacrepo.api.model.PkgData;
@@ -7,6 +8,7 @@ import net.woggioni.jpacrepo.api.model.PkgData;
import java.util.List; import java.util.List;
@Local @Local
@PermitAll
public interface PacmanServiceLocal extends PacmanServiceRemote { public interface PacmanServiceLocal extends PacmanServiceRemote {
long countResults(String name, String version, String arch); long countResults(String name, String version, String arch);
List<PkgData> searchPackage(String name, List<PkgData> searchPackage(String name,

View File

@@ -2,10 +2,13 @@ package net.woggioni.jpacrepo.api.service;
import jakarta.annotation.Nonnull; import jakarta.annotation.Nonnull;
import jakarta.annotation.Nullable; import jakarta.annotation.Nullable;
import jakarta.annotation.security.PermitAll;
import jakarta.annotation.security.RolesAllowed;
import jakarta.ejb.Remote; import jakarta.ejb.Remote;
import net.woggioni.jpacrepo.api.model.CompressionFormat; import net.woggioni.jpacrepo.api.model.CompressionFormat;
import net.woggioni.jpacrepo.api.model.PkgData; import net.woggioni.jpacrepo.api.model.PkgData;
import net.woggioni.jpacrepo.api.model.PkgId; import net.woggioni.jpacrepo.api.model.PkgId;
import net.woggioni.jpacrepo.api.security.Roles;
import net.woggioni.jpacrepo.api.wire.PkgTuple; import net.woggioni.jpacrepo.api.wire.PkgTuple;
import java.io.InputStream; import java.io.InputStream;
@@ -15,6 +18,7 @@ import java.util.NavigableMap;
import java.util.Set; import java.util.Set;
@Remote @Remote
@PermitAll
public interface PacmanServiceRemote { public interface PacmanServiceRemote {
void syncDB(); void syncDB();
@@ -42,9 +46,11 @@ public interface PacmanServiceRemote {
@Nullable @Nullable
Long getFileSize(String fileName); Long getFileSize(String fileName);
@RolesAllowed(Roles.WRITER)
Set<String> missingFiles(Collection<String> fileNames); Set<String> missingFiles(Collection<String> fileNames);
NavigableMap<PkgId, PkgTuple> getPkgMap(); NavigableMap<PkgId, PkgTuple> getPkgMap();
@RolesAllowed(Roles.WRITER)
boolean addPackage(String fileName, InputStream inputStream); boolean addPackage(String fileName, InputStream inputStream);
} }

View File

@@ -70,6 +70,7 @@ import java.util.stream.Stream;
@Local({PacmanServiceLocal.class}) @Local({PacmanServiceLocal.class})
@Remote({PacmanServiceRemote.class}) @Remote({PacmanServiceRemote.class})
public class PacmanServiceEJB implements PacmanServiceLocal { public class PacmanServiceEJB implements PacmanServiceLocal {
@PersistenceContext @PersistenceContext
private EntityManager em; private EntityManager em;

View File

@@ -1,5 +1,6 @@
package net.woggioni.jpacrepo.service; package net.woggioni.jpacrepo.service;
import jakarta.annotation.security.PermitAll;
import jakarta.ejb.ConcurrencyManagement; import jakarta.ejb.ConcurrencyManagement;
import jakarta.ejb.ConcurrencyManagementType; import jakarta.ejb.ConcurrencyManagementType;
import jakarta.ejb.TransactionAttribute; import jakarta.ejb.TransactionAttribute;
@@ -7,11 +8,8 @@ import jakarta.ejb.TransactionAttributeType;
import jakarta.ejb.TransactionManagement; import jakarta.ejb.TransactionManagement;
import jakarta.ejb.TransactionManagementType; import jakarta.ejb.TransactionManagementType;
import jakarta.inject.Inject; import jakarta.inject.Inject;
import jakarta.persistence.EntityManager;
import jakarta.persistence.EntityManagerFactory;
import jakarta.persistence.NoResultException; import jakarta.persistence.NoResultException;
import jakarta.persistence.NonUniqueResultException; import jakarta.persistence.NonUniqueResultException;
import jakarta.persistence.TypedQuery;
import jakarta.ws.rs.BadRequestException; import jakarta.ws.rs.BadRequestException;
import jakarta.ws.rs.Consumes; import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.FormParam; 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.MediaType;
import jakarta.ws.rs.core.Request; import jakarta.ws.rs.core.Request;
import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.SecurityContext;
import jakarta.ws.rs.core.StreamingOutput; import jakarta.ws.rs.core.StreamingOutput;
import jakarta.ws.rs.core.UriInfo; import jakarta.ws.rs.core.UriInfo;
import lombok.SneakyThrows; import lombok.SneakyThrows;
import net.woggioni.jpacrepo.api.model.CompressionFormat; import net.woggioni.jpacrepo.api.model.CompressionFormat;
import net.woggioni.jpacrepo.api.model.PkgData; import net.woggioni.jpacrepo.api.model.PkgData;
import net.woggioni.jpacrepo.api.model.PkgId; 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.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.PkgDataList;
import net.woggioni.jpacrepo.api.wire.PkgTuple; import net.woggioni.jpacrepo.api.wire.PkgTuple;
import net.woggioni.jpacrepo.api.wire.StringList; 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.jpacrepo.version.VersionComparator;
import net.woggioni.jwo.CollectionUtils; import net.woggioni.jwo.CollectionUtils;
import net.woggioni.jwo.Con;
import net.woggioni.jwo.JWO; 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.TarArchiveEntry;
import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream; import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream;
import org.slf4j.Logger; import org.slf4j.Logger;
@@ -60,10 +55,9 @@ import java.net.URI;
import java.net.URLDecoder; import java.net.URLDecoder;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.Files; import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.Arrays; import java.util.Arrays;
import java.util.Comparator; import java.util.Comparator;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.NavigableMap; import java.util.NavigableMap;
@@ -72,14 +66,15 @@ import java.util.Optional;
import java.util.TreeMap; import java.util.TreeMap;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static net.woggioni.jpacrepo.api.security.Roles.WRITER;
@Path("/pkg") @Path("/pkg")
@PermitAll
@ConcurrencyManagement(ConcurrencyManagementType.BEAN) @ConcurrencyManagement(ConcurrencyManagementType.BEAN)
@Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON}) @Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON})
@Consumes({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON}) @Consumes({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON})
@TransactionManagement(TransactionManagementType.CONTAINER) @TransactionManagement(TransactionManagementType.CONTAINER)
public class PacmanWebService { public class PacmanWebService {
@Inject
private EntityManagerFactory emf;
@Inject @Inject
private Logger log; private Logger log;
@@ -139,7 +134,7 @@ public class PacmanWebService {
cc.setNoCache(true); cc.setNoCache(true);
NavigableMap<PkgId, PkgTuple> cachedMap = service.getPkgMap(); NavigableMap<PkgId, PkgTuple> 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); Response.ResponseBuilder builder = request.evaluatePreconditions(etag);
if (builder == null) { if (builder == null) {
@@ -238,8 +233,10 @@ public class PacmanWebService {
@POST @POST
@Path("/doYouWantAny") @Path("/doYouWantAny")
@Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON}) @Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON})
public Response doYouWantAny(List<String> fileNames) { public Response doYouWantAny(List<String> fileNames, @Context SecurityContext sctx) {
if (fileNames.isEmpty()) { if(!sctx.isUserInRole(WRITER)) {
return Response.status(Response.Status.UNAUTHORIZED).build();
} else if (fileNames.isEmpty()) {
return Response.ok(fileNames).build(); return Response.ok(fileNames).build();
} }
else { else {
@@ -255,9 +252,11 @@ public class PacmanWebService {
public Response createPackage( public Response createPackage(
InputStream input, InputStream input,
@MatrixParam("filename") String filename, @MatrixParam("filename") String filename,
@Context UriInfo uriInfo) { @Context UriInfo uriInfo,
EntityManager em = emf.createEntityManager(); @Context SecurityContext sctx) {
if (filename == null) throw new BadRequestException(); if(!sctx.isUserInRole(Roles.WRITER)) {
return Response.status(Response.Status.UNAUTHORIZED).build();
} else if (filename == null) throw new BadRequestException();
Response result; Response result;
boolean added = service.addPackage(filename, input); boolean added = service.addPackage(filename, input);
if(!added) { if(!added) {
@@ -334,4 +333,14 @@ public class PacmanWebService {
}; };
return Response.ok(stream).header("Content-Disposition", "attachment; filename=pkgs.tar").build(); return Response.ok(stream).header("Content-Disposition", "attachment; filename=pkgs.tar").build();
} }
@GET
@Path("whoAmI")
@SneakyThrows
public Response whoAmI(@Context SecurityContext securityContext) {
Map<String, String> result = new HashMap<>();
result.put("user", securityContext.getUserPrincipal().getName());
result.put("jpacrepo", Boolean.toString(securityContext.isUserInRole("jpacrepo")));
return Response.ok(result).build();
}
} }

View File

@@ -1,18 +1,23 @@
package net.woggioni.jpacrepo.service; package net.woggioni.jpacrepo.service;
import jakarta.annotation.security.DeclareRoles;
import jakarta.ws.rs.ApplicationPath; import jakarta.ws.rs.ApplicationPath;
import jakarta.ws.rs.core.Application; 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.HashSet;
import java.util.Set; import java.util.Set;
@ApplicationPath("api") @ApplicationPath("api")
@DeclareRoles({Roles.WRITER})
public class WxRsApplication extends Application { public class WxRsApplication extends Application {
@Override @Override
public Set<Class<?>> getClasses() { public Set<Class<?>> getClasses() {
Set<Class<?>> result = new HashSet<>(); Set<Class<?>> result = new HashSet<>();
result.add(PacmanWebService.class); result.add(PacmanWebService.class);
result.add(EJBAccessExceptionMapper.class);
return result; return result;
} }
} }

View File

@@ -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<EJBAccessException> {
@Override
public Response toResponse(EJBAccessException exception) {
return Response.status(Response.Status.UNAUTHORIZED).build();
}
}

View File

@@ -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<T> {
private final EntityManager em;
private final CriteriaBuilder cb;
private final CriteriaQuery<T> criteriaQuery;
private final Metamodel metamodel;
public QueryBuilder(EntityManager em, Class<T> cls) {
this.em = em;
this.cb = em.getCriteriaBuilder();
this.criteriaQuery = cb.createQuery(cls);
this.metamodel = em.getMetamodel();
}
public QueryBuilder<T> select(Selection<? extends T> a) {
criteriaQuery.select(a);
return this;
}
public <U> Root<U> from(Class<U> cls) {
return criteriaQuery.from(metamodel.entity(cls));
}
public TypedQuery<T> build() {
return em.createQuery(criteriaQuery);
}
}

View File

@@ -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
}

View File

@@ -0,0 +1,21 @@
<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_6_0.xsd"
version="6.0">
<login-config>
<auth-method>OIDC</auth-method>
</login-config>
<security-constraint>
<web-resource-collection>
<web-resource-name>login2</web-resource-name>
<url-pattern>/api/pkg/doYouWantAny</url-pattern>
<url-pattern>/api/pkg/upload</url-pattern>
</web-resource-collection>
<auth-constraint>
<role-name>jpacrepo</role-name>
</auth-constraint>
</security-constraint>
<security-role>
<role-name>jpacrepo</role-name>
</security-role>
</web-app>