add restentpoint for login/logout, restructuring of modules and components, add flow usage

This commit is contained in:
Maren Süwer
2018-07-04 16:43:46 +02:00
parent 85902010ce
commit 3cc87ede73
22 changed files with 646 additions and 99 deletions

View File

@@ -0,0 +1,269 @@
package sonia.scm.api.v2.resources;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.base.Preconditions;
import com.google.common.base.Strings;
import com.google.inject.Inject;
import com.webcohesion.enunciate.metadata.rs.ResponseCode;
import com.webcohesion.enunciate.metadata.rs.StatusCodes;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.DisabledAccountException;
import org.apache.shiro.authc.ExcessiveAttemptsException;
import org.apache.shiro.subject.Subject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sonia.scm.api.rest.RestActionResult;
import sonia.scm.security.*;
import sonia.scm.util.HttpUtil;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.*;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlRootElement;
import java.util.List;
/**
* Created by masuewer on 04.07.18.
*/
@Path(AuthenticationResource.PATH)
public class AuthenticationResource {
private static final Logger LOG = LoggerFactory.getLogger(AuthenticationResource.class);
public static final String PATH = "v2/auth";
private final AccessTokenBuilderFactory tokenBuilderFactory;
private final AccessTokenCookieIssuer cookieIssuer;
@Inject
public AuthenticationResource(AccessTokenBuilderFactory tokenBuilderFactory, AccessTokenCookieIssuer cookieIssuer)
{
this.tokenBuilderFactory = tokenBuilderFactory;
this.cookieIssuer = cookieIssuer;
}
@POST
@Path("access_token")
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
@StatusCodes({
@ResponseCode(code = 200, condition = "success"),
@ResponseCode(code = 400, condition = "bad request, required parameter is missing"),
@ResponseCode(code = 401, condition = "unauthorized, the specified username or password is wrong"),
@ResponseCode(code = 500, condition = "internal server error")
})
public Response authenticateViaForm(
@Context HttpServletRequest request,
@Context HttpServletResponse response,
@BeanParam AuthenticationRequest authentication
) {
return authenticate(request, response, authentication);
}
@POST
@Path("access_token")
@Consumes(MediaType.APPLICATION_JSON)
@StatusCodes({
@ResponseCode(code = 200, condition = "success"),
@ResponseCode(code = 400, condition = "bad request, required parameter is missing"),
@ResponseCode(code = 401, condition = "unauthorized, the specified username or password is wrong"),
@ResponseCode(code = 500, condition = "internal server error")
})
public Response authenticateViaJSONBody(
@Context HttpServletRequest request,
@Context HttpServletResponse response,
AuthenticationRequest authentication
) {
return authenticate(request, response, authentication);
}
private Response authenticate(
HttpServletRequest request,
HttpServletResponse response,
AuthenticationRequest authentication
) {
authentication.validate();
Response res;
Subject subject = SecurityUtils.getSubject();
try
{
subject.login(Tokens.createAuthenticationToken(request, authentication.getUsername(), authentication.getPassword()));
AccessTokenBuilder tokenBuilder = tokenBuilderFactory.create();
if ( authentication.getScope() != null ) {
tokenBuilder.scope(Scope.valueOf(authentication.getScope()));
}
AccessToken token = tokenBuilder.build();
if (authentication.isCookie()) {
cookieIssuer.authenticate(request, response, token);
res = Response.noContent().build();
} else {
res = Response.ok( token.compact() ).build();
}
}
catch (DisabledAccountException ex)
{
if (LOG.isTraceEnabled())
{
LOG.trace(
"authentication failed, account user ".concat(authentication.getUsername()).concat(
" is locked"), ex);
}
else
{
LOG.warn("authentication failed, account {} is locked", authentication.getUsername());
}
res = handleFailedAuthentication(request, ex, Response.Status.FORBIDDEN,
WUIAuthenticationFailure.LOCKED);
}
catch (ExcessiveAttemptsException ex)
{
if (LOG.isTraceEnabled())
{
LOG.trace(
"authentication failed, account user ".concat(authentication.getUsername()).concat(
" is temporary locked"), ex);
}
else
{
LOG.warn("authentication failed, account {} is temporary locked", authentication.getUsername());
}
res = handleFailedAuthentication(request, ex, Response.Status.FORBIDDEN,
WUIAuthenticationFailure.TEMPORARY_LOCKED);
}
catch (AuthenticationException ex)
{
if (LOG.isTraceEnabled())
{
LOG.trace("authentication failed for user ".concat(authentication.getUsername()), ex);
}
else
{
LOG.warn("authentication failed for user {}", authentication.getUsername());
}
res = handleFailedAuthentication(request, ex, Response.Status.UNAUTHORIZED,
WUIAuthenticationFailure.WRONG_CREDENTIALS);
}
return res;
}
@DELETE
@Path("access_token")
@StatusCodes({
@ResponseCode(code = 204, condition = "success"),
@ResponseCode(code = 500, condition = "internal server error")
})
public Response logout(@Context HttpServletRequest request, @Context HttpServletResponse response)
{
Subject subject = SecurityUtils.getSubject();
subject.logout();
// remove authentication cookie
cookieIssuer.invalidate(request, response);
// TODO anonymous access ??
return Response.noContent().build();
}
public static class AuthenticationRequest {
@FormParam("grant_type")
@JsonProperty("grant_type")
private String grantType;
@FormParam("username")
private String username;
@FormParam("password")
private String password;
@FormParam("cookie")
private boolean cookie;
@FormParam("scope")
private List<String> scope;
public String getGrantType() {
return grantType;
}
public String getUsername() {
return username;
}
public String getPassword() {
return password;
}
public boolean isCookie() {
return cookie;
}
public List<String> getScope() {
return scope;
}
public void validate() {
Preconditions.checkArgument(!Strings.isNullOrEmpty(grantType), "grant_type parameter is required");
Preconditions.checkArgument(!Strings.isNullOrEmpty(username), "username parameter is required");
Preconditions.checkArgument(!Strings.isNullOrEmpty(password), "password parameter is required");
}
}
private Response handleFailedAuthentication(HttpServletRequest request,
AuthenticationException ex, Response.Status status,
WUIAuthenticationFailure failure) {
Response response;
if (HttpUtil.isWUIRequest(request)) {
response = Response.ok(new WUIAuthenticationFailedResult(failure,
ex.getMessage())).build();
} else {
response = Response.status(status).build();
}
return response;
}
private enum WUIAuthenticationFailure { LOCKED, TEMPORARY_LOCKED, WRONG_CREDENTIALS }
@XmlRootElement(name = "result")
@XmlAccessorType(XmlAccessType.FIELD)
private static final class WUIAuthenticationFailedResult extends RestActionResult {
private final WUIAuthenticationFailure failure;
private final String message;
public WUIAuthenticationFailedResult(WUIAuthenticationFailure failure, String message) {
super(false);
this.failure = failure;
this.message = message;
}
public WUIAuthenticationFailure getFailure() {
return failure;
}
public String getMessage() {
return message;
}
}
}

