From bf7e93670e91301f6891c82338c029ecc3ebc824 Mon Sep 17 00:00:00 2001 From: Brian Long Date: Fri, 3 Feb 2023 18:57:49 -0500 Subject: [PATCH] various fixes --- .../com/poststats/golf/api/Constants.java | 13 +++ .../poststats/golf/api/EventFinanceApi.java | 20 ++-- .../com/poststats/golf/api/model/Event.java | 4 +- .../security/EventPersonSecurityContext.java | 15 ++- .../com/poststats/golf/security/Person.java | 50 ++++++++++ .../poststats/golf/service/PersonService.java | 12 +++ .../service/db/EventFinanceServiceDAO.java | 5 +- .../service/db/EventPersonServiceDAO.java | 7 +- .../golf/service/db/PersonServiceDAO.java | 99 +++++++++++++++++++ .../poststats/golf/servlet/EventFilter.java | 21 ++-- .../poststats/golf/servlet/PersonFilter.java | 69 +++++++++++++ .../poststats/golf/servlet/SeriesFilter.java | 6 +- 12 files changed, 286 insertions(+), 35 deletions(-) create mode 100644 src/main/java/com/poststats/golf/api/Constants.java create mode 100644 src/main/java/com/poststats/golf/security/Person.java create mode 100644 src/main/java/com/poststats/golf/service/PersonService.java create mode 100644 src/main/java/com/poststats/golf/service/db/PersonServiceDAO.java create mode 100644 src/main/java/com/poststats/golf/servlet/PersonFilter.java diff --git a/src/main/java/com/poststats/golf/api/Constants.java b/src/main/java/com/poststats/golf/api/Constants.java new file mode 100644 index 0000000..2083b8e --- /dev/null +++ b/src/main/java/com/poststats/golf/api/Constants.java @@ -0,0 +1,13 @@ +package com.poststats.golf.api; + +public class Constants extends com.poststats.api.Constants { + + public static final String EVENT_ROLE_PREFIX = "event:"; + public static final String EVENT_SERIES_ROLE_PREFIX = "series:"; + public static final String COURSE_ROLE_PREFIX = "course:"; + + public static final String EVENT_ID = "eventId"; + public static final String EVENT_SERIES_ID = "seriesId"; + public static final String COURSE_ID = "courseId"; + +} diff --git a/src/main/java/com/poststats/golf/api/EventFinanceApi.java b/src/main/java/com/poststats/golf/api/EventFinanceApi.java index 24347fc..ad9f239 100644 --- a/src/main/java/com/poststats/golf/api/EventFinanceApi.java +++ b/src/main/java/com/poststats/golf/api/EventFinanceApi.java @@ -2,9 +2,9 @@ package com.poststats.golf.api; import com.brianlong.sql.DataSet; import com.fasterxml.jackson.core.JsonProcessingException; -import com.poststats.api.Constants; import com.poststats.golf.service.EventFinanceService; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; @@ -42,16 +42,15 @@ public class EventFinanceApi { @GET @Path("/balance/persons") - @RolesAllowed("member") + @RolesAllowed(Constants.EVENT_ROLE_PREFIX + "finance") @Produces(Constants.V1_JSON) @Operation(summary = "Retrieves the balances of all participants in an event.") @ApiResponses({ - @ApiResponse(responseCode = "200", description = "Success"), - @ApiResponse(responseCode = "404", description = "An event with the specified ID could not be found") + @ApiResponse(responseCode = "200", description = "Success", content = { + @Content(mediaType = Constants.V1_JSON), @Content(mediaType = "text/csv") + }), @ApiResponse(responseCode = "404", description = "An event with the specified ID could not be found") }) public List> getBalanceByPersonsAsJson(@Context SecurityContext securityContext) throws JsonProcessingException { - if (!securityContext.isUserInRole(this.eventId + "~finance")) throw new SecurityException("Not permitted"); - List personsBalances = this.eventFinanceService.getPersonsBalances(this.eventId); List> personsBalancesJson = new ArrayList<>(personsBalances.size()); @@ -69,16 +68,9 @@ public class EventFinanceApi { @GET @Path("/balance/persons") - @RolesAllowed("member") + @RolesAllowed(Constants.EVENT_ROLE_PREFIX + "finance") @Produces("text/csv") - @Operation(summary = "Retrieves the balances of all participants in an event.") - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "Success"), - @ApiResponse(responseCode = "404", description = "An event with the specified ID could not be found") - }) public StreamingOutput getBalanceByPersonsAsCsv(@Context SecurityContext securityContext) throws IOException { - if (!securityContext.isUserInRole(this.eventId + "~finance")) throw new SecurityException("Not permitted"); - List personsBalances = this.eventFinanceService.getPersonsBalances(this.eventId); return new StreamingOutput() { diff --git a/src/main/java/com/poststats/golf/api/model/Event.java b/src/main/java/com/poststats/golf/api/model/Event.java index 72f7183..06cb069 100644 --- a/src/main/java/com/poststats/golf/api/model/Event.java +++ b/src/main/java/com/poststats/golf/api/model/Event.java @@ -18,10 +18,10 @@ public class Event { private String name; @JsonProperty private String location; - @JsonProperty + @JsonProperty("begins") @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "YYYY-MM-dd") private LocalDate liveline; - @JsonProperty + @JsonProperty("ends") @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "YYYY-MM-dd") private LocalDate deadline; @JsonProperty diff --git a/src/main/java/com/poststats/golf/security/EventPersonSecurityContext.java b/src/main/java/com/poststats/golf/security/EventPersonSecurityContext.java index dc8fdd6..8dedc49 100644 --- a/src/main/java/com/poststats/golf/security/EventPersonSecurityContext.java +++ b/src/main/java/com/poststats/golf/security/EventPersonSecurityContext.java @@ -1,11 +1,14 @@ package com.poststats.golf.security; -import com.poststats.security.Person; +import com.poststats.golf.api.Constants; import jakarta.ws.rs.core.SecurityContext; import java.security.Principal; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; public class EventPersonSecurityContext implements SecurityContext { + private final Logger logger = LoggerFactory.getLogger(this.getClass()); private final SecurityContext securityContext; private final long eventId; @@ -21,8 +24,16 @@ public class EventPersonSecurityContext implements SecurityContext { @Override public boolean isUserInRole(String role) { + this.logger.trace("Checking if user {} in is in role {} for event {}", this.securityContext.getUserPrincipal() + .getName(), role, this.eventId); Person person = (Person) this.securityContext.getUserPrincipal(); - return person == null ? false : person.hasAccessControl(role, this.eventId); + if (person == null) { + return false; + } else if (role.startsWith(Constants.EVENT_ROLE_PREFIX)) { + return person.hasAccessControl(role.substring(Constants.EVENT_ROLE_PREFIX.length()), this.eventId); + } else { + return person.hasAccessControl(role); + } } @Override diff --git a/src/main/java/com/poststats/golf/security/Person.java b/src/main/java/com/poststats/golf/security/Person.java new file mode 100644 index 0000000..e31d109 --- /dev/null +++ b/src/main/java/com/poststats/golf/security/Person.java @@ -0,0 +1,50 @@ +package com.poststats.golf.security; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + +public class Person extends com.poststats.security.Person { + + private final Set acSids; + private final Map> eventAcSids; + + public Person(com.poststats.security.Person person, Set acSids, Map> eventAcSids) { + super(person); + this.acSids = new HashSet<>(acSids); + this.acSids.addAll(person.getAccessControls()); + this.eventAcSids = eventAcSids; + } + + @Override + public Set getAccessControls() { + return Collections.unmodifiableSet(this.acSids); + } + + public Set getEventAccessControls(long eventId) { + Set roles = this.eventAcSids.get(eventId); + return roles == null ? null : Collections.unmodifiableSet(roles); + } + + public Set getAllAccessControls() { + Set roles = new HashSet<>(); + roles.addAll(this.acSids); + for (Entry> eroles : this.eventAcSids.entrySet()) + for (String role : eroles.getValue()) + roles.add(eroles.getKey() + ":" + role); + return roles == null ? null : Collections.unmodifiableSet(roles); + } + + @Override + public boolean hasAccessControl(String ac) { + return this.acSids.contains(ac); + } + + public boolean hasAccessControl(String ac, long eventId) { + Set sids = this.eventAcSids.get(eventId); + return sids != null && sids.contains(ac); + } + +} diff --git a/src/main/java/com/poststats/golf/service/PersonService.java b/src/main/java/com/poststats/golf/service/PersonService.java new file mode 100644 index 0000000..b6c7e20 --- /dev/null +++ b/src/main/java/com/poststats/golf/service/PersonService.java @@ -0,0 +1,12 @@ +package com.poststats.golf.service; + +import com.brianlong.sql.DataSet; +import com.poststats.golf.security.Person; + +public interface PersonService { + + Person buildUserPrincipal(com.poststats.security.Person person); + + DataSet get(long personId); + +} diff --git a/src/main/java/com/poststats/golf/service/db/EventFinanceServiceDAO.java b/src/main/java/com/poststats/golf/service/db/EventFinanceServiceDAO.java index 2ba2760..12abbb4 100644 --- a/src/main/java/com/poststats/golf/service/db/EventFinanceServiceDAO.java +++ b/src/main/java/com/poststats/golf/service/db/EventFinanceServiceDAO.java @@ -4,11 +4,10 @@ import com.brianlong.sql.DataSet; import com.brianlong.sql.FlexPreparedStatement; import com.poststats.golf.service.EventFinanceService; import com.poststats.golf.sql.GolfSQL; +import com.poststats.service.ServiceException; import com.poststats.sql.PostStatsDataSource; import jakarta.annotation.PostConstruct; import jakarta.enterprise.context.ApplicationScoped; -import jakarta.ws.rs.WebApplicationException; -import jakarta.ws.rs.core.Response.Status; import java.sql.Connection; import java.sql.SQLException; import java.util.List; @@ -28,7 +27,7 @@ public class EventFinanceServiceDAO implements EventFinanceService { try { return this.queryPersonsBalances(dbcon, eventId); } catch (SQLException se) { - throw new WebApplicationException("Database call failure", se, Status.INTERNAL_SERVER_ERROR); + throw new ServiceException(se); } finally { PostStatsDataSource.getInstance() .release(dbcon); diff --git a/src/main/java/com/poststats/golf/service/db/EventPersonServiceDAO.java b/src/main/java/com/poststats/golf/service/db/EventPersonServiceDAO.java index 5044efc..b00f1f4 100644 --- a/src/main/java/com/poststats/golf/service/db/EventPersonServiceDAO.java +++ b/src/main/java/com/poststats/golf/service/db/EventPersonServiceDAO.java @@ -2,6 +2,7 @@ package com.poststats.golf.service.db; import com.brianlong.sql.DataSet; import com.poststats.golf.service.EventPersonService; +import com.poststats.service.ServiceException; import com.poststats.sql.PostStatsDataSource; import jakarta.annotation.PostConstruct; import jakarta.enterprise.context.ApplicationScoped; @@ -27,7 +28,7 @@ public class EventPersonServiceDAO implements EventPersonService { try { return this.queryPersons(dbcon, eventId); } catch (SQLException se) { - throw new WebApplicationException("Database call failure", se, Status.INTERNAL_SERVER_ERROR); + throw new ServiceException(se); } finally { PostStatsDataSource.getInstance() .release(dbcon); @@ -41,7 +42,7 @@ public class EventPersonServiceDAO implements EventPersonService { try { return this.queryParticipants(dbcon, eventId); } catch (SQLException se) { - throw new WebApplicationException("Database call failure", se, Status.INTERNAL_SERVER_ERROR); + throw new ServiceException(se); } finally { PostStatsDataSource.getInstance() .release(dbcon); @@ -55,7 +56,7 @@ public class EventPersonServiceDAO implements EventPersonService { try { return this.querySeriesEventIdsAsParticipant(dbcon, seriesId, personId); } catch (SQLException se) { - throw new WebApplicationException("Database call failure", se, Status.INTERNAL_SERVER_ERROR); + throw new ServiceException(se); } finally { PostStatsDataSource.getInstance() .release(dbcon); diff --git a/src/main/java/com/poststats/golf/service/db/PersonServiceDAO.java b/src/main/java/com/poststats/golf/service/db/PersonServiceDAO.java new file mode 100644 index 0000000..fdbdb4d --- /dev/null +++ b/src/main/java/com/poststats/golf/service/db/PersonServiceDAO.java @@ -0,0 +1,99 @@ +package com.poststats.golf.service.db; + +import com.brianlong.sql.DataSet; +import com.brianlong.sql.FlexPreparedStatement; +import com.poststats.golf.security.Person; +import com.poststats.golf.service.PersonService; +import com.poststats.golf.sql.GolfDataSource; +import com.poststats.golf.sql.GolfSQL; +import com.poststats.service.ServiceException; +import jakarta.enterprise.context.ApplicationScoped; +import java.sql.Connection; +import java.sql.SQLException; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +@ApplicationScoped +public class PersonServiceDAO implements PersonService { + + @Override + public Person buildUserPrincipal(com.poststats.security.Person person) { + Connection dbcon = GolfDataSource.getInstance() + .acquire(true); + try { + return new Person(person, this.queryAccessControls(dbcon, person.getId()), this.queryEventAccessControls(dbcon, person.getId())); + } catch (SQLException se) { + throw new ServiceException(se); + } finally { + GolfDataSource.getInstance() + .release(dbcon); + } + } + + @Override + public DataSet get(long personId) { + Connection dbcon = GolfDataSource.getInstance() + .acquire(true); + try { + return this.queryBasics(dbcon, personId); + } catch (SQLException se) { + throw new ServiceException(se); + } finally { + GolfDataSource.getInstance() + .release(dbcon); + } + } + + private DataSet queryBasics(Connection dbcon, long personId) throws SQLException { + FlexPreparedStatement fps = new FlexPreparedStatement(dbcon, GolfSQL.changeSchema( + "SELECT * FROM ~g~.Person WHERE personID=?")); + try { + fps.setIntegerU(1, personId); + return fps.executeQuery() + .getNextRow(); + } finally { + fps.close(); + } + } + + private Set queryAccessControls(Connection dbcon, long personId) throws SQLException { + FlexPreparedStatement fps = new FlexPreparedStatement(dbcon, SQL_SELECT_ACS); + try { + fps.setIntegerU(1, personId); + + Set set = new HashSet<>(); + fps.executeQuery() + .getFirstColumn(String.class, set); + return set; + } finally { + fps.close(); + } + } + + private Map> queryEventAccessControls(Connection dbcon, long personId) throws SQLException { + FlexPreparedStatement fps = new FlexPreparedStatement(dbcon, SQL_SELECT_EVENT_ACS); + try { + fps.setIntegerU(1, personId); + return fps.executeQuery() + .getFirstTwoColumnsOneToMany(Long.class, String.class); + } finally { + fps.close(); + } + } + + + + private static final String SQL_SELECT_ACS = GolfSQL.changeSchema( + "SELECT AC.acSID " + + "FROM ~g~.PersonAccessControl PAC " + + " INNER JOIN ~p~.AccessControl AC ON (PAC.acID=AC.acID) " + + "WHERE PAC.personID=?"); + + private static final String SQL_SELECT_EVENT_ACS = GolfSQL.changeSchema( + "SELECT EPAC.eventID, AC.acSID " + + "FROM ~g~.EventPersonAccessControl EPAC " + + " INNER JOIN ~p~.AccessControl AC ON (EPAC.acID=AC.acID) " + + "WHERE EPAC.personID=?"); + +} diff --git a/src/main/java/com/poststats/golf/servlet/EventFilter.java b/src/main/java/com/poststats/golf/servlet/EventFilter.java index b816722..f13e695 100644 --- a/src/main/java/com/poststats/golf/servlet/EventFilter.java +++ b/src/main/java/com/poststats/golf/servlet/EventFilter.java @@ -1,14 +1,14 @@ package com.poststats.golf.servlet; import com.brianlong.util.StringUtil; -import com.poststats.golf.Constants; +import com.poststats.golf.api.Constants; import com.poststats.golf.security.EventPersonSecurityContext; +import com.poststats.golf.security.Person; import jakarta.annotation.Priority; import jakarta.enterprise.context.ApplicationScoped; import jakarta.ws.rs.Priorities; import jakarta.ws.rs.container.ContainerRequestContext; import jakarta.ws.rs.container.ContainerRequestFilter; -import jakarta.ws.rs.container.PreMatching; import jakarta.ws.rs.core.SecurityContext; import jakarta.ws.rs.ext.Provider; import java.io.IOException; @@ -17,8 +17,7 @@ import org.slf4j.LoggerFactory; @ApplicationScoped @Provider -@PreMatching -@Priority(Priorities.AUTHORIZATION + 10) +@Priority(Priorities.AUTHORIZATION - 5) public class EventFilter implements ContainerRequestFilter { private final Logger logger = LoggerFactory.getLogger(this.getClass()); @@ -47,10 +46,18 @@ public class EventFilter implements ContainerRequestFilter { requestContext.setProperty(Constants.EVENT_ID, eventId); SecurityContext scontext = requestContext.getSecurityContext(); - if (scontext.getUserPrincipal() != null) { - this.logger.debug("Narrowing authorization for event: {} => {}", scontext.getUserPrincipal(), eventId); - requestContext.setSecurityContext(new EventPersonSecurityContext(scontext, eventId)); + if (scontext.getUserPrincipal() == null) return; + + this.logger.debug("Narrowing authorization for event: {} => {}", scontext.getUserPrincipal(), eventId); + + EventPersonSecurityContext epscontext = new EventPersonSecurityContext(scontext, eventId); + + if (this.logger.isTraceEnabled()) { + Person person = (Person) epscontext.getUserPrincipal(); + this.logger.trace("Authorized event roles: {} => {}", person.getId(), person.getEventAccessControls(eventId)); } + + requestContext.setSecurityContext(epscontext); } } diff --git a/src/main/java/com/poststats/golf/servlet/PersonFilter.java b/src/main/java/com/poststats/golf/servlet/PersonFilter.java new file mode 100644 index 0000000..e2e6367 --- /dev/null +++ b/src/main/java/com/poststats/golf/servlet/PersonFilter.java @@ -0,0 +1,69 @@ +package com.poststats.golf.servlet; + +import com.brianlong.cache.CacheException; +import com.brianlong.cache.CacheRetrievalException; +import com.brianlong.cache.MemoryCacher; +import com.brianlong.cache.Reaper; +import com.poststats.golf.service.PersonService; +import com.poststats.security.Person; +import com.poststats.security.PersonSecurityContext; +import jakarta.annotation.Priority; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.ws.rs.Priorities; +import jakarta.ws.rs.container.ContainerRequestContext; +import jakarta.ws.rs.container.ContainerRequestFilter; +import jakarta.ws.rs.core.SecurityContext; +import jakarta.ws.rs.ext.Provider; +import java.io.IOException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@ApplicationScoped +@Provider +@Priority(Priorities.AUTHORIZATION - 10) +public class PersonFilter implements ContainerRequestFilter { + + private final Logger logger = LoggerFactory.getLogger(this.getClass()); + + private MemoryCacher cacher = new MemoryCacher<>(60000L, new Reaper(30000L)); + + @Inject + private PersonService personService; + + @Override + public void filter(ContainerRequestContext requestContext) throws IOException { + SecurityContext scontext = requestContext.getSecurityContext(); + if (scontext.getUserPrincipal() == null) return; + + Person person = (Person) scontext.getUserPrincipal(); + try { + scontext = this.cacher.get(person.getId()); + } catch (CacheRetrievalException cre) { + this.logger.warn("This should never happen; if it does, skip cache", cre); + scontext = null; + } + + if (scontext != null) { + this.logger.debug("Using cached security context: {}", scontext.getUserPrincipal()); + } else { + this.logger.debug("Gathering roles for golf: {}", person); + + com.poststats.golf.security.Person gperson = this.personService.buildUserPrincipal(person); + + if (this.logger.isTraceEnabled()) { + this.logger.trace("Authorized roles: {} => {}", gperson.getId(), gperson.getAllAccessControls()); + } + + scontext = new PersonSecurityContext(gperson); + try { + this.cacher.add(person.getId(), scontext, false); + } catch (CacheException ce) { + this.logger.warn("This should never happen; if it does, caching failed", ce); + } + } + + requestContext.setSecurityContext(scontext); + } + +} diff --git a/src/main/java/com/poststats/golf/servlet/SeriesFilter.java b/src/main/java/com/poststats/golf/servlet/SeriesFilter.java index bdbde6a..5567682 100644 --- a/src/main/java/com/poststats/golf/servlet/SeriesFilter.java +++ b/src/main/java/com/poststats/golf/servlet/SeriesFilter.java @@ -1,13 +1,12 @@ package com.poststats.golf.servlet; import com.brianlong.util.StringUtil; -import com.poststats.golf.Constants; +import com.poststats.golf.api.Constants; import jakarta.annotation.Priority; import jakarta.enterprise.context.ApplicationScoped; import jakarta.ws.rs.Priorities; import jakarta.ws.rs.container.ContainerRequestContext; import jakarta.ws.rs.container.ContainerRequestFilter; -import jakarta.ws.rs.container.PreMatching; import jakarta.ws.rs.ext.Provider; import java.io.IOException; import org.slf4j.Logger; @@ -15,8 +14,7 @@ import org.slf4j.LoggerFactory; @ApplicationScoped @Provider -@PreMatching -@Priority(Priorities.AUTHORIZATION + 5) +@Priority(Priorities.HEADER_DECORATOR + 10) public class SeriesFilter implements ContainerRequestFilter { private final Logger logger = LoggerFactory.getLogger(this.getClass());