various fixes
This commit is contained in:
13
src/main/java/com/poststats/golf/api/Constants.java
Normal file
13
src/main/java/com/poststats/golf/api/Constants.java
Normal 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";
|
||||
|
||||
}
|
@@ -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() {
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
50
src/main/java/com/poststats/golf/security/Person.java
Normal file
50
src/main/java/com/poststats/golf/security/Person.java
Normal 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);
|
||||
}
|
||||
|
||||
}
|
12
src/main/java/com/poststats/golf/service/PersonService.java
Normal file
12
src/main/java/com/poststats/golf/service/PersonService.java
Normal 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);
|
||||
|
||||
}
|
@@ -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);
|
||||
|
@@ -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);
|
||||
|
@@ -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=?");
|
||||
|
||||
}
|
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
69
src/main/java/com/poststats/golf/servlet/PersonFilter.java
Normal file
69
src/main/java/com/poststats/golf/servlet/PersonFilter.java
Normal 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);
|
||||
}
|
||||
|
||||
}
|
@@ -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());
|
||||
|
Reference in New Issue
Block a user