View File

@@ -44,6 +44,7 @@ import org.apache.shiro.subject.Subject;
import sonia.scm.Priority;
import sonia.scm.SCMContext;
import sonia.scm.config.ScmConfiguration;
import sonia.scm.security.SecurityRequests;
import sonia.scm.web.filter.HttpFilter;
import sonia.scm.web.filter.SecurityHttpServletRequestWrapper;
@@ -72,6 +73,8 @@ public class SecurityFilter extends HttpFilter
/** Field description */
public static final String URL_AUTHENTICATION = "/api/rest/auth";
public static final String URLV2_AUTHENTICATION = "/api/rest/v2/auth";
//~--- constructors ---------------------------------------------------------
/**
@@ -104,10 +107,7 @@ public class SecurityFilter extends HttpFilter
HttpServletResponse response, FilterChain chain)
throws IOException, ServletException
{
String uri =
request.getRequestURI().substring(request.getContextPath().length());
if (!uri.startsWith(URL_AUTHENTICATION))
if (!SecurityRequests.isAuthenticationRequest(request))
{
Subject subject = SecurityUtils.getSubject();
if (hasPermission(subject))

View File

@@ -0,0 +1,24 @@
package sonia.scm.security;
import javax.servlet.http.HttpServletRequest;
import java.util.regex.Pattern;
/**
* Created by masuewer on 04.07.18.
*/
public final class SecurityRequests {
private static final Pattern URI_LOGIN_PATTERN = Pattern.compile("/api/rest(?:/v2)?/auth/access_token");
private SecurityRequests() {}
public static boolean isAuthenticationRequest(HttpServletRequest request) {
String uri = request.getRequestURI().substring(request.getContextPath().length());
return isAuthenticationRequest(uri);
}
public static boolean isAuthenticationRequest(String uri) {
return URI_LOGIN_PATTERN.matcher(uri).matches();
}
}

View File

@@ -36,24 +36,22 @@ package sonia.scm.web.security;
//~--- non-JDK imports --------------------------------------------------------
import com.google.inject.Inject;
import sonia.scm.Priority;
import sonia.scm.config.ScmConfiguration;
import sonia.scm.filter.Filters;
import sonia.scm.filter.WebElement;
import sonia.scm.web.filter.AuthenticationFilter;
import sonia.scm.security.SecurityRequests;
import sonia.scm.web.WebTokenGenerator;
//~--- JDK imports ------------------------------------------------------------
import java.io.IOException;
import java.util.Set;
import sonia.scm.web.filter.AuthenticationFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Set;
//~--- JDK imports ------------------------------------------------------------
/**
* Filter to handle authentication for the rest api of SCM-Manager.
@@ -66,9 +64,6 @@ import javax.servlet.http.HttpServletResponse;
public class ApiAuthenticationFilter extends AuthenticationFilter
{
/** login uri */
public static final String URI_LOGIN = "/api/rest/auth/access_token";
//~--- constructors ---------------------------------------------------------
/**
@@ -104,7 +99,7 @@ public class ApiAuthenticationFilter extends AuthenticationFilter
throws IOException, ServletException
{
// skip filter on login resource
if (request.getRequestURI().contains(URI_LOGIN))
if (SecurityRequests.isAuthenticationRequest(request))
{
chain.doFilter(request, response);
}