implemented UserLock annotation

has SPeL parsing for getting the args from the redirect from the method parameters
This commit is contained in:
Jake Potrebic 2020-09-06 17:38:10 -07:00
parent 2e596e7591
commit b14bf1bdfc
No known key found for this signature in database
GPG Key ID: 7C58557EC9C421F8
13 changed files with 257 additions and 14 deletions

View File

@ -3,9 +3,12 @@ package io.papermc.hangar.config;
import io.papermc.hangar.security.metadatasources.GlobalPermissionSource;
import io.papermc.hangar.security.metadatasources.HangarMetadataSources;
import io.papermc.hangar.security.metadatasources.ProjectPermissionSource;
import io.papermc.hangar.security.metadatasources.UserLockSource;
import io.papermc.hangar.security.voters.GlobalPermissionVoter;
import io.papermc.hangar.security.voters.ProjectPermissionVoter;
import io.papermc.hangar.security.voters.UserLockVoter;
import io.papermc.hangar.service.PermissionService;
import io.papermc.hangar.service.UserService;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
@ -34,16 +37,18 @@ public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {
private final ApplicationContext applicationContext;
private final PermissionService permissionService;
private final UserService userService;
@Autowired
public MethodSecurityConfig(ApplicationContext applicationContext, PermissionService permissionService) {
public MethodSecurityConfig(ApplicationContext applicationContext, PermissionService permissionService, UserService userService) {
this.applicationContext = applicationContext;
this.permissionService = permissionService;
this.userService = userService;
}
@Override
protected MethodSecurityMetadataSource customMethodSecurityMetadataSource() {
return new HangarMetadataSources(new GlobalPermissionSource(), new ProjectPermissionSource());
return new HangarMetadataSources(new GlobalPermissionSource(), new ProjectPermissionSource(), new UserLockSource());
}
@Override
@ -63,6 +68,7 @@ public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {
decisionVoters.add(new AuthenticatedVoter());
decisionVoters.add(new ProjectPermissionVoter(permissionService));
decisionVoters.add(new GlobalPermissionVoter(permissionService));
decisionVoters.add(new UserLockVoter(userService));
return new UnanimousBased(decisionVoters);
}
}

View File

@ -3,6 +3,7 @@ package io.papermc.hangar.config;
import freemarker.template.TemplateException;
import io.papermc.hangar.controller.converters.ColorHexConverter;
import io.papermc.hangar.controller.converters.StringToEnumConverterFactory;
import io.papermc.hangar.security.UserLockExceptionResolver;
import io.papermc.hangar.service.PermissionService;
import io.papermc.hangar.service.project.ProjectService;
import io.papermc.hangar.util.Routes;
@ -119,9 +120,7 @@ public class MvcConfig implements WebMvcConfigurer {
return messageSource;
}
// yeah, idk
@Override
@SuppressWarnings({"unchecked", "rawtypes"})
public void addFormatters(FormatterRegistry registry) {
registry.addConverterFactory(new StringToEnumConverterFactory());
registry.addConverter(new ColorHexConverter());
@ -136,4 +135,9 @@ public class MvcConfig implements WebMvcConfigurer {
restTemplate.setMessageConverters(messageConverters);
return restTemplate;
}
@Bean
public UserLockExceptionResolver userLockExceptionResolver() {
return new UserLockExceptionResolver();
}
}

View File

@ -4,7 +4,9 @@ import io.papermc.hangar.filter.HangarAuthenticationFilter;
import io.papermc.hangar.security.HangarAuthenticationProvider;
import io.papermc.hangar.security.voters.GlobalPermissionVoter;
import io.papermc.hangar.security.voters.ProjectPermissionVoter;
import io.papermc.hangar.security.voters.UserLockVoter;
import io.papermc.hangar.service.PermissionService;
import io.papermc.hangar.service.UserService;
import io.papermc.hangar.util.Routes;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
@ -30,11 +32,13 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final HangarAuthenticationProvider authProvider;
private final PermissionService permissionService;
private final UserService userService;
@Autowired
public SecurityConfig(HangarAuthenticationProvider authProvider, PermissionService permissionService) {
public SecurityConfig(HangarAuthenticationProvider authProvider, PermissionService permissionService, UserService userService) {
this.authProvider = authProvider;
this.permissionService = permissionService;
this.userService = userService;
}
@Override
@ -68,7 +72,8 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
new RoleVoter(),
new AuthenticatedVoter(),
new ProjectPermissionVoter(permissionService),
new GlobalPermissionVoter(permissionService)
new GlobalPermissionVoter(permissionService),
new UserLockVoter(userService)
);
return new UnanimousBased(decisionVoters);
}

View File

