Skip to content
Snippets Groups Projects
Commit d1911902 authored by Tucker Gary Siegel's avatar Tucker Gary Siegel
Browse files

Merge branch 'addition' into 'develop'

add new features

See merge request !5
parents c65c3f22 4fd13deb
No related branches found
No related tags found
1 merge request!5add new features
Showing
with 517 additions and 6 deletions
...@@ -30,7 +30,14 @@ dependencies { ...@@ -30,7 +30,14 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-log4j2:3.0.4' implementation 'org.springframework.boot:spring-boot-starter-log4j2:3.0.4'
implementation 'org.springframework.boot:spring-boot-starter-actuator' implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.4' implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.4'
implementation 'jakarta.validation:jakarta.validation-api:3.0.2' // common
implementation 'com.google.guava:guava:31.1-jre' // common
implementation 'org.apache.commons:commons-text:1.10.0' // common
implementation 'com.auth0:java-jwt:4.3.0' // common
implementation 'org.aspectj:aspectjweaver:1.9.19' // common
implementation 'org.aspectj:aspectjrt:1.9.19' // common
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.15.0-rc1' implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.15.0-rc1'
testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.boot:spring-boot-starter-test'
} }
......
package edu.umd.dawn.common.annotations;
import java.lang.annotation.Annotation;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
/**
* base class for custom aspects for annotations. includes some helper functions
*/
@Aspect
@Component
public class AspectBase<T extends Annotation> {
@Pointcut(value = "execution(* *.*(..))")
protected void allMethods() {}
protected String getFullDescriptor(ProceedingJoinPoint jointPoint) {
String className = ((MethodSignature) jointPoint.getSignature()).getDeclaringTypeName();
String methodName =
((MethodSignature) jointPoint.getSignature()).getMethod().getName();
return className + "." + methodName;
}
protected T grabAnnotation(ProceedingJoinPoint jointPoint, Class<T> clazz) {
return ((MethodSignature) jointPoint.getSignature()).getMethod().getAnnotation(clazz);
}
protected T grabAnnotationFromClass(ProceedingJoinPoint jointPoint, Class<T> clazz) {
return (T)
((MethodSignature) jointPoint.getSignature()).getDeclaringType().getAnnotation(clazz);
}
// protected T grabMethodArgument(ProceedingJoinPoint jointPoint, Class<T> clazz) {
// return (T)
// ((MethodSignature) jointPoint.getSignature()).getDeclaringType().getAnnotation(clazz);
// }
}
package edu.umd.dawn.common.annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* This serves as a stronger deprecation notice - will report warnings
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Deprecated {
public String value() default "";
}
package edu.umd.dawn.common.annotations;
import lombok.extern.log4j.Log4j2;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
@Aspect
@Component
@Log4j2
public class DeprecatedAspect extends AspectBase<Deprecated> {
@Around("@annotation(Deprecated)")
public Object run(ProceedingJoinPoint jointPoint) throws Exception, Throwable {
Deprecated annotation = grabAnnotation(jointPoint, Deprecated.class);
String descriptor = getFullDescriptor(jointPoint);
if (!annotation.value().equals("")) {
log.warn(String.format("method %s is deprecated - reason: %s", descriptor, annotation.value()));
} else {
log.warn(String.format("method %s is deprecated", descriptor));
}
Object proceed = jointPoint.proceed();
return proceed;
}
}
package edu.umd.dawn.common.annotations;
import edu.umd.dawn.common.enums.Role;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RoleRestriction {
public Role value() default Role.USER;
boolean disableWarning() default false;
}
package edu.umd.dawn.common.annotations;
import edu.umd.dawn.common.exceptions.BaseExceptions;
import edu.umd.dawn.common.exceptions.DawnException;
import edu.umd.dawn.common.jwt.Claims;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Aspect
@Component
@Log4j2
@RequiredArgsConstructor(onConstructor = @__(@Autowired)) // Autowired annotated lombok generated constructor
public class RoleRestrictionAspect extends AspectBase<RoleRestriction> {
private final HttpServletRequest request;
@Value("${config.local}")
private boolean local = false;
@Around("@annotation(RoleRestriction)")
public Object run(ProceedingJoinPoint jointPoint) throws Exception, Throwable {
RoleRestriction annotation = grabAnnotation(jointPoint, RoleRestriction.class);
if (!local) {
Claims claims = (Claims) request.getAttribute("claims");
if (claims == null) {
throw new DawnException(BaseExceptions.FORBIDDEN);
}
// call db to check
} else if (!annotation.disableWarning()) {
log.warn("RoleRestriction annotation has been disabled - if this is a production environment, consider this"
+ " a critical security error. Request with unknown role accessed endpoint with restriction "
+ annotation.value().toString() + ".");
}
Object proceed = jointPoint.proceed();
return proceed;
}
}
package edu.umd.dawn.common.annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.apache.logging.log4j.spi.StandardLevel;
/**
* Traces entry and exit from functions at specified log level
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Traceable {
StandardLevel level() default StandardLevel.TRACE;
}
package edu.umd.dawn.common.annotations;
import lombok.extern.log4j.Log4j2;
import org.apache.logging.log4j.Level;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
/**
* Traces entry and exit from functions at specified log level
*/
@Log4j2
@Aspect
@Component
public class TraceableAspect extends AspectBase<Traceable> {
@Around("@annotation(Traceable)")
public Object run(ProceedingJoinPoint jointPoint) throws Exception, Throwable {
Traceable traceable = grabAnnotation(jointPoint, Traceable.class);
String descriptor = getFullDescriptor(jointPoint);
log.log(
Level.forName(traceable.level().name(), traceable.level().intLevel()),
String.format("entering %s", descriptor));
Object proceed = jointPoint.proceed();
log.info(String.format("exiting %s", descriptor));
return proceed;
}
}
package edu.umd.dawn.common.annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* warns that a method is unfinished
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface Unfinished {
String value() default "";
}
package edu.umd.dawn.common.annotations;
import lombok.extern.log4j.Log4j2;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
/**
* warns that a method is unfinished
*/
@Aspect
@Component
@Log4j2
public class UnfinishedAspect extends AspectBase<Unfinished> {
@Around("@annotation(Unfinished)")
public Object run(ProceedingJoinPoint jointPoint) throws Exception, Throwable {
Unfinished unfinished = grabAnnotation(jointPoint, Unfinished.class);
String descriptor = getFullDescriptor(jointPoint);
if (!unfinished.value().equals("")) {
log.warn(String.format("method %s is unfinished - reason: %s", descriptor, unfinished.value()));
} else {
log.warn(String.format("method %s is unfinished", descriptor));
}
Object proceed = jointPoint.proceed();
return proceed;
}
}
package edu.umd.dawn.common.annotations;
import java.lang.annotation.Annotation;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.reflect.MethodSignature;
public class Utils<T> {
public static String getFullDescriptor(ProceedingJoinPoint jointPoint) {
String className = ((MethodSignature) jointPoint.getSignature()).getDeclaringTypeName();
String methodName =
((MethodSignature) jointPoint.getSignature()).getMethod().getName();
return className + "." + methodName;
}
public static <T extends Annotation> T grabAnnotation(ProceedingJoinPoint jointPoint, Class<T> clazz) {
return ((MethodSignature) jointPoint.getSignature()).getMethod().getAnnotation(clazz);
}
}
package edu.umd.dawn.common.converter;
import org.springframework.core.convert.converter.Converter;
public class CaseInsensitiveEnumConverter<T extends Enum<T>> implements Converter<String, T> {
private Class<T> enumClass;
public CaseInsensitiveEnumConverter(Class<T> enumClass) {
this.enumClass = enumClass;
}
@Override
public T convert(String from) {
return T.valueOf(enumClass, from.toUpperCase());
}
}
package edu.umd.dawn.common.enums;
public enum Role {
USER(0),
REPORTER(1),
ADMIN(2),
SUPER(3);
private int level;
private Role(int level) {
this.level = level;
}
public static Role fromInt(int level) {
return Role.values()[level];
}
public boolean isAccessAllowed(Role minimum) {
return minimum.level <= level;
}
}
...@@ -9,4 +9,18 @@ public class BaseExceptions { ...@@ -9,4 +9,18 @@ public class BaseExceptions {
new DawnExceptionParameters(500, "INTERNAL_SERVER_ERROR", "Internal server error", "out of bounds"); new DawnExceptionParameters(500, "INTERNAL_SERVER_ERROR", "Internal server error", "out of bounds");
public static final DawnExceptionParameters UNHANDLED_INTERNAL_SERVER_ERROR = public static final DawnExceptionParameters UNHANDLED_INTERNAL_SERVER_ERROR =
new DawnExceptionParameters(500, "INTERNAL_SERVER_ERROR", "Internal server error", "unhandled error"); new DawnExceptionParameters(500, "INTERNAL_SERVER_ERROR", "Internal server error", "unhandled error");
public static final DawnExceptionParameters FORBIDDEN =
new DawnExceptionParameters(403, "FORBIDDEN", "user cannot access requested resource", "");
public static final DawnExceptionParameters INVALID_JWT = new DawnExceptionParameters(
401, "UNAUTHORIZED", "Invalid or empty JWT provided", "JWT provided is invalid");
public static final DawnExceptionParameters NOT_FOUND =
new DawnExceptionParameters(404, "NOT_FOUND", "Resource not found", "");
public static DawnExceptionParameters INVALID_LIMIT(int limit) {
return new DawnExceptionParameters(400, "BAD_REQUEST", String.format("limit of %d is invalid", limit), "");
}
public static DawnExceptionParameters INVALID_OFFSET(int offset) {
return new DawnExceptionParameters(400, "BAD_REQUEST", String.format("offset of %d is invalid", offset), "");
}
} }
...@@ -5,7 +5,6 @@ import java.io.StringWriter; ...@@ -5,7 +5,6 @@ import java.io.StringWriter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.slf4j.MDC; import org.slf4j.MDC;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.validation.BindException; import org.springframework.validation.BindException;
...@@ -55,9 +54,11 @@ public class CustomExceptionHandler { ...@@ -55,9 +54,11 @@ public class CustomExceptionHandler {
@ExceptionHandler(BindException.class) @ExceptionHandler(BindException.class)
protected ResponseEntity<Object> handleBindException(BindException ex, WebRequest request) { protected ResponseEntity<Object> handleBindException(BindException ex, WebRequest request) {
FieldError err = ex.getFieldError(); FieldError err = ex.getFieldError();
DawnException wrapped = new DawnException(BaseExceptions.BAD_REQUEST, ex, "Value " + err.getRejectedValue() + " is invalid for field " + err.getField()); DawnException wrapped = new DawnException(
BaseExceptions.BAD_REQUEST,
ex,
"Value " + err.getRejectedValue() + " is invalid for field " + err.getField());
return returnDawnException(wrapped); return returnDawnException(wrapped);
} }
......
package edu.umd.dawn.common.filters;
import com.google.common.base.CaseFormat;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequestWrapper;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import lombok.extern.log4j.Log4j2;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
/**
* This filter will convert any underscore case variable to camelcase.
* It is intended to support backwards compatibility with golang requests which had variables written
* like user_id instead of userId.
*/
@Log4j2
@Component
public class CaseInsensitiveFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
final Map<String, String[]> formattedParams = new ConcurrentHashMap<>();
List<String> invalidParameterCase = new ArrayList<>();
for (String param : request.getParameterMap().keySet()) {
String formattedParam = param;
if (param.contains("_")) {
invalidParameterCase.add(param);
formattedParam = CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, param);
}
formattedParams.put(formattedParam, request.getParameterValues(param));
}
filterChain.doFilter(
new HttpServletRequestWrapper(request) {
@Override
public String getParameter(String name) {
return formattedParams.containsKey(name)
? formattedParams.get(name)[0]
: null;
}
@Override
public Enumeration<String> getParameterNames() {
return Collections.enumeration(formattedParams.keySet());
}
@Override
public String[] getParameterValues(String name) {
return formattedParams.get(name);
}
@Override
public Map<String, String[]> getParameterMap() {
return formattedParams;
}
},
response);
if (invalidParameterCase.size() > 0) {
log.warn(String.format(
"client is still using deprecated naming scheme for parameter(s) %s",
invalidParameterCase.toString()));
}
}
}
...@@ -68,7 +68,7 @@ public class RequestInterceptor implements HandlerInterceptor { ...@@ -68,7 +68,7 @@ public class RequestInterceptor implements HandlerInterceptor {
Object p = request.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE); Object p = request.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
if (p != null) { if (p != null) {
Map<String, String> pathParams = (Map<String, String>)p; Map<String, String> pathParams = (Map<String, String>) p;
pathParams.forEach((k, v) -> parameters.merge(k, v, String::concat)); pathParams.forEach((k, v) -> parameters.merge(k, v, String::concat));
} }
...@@ -83,8 +83,7 @@ public class RequestInterceptor implements HandlerInterceptor { ...@@ -83,8 +83,7 @@ public class RequestInterceptor implements HandlerInterceptor {
long executeTime = endTime - startTime; long executeTime = endTime - startTime;
String fullPath = request.getRequestURI(); String fullPath = request.getRequestURI();
String servletPattern = (String) request.getAttribute( String servletPattern = (String) request.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE);
HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE);
String servletPath = request.getServletPath(); String servletPath = request.getServletPath();
String path = fullPath.replace(servletPath, servletPattern); String path = fullPath.replace(servletPath, servletPattern);
......
package edu.umd.dawn.common.jwt;
import com.auth0.jwt.interfaces.DecodedJWT;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public class Claims {
private String userId;
public static Claims build(JWTUtil parser) {
return new Claims(parser.getJwt().getClaim("id").asString());
}
public static Claims build(DecodedJWT jwt) {
return new Claims(jwt.getClaim("id").asString());
}
}
package edu.umd.dawn.common.jwt;
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import edu.umd.dawn.common.exceptions.BaseExceptions;
import edu.umd.dawn.common.exceptions.DawnException;
import lombok.Getter;
@Getter
public class JWTUtil {
private Algorithm algorithm;
private String accessSecret;
private String jwtString;
private DecodedJWT jwt;
public JWTUtil(String accessSecret, String jwt) {
this.accessSecret = accessSecret;
this.jwtString = jwt;
initAlgorithm();
decode();
}
private void initAlgorithm() {
this.algorithm = Algorithm.HMAC256(accessSecret);
}
private void decode() {
try {
this.jwt = JWT.require(algorithm)
.acceptLeeway(1) // 1 sec for nbf and iat
.acceptExpiresAt(5) // 5 secs for exp
.withClaimPresence("id") // need id in the claim
.build()
.verify(jwtString);
} catch (Exception e) {
throw new DawnException(BaseExceptions.INVALID_JWT, e);
}
}
public Claims getClaims() {
return Claims.build(this);
}
public static JWTUtil parse(String accessSecret, String jwt) {
return new JWTUtil(accessSecret, jwt);
}
}
package edu.umd.dawn.common.services;
import com.mongodb.client.MongoClient;
import com.mongodb.client.MongoClients;
import com.mongodb.client.MongoCollection;
import com.mongodb.client.MongoDatabase;
import edu.umd.dawn.common.enums.Role;
import edu.umd.dawn.common.exceptions.BaseExceptions;
import edu.umd.dawn.common.exceptions.DawnException;
import java.util.Optional;
import org.bson.Document;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Component
public class UserAuthService {
private MongoClient mongoClient;
private MongoDatabase mongoDatabase;
@Value("${common.annotation.mongodb.uri}")
private String uri;
@Value("${common.annotation.mongodb.database}")
private String db;
protected MongoDatabase getDatabase() {
if (mongoClient == null) {
mongoClient = MongoClients.create(uri);
}
if (mongoDatabase == null) {
mongoDatabase = mongoClient.getDatabase(db);
}
return mongoDatabase;
}
public Role getUserRole(String userId) {
MongoDatabase database = getDatabase();
MongoCollection<Document> collection = database.getCollection("users");
Document query = new Document();
query.put("_id", userId);
// should prob throw an auth type error
Document doc = Optional.of(collection.find(query).first())
.orElseThrow(() -> new DawnException(BaseExceptions.NOT_FOUND));
int role = (int) doc.get("role");
return Role.fromInt(role);
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment