various fixes

This commit is contained in:
2023-02-03 18:57:49 -05:00
parent a3dcee166d
commit bf7e93670e
12 changed files with 286 additions and 35 deletions

View File

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

View File

@@ -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<Map<String, Object>> getBalanceByPersonsAsJson(@Context SecurityContext securityContext) throws JsonProcessingException {
if (!securityContext.isUserInRole(this.eventId + "~finance")) throw new SecurityException("Not permitted");
List<DataSet> personsBalances = this.eventFinanceService.getPersonsBalances(this.eventId);
List<Map<String, Object>> 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<DataSet> personsBalances = this.eventFinanceService.getPersonsBalances(this.eventId);
return new StreamingOutput() {

View File

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

View File

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

View File

@@ -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<String> acSids;
private final Map<Long, Set<String>> eventAcSids;
public Person(com.poststats.security.Person person, Set<String> acSids, Map<Long, Set<String>> eventAcSids) {
super(person);
this.acSids = new HashSet<>(acSids);
this.acSids.addAll(person.getAccessControls());
this.eventAcSids = eventAcSids;
}
@Override
public Set<String> getAccessControls() {
return Collections.unmodifiableSet(this.acSids);
}
public Set<String> getEventAccessControls(long eventId) {
Set<String> roles = this.eventAcSids.get(eventId);
return roles == null ? null : Collections.unmodifiableSet(roles);
}
public Set<String> getAllAccessControls() {
Set<String> roles = new HashSet<>();
roles.addAll(this.acSids);
for (Entry<Long, Set<String>> 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<String> sids = this.eventAcSids.get(eventId);
return sids != null && sids.contains(ac);
}
}

View File

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

View File

@@ -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);

View File

@@ -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);

View File

@@ -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<String> queryAccessControls(Connection dbcon, long personId) throws SQLException {
FlexPreparedStatement fps = new FlexPreparedStatement(dbcon, SQL_SELECT_ACS);
try {
fps.setIntegerU(1, personId);
Set<String> set = new HashSet<>();
fps.executeQuery()
.getFirstColumn(String.class, set);
return set;
} finally {
fps.close();
}
}
private Map<Long, Set<String>> 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=?");
}

View File

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

View File

@@ -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<Long, SecurityContext> 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);
}
}

View File

@@ -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());