@ -15,9 +15,10 @@ public class HangarErrorController extends HangarController implements ErrorCont
@RequestMapping("/error")
public ModelAndView handleError(HttpServletRequest request) {
Object status = request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE); // TODO redirect to sign on if not signed in
Object status = request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
String errorRequestUri = (String) request.getAttribute(RequestDispatcher.ERROR_REQUEST_URI);
// request.getAttributeNames().asIterator().forEachRemaining(s -> System.out.println(s + ": " + request.getAttribute(s))); // TODO for logging attributes to see what's there
ModelAndView mav = new ModelAndView("errors/error"); // TODO show custom message with error if applicable
if (status != null) {
int statusCode = Integer.parseInt(status.toString());

View File

@ -0,0 +1,21 @@
package io.papermc.hangar.security;
import io.papermc.hangar.util.HangarException;
import io.papermc.hangar.util.Routes;
import org.springframework.web.servlet.ModelAndView;
public class UserLockException extends HangarException {
private final Routes redirectRoute;
private final String[] routeArgs;
public UserLockException(String messageKey, Routes redirectRoute, String[] routeArgs) {
super(messageKey);
this.redirectRoute = redirectRoute;
this.routeArgs = routeArgs;
}
public ModelAndView getRedirectView() {
return redirectRoute.getRedirect(routeArgs);
}
}

View File

@ -0,0 +1,24 @@
package io.papermc.hangar.security;
import io.papermc.hangar.util.AlertUtil;
import io.papermc.hangar.util.AlertUtil.AlertType;
import org.jetbrains.annotations.NotNull;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.support.RequestContextUtils;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class UserLockExceptionResolver implements HandlerExceptionResolver {
@Override
public ModelAndView resolveException(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response, Object handler, @NotNull Exception ex) {
if (ex instanceof UserLockException) {
UserLockException exception = (UserLockException) ex;
AlertUtil.applyAlert(RequestContextUtils.getOutputFlashMap(request), AlertType.ERROR, exception.getMessageKey());
return exception.getRedirectView();
}
return null;
}
}

View File

@ -0,0 +1,20 @@
package io.papermc.hangar.security.annotations;
import io.papermc.hangar.util.Routes;
import org.springframework.core.annotation.AliasFor;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface UserLock {
@AliasFor("route")
Routes value() default Routes.SHOW_HOME;
@AliasFor("value")
Routes route() default Routes.SHOW_HOME;
String args() default "";
}

View File

@ -0,0 +1,49 @@
package io.papermc.hangar.security.attributes;
import io.papermc.hangar.util.Routes;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.util.Assert;
public class UserLockAttribute implements ConfigAttribute {
private final Routes route;
private final String args;
public UserLockAttribute(Routes route, String args) {
this.route = route;
Assert.notNull(route, "You must provide a route!");
this.args = args;
}
public Routes getRoute() {
return route;
}
public String getArgs() {
return args;
}
@Override
public String getAttribute() {
return null;
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof UserLockAttribute)) return false;
UserLockAttribute that = (UserLockAttribute) obj;
return that.route == this.route && that.args == this.args;
}
@Override
public int hashCode() {
return super.hashCode();
}
@Override
public String toString() {
return super.toString();
}
}

View File

@ -0,0 +1,27 @@
package io.papermc.hangar.security.metadatasources;
import io.papermc.hangar.security.annotations.UserLock;
import io.papermc.hangar.security.attributes.UserLockAttribute;
import io.papermc.hangar.util.Routes;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.annotation.AnnotationMetadataExtractor;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
public class UserLockSource implements AnnotationMetadataExtractor<UserLock> {
@Override
public Collection<? extends ConfigAttribute> extractAttributes(UserLock securityAnnotation) {
Set<UserLockAttribute> attributes = new HashSet<>();
Routes route = securityAnnotation.value();
if (securityAnnotation.route() != route && securityAnnotation.route() != Routes.SHOW_HOME) {
route = securityAnnotation.route();
}
attributes.add(new UserLockAttribute(
route,
securityAnnotation.args()
));
return attributes;
}
}

View File

@ -0,0 +1,75 @@
package io.papermc.hangar.security.voters;
import io.papermc.hangar.security.HangarAuthentication;
import io.papermc.hangar.security.UserLockException;
import io.papermc.hangar.security.attributes.UserLockAttribute;
import io.papermc.hangar.service.UserService;
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.context.expression.MethodBasedEvaluationContext;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.security.access.AccessDecisionVoter;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.core.Authentication;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
public class UserLockVoter implements AccessDecisionVoter {
private final UserService userService;
private final ExpressionParser parser = new SpelExpressionParser();
private final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
public UserLockVoter(UserService userService) {
this.userService = userService;
}
@Override
public boolean supports(ConfigAttribute attribute) {
return attribute instanceof UserLockAttribute;
}
@Override
public boolean supports(Class clazz) {
return true;
}
@Override
public int vote(Authentication authentication, Object object, Collection collection) {
if (!(object instanceof MethodInvocation)) return ACCESS_ABSTAIN;
if (!(authentication instanceof HangarAuthentication) || authentication.getPrincipal().equals("anonymousUser")) {
return ACCESS_ABSTAIN;
}
MethodInvocation methodInvocation = (MethodInvocation) object;
HangarAuthentication hangarAuth = (HangarAuthentication) authentication;
Collection<UserLockAttribute> attributes = ((Collection<ConfigAttribute>) collection).stream().filter(this::supports).map(UserLockAttribute.class::cast).collect(Collectors.toSet());
if (attributes.size() > 1) {
throw new IllegalStateException("Should have, at most, 1 user lock attribute");
}
if (attributes.isEmpty()) {
return ACCESS_GRANTED;
}
UserLockAttribute userLockAttribute = attributes.stream().findAny().get();
EvaluationContext context = new MethodBasedEvaluationContext(
methodInvocation.getMethod().getDeclaringClass(),
methodInvocation.getMethod(),
methodInvocation.getArguments(),
parameterNameDiscoverer
);
Expression exp = parser.parseExpression(userLockAttribute.getArgs());
List<String> argList = (List<String>) exp.getValue(context);
if (argList == null) {
argList = List.of();
}
if (userService.getUsersTable(hangarAuth.getUserId()).isLocked()) {
throw new UserLockException("error.user.locked", userLockAttribute.getRoute(), argList.toArray(new String[0]));
}
return ACCESS_GRANTED;
}
}

View File

@ -75,7 +75,7 @@ public class UserService extends HangarService {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && !(authentication instanceof AnonymousAuthenticationToken)) {
HangarAuthentication auth = (HangarAuthentication) authentication;
return () -> Optional.ofNullable(auth.getTable());
return () -> Optional.ofNullable(userDao.get().getById(auth.getUserId()));
}
return Optional::empty;
}
@ -188,6 +188,10 @@ public class UserService extends HangarService {
return new UserData(getHeaderData(), user, isOrga, projectCount, organizations, globalRoles, userPerm, orgaPerm);
}
public UsersTable getUsersTable(long userId) {
return userDao.get().getById(userId);
}
public List<UsersTable> getUsers(List<String> userNames) {
return userDao.get().getUsers(userNames);
}

View File

@ -20,16 +20,21 @@ public class AlertUtil {
public static final String MSG = "alertMsg";
public static final String ARGS = "alertArgs";
public static ModelAndView showAlert(ModelAndView mav, AlertType alertType, String alertMessage, Object...args) {
Map<String, Object> alerts = (Map<String, Object>) mav.getModelMap().getAttribute("alerts");
public static Map<String, Object> applyAlert(HashMap<String, Object> input, AlertType alertType, String alertMsg, Object...args) {
Map<String, Object> alerts = (Map<String, Object>) input.get("alerts");
if (alerts == null) {
alerts = new HashMap<>();
}
Map<String, Object> thisAlert = new HashMap<>();
thisAlert.put("message", alertMessage);
thisAlert.put("message", alertMsg);
thisAlert.put("args", args);
alerts.put(alertType.name().toLowerCase(), thisAlert);
mav.addObject("alerts", alerts);
input.put("alerts", alerts);
return input;
}
public static ModelAndView showAlert(ModelAndView mav, AlertType alertType, String alertMessage, Object...args) {
applyAlert(mav.getModelMap(), alertType, alertMessage, args);
return mav;
}
@ -49,6 +54,8 @@ public class AlertUtil {
args = (String[]) modelMap.getAttribute(ARGS);
}
return showAlert(mav, type, msg, args);
} else if (modelMap.containsAttribute("alerts")) {
mav.addObject("alerts", modelMap.getAttribute("alerts"));
}
return mav;
}

View File

@ -75,10 +75,10 @@
<@panel title="Other Administration" size=3>
<div class="list-group">
<div class="list-group-item">
<a href="${config.authUrl}/admin/accounts/user/${user.user.id}/change/">HangarAuth Profile</a>
<a href="${config.authUrl}/admin/accounts/user/${u.user.id}/change/">HangarAuth Profile</a>
</div>
<div class="list-group-item">
<a href="https://papermc.io/forums${user.user.name}">Forum Profile</a>
<a href="https://papermc.io/forums${u.user.name}">Forum Profile</a>
</div>
</div>
</@panel>