Skip to content

Commit

Permalink
Embedded the security checks inside the HTTP handler base.
Browse files Browse the repository at this point in the history
  • Loading branch information
nmihajlovski committed Mar 5, 2016
1 parent affacdf commit 6d4735a
Show file tree
Hide file tree
Showing 15 changed files with 201 additions and 70 deletions.
Expand Up @@ -298,7 +298,16 @@ public HttpClient dontClose() {
}

public String fetch() {
return new String(execute(null).get());
return new String(execute());
}

public String fetchRaw() {
raw(true);
try {
return new String(execute());
} finally {
raw(false);
}
}

public <T> T parse() {
Expand Down
Expand Up @@ -109,7 +109,7 @@ public static void write200(Channel ctx, boolean isKeepAlive, MediaType contentT
}

public static void error(Req req, Throwable error, ErrorHandler errorHandler) {
Log.error("HTTP handler error!", error);
Log.debug("HTTP handler error!", "error", error);

try {
Resp resp = req.response().code(500);
Expand Down
Expand Up @@ -23,7 +23,6 @@
import org.rapidoid.annotation.Authors;
import org.rapidoid.annotation.Since;
import org.rapidoid.annotation.TransactionMode;
import org.rapidoid.ctx.Current;
import org.rapidoid.ctx.With;
import org.rapidoid.http.*;
import org.rapidoid.jpa.JPA;
Expand Down Expand Up @@ -58,9 +57,12 @@ public boolean needsParams() {
public HttpStatus handle(Channel ctx, boolean isKeepAlive, Req req, Object extra) {
U.notNull(req, "HTTP request");

String username = req.cookiepack(HttpUtils._USER, null);
Set<String> roles = userRoles(username);

TransactionMode txMode;
try {
txMode = before(req);
txMode = before(req, username, roles);

} catch (Throwable e) {
HttpIO.errorAndDone(req, e, http.custom().errorHandler());
Expand All @@ -69,7 +71,7 @@ public HttpStatus handle(Channel ctx, boolean isKeepAlive, Req req, Object extra

try {
ctx.async();
execHandlerJob(ctx, isKeepAlive, req, extra, txMode);
execHandlerJob(ctx, isKeepAlive, req, extra, txMode, username, roles);

} catch (Throwable e) {
// if there was an error in the job scheduling:
Expand All @@ -80,9 +82,19 @@ public HttpStatus handle(Channel ctx, boolean isKeepAlive, Req req, Object extra
return HttpStatus.ASYNC;
}

private TransactionMode before(final Req req) {
String username = Current.username();
Set<String> roles = Current.roles();
private Set<String> userRoles(String username) {
if (username != null) {
try {
return http.custom().rolesProvider().getRolesForUser(username);
} catch (Exception e) {
throw U.rte(e);
}
} else {
return Collections.emptySet();
}
}

private TransactionMode before(final Req req, String username, Set<String> roles) {

if (U.notEmpty(options.roles) && !Secure.hasAnyRole(username, roles, options.roles)) {
throw new SecurityException("The user doesn't have the required roles!");
Expand Down Expand Up @@ -120,20 +132,7 @@ protected Object postprocessResult(Req req, Object result) throws Exception {
}

private void execHandlerJob(final Channel channel, final boolean isKeepAlive, final Req req,
final Object extra, final TransactionMode txMode) {

String username = req.cookiepack(HttpUtils._USER, null);
Set<String> roles;

if (username != null) {
try {
roles = http.custom().rolesProvider().getRolesForUser(username);
} catch (Exception e) {
throw U.rte(e);
}
} else {
roles = Collections.emptySet();
}
final Object extra, final TransactionMode txMode, String username, Set<String> roles) {

Runnable handleRequest = handlerWithWrappers(channel, isKeepAlive, req, extra);
Runnable handleRequestMaybeInTx = txWrap(txMode, handleRequest);
Expand Down
Expand Up @@ -26,6 +26,7 @@
import org.rapidoid.commons.Err;
import org.rapidoid.commons.Rnd;
import org.rapidoid.ctx.Current;
import org.rapidoid.security.Roles;
import org.rapidoid.setup.On;
import org.rapidoid.u.U;

Expand All @@ -39,6 +40,8 @@ public class HttpLoginTest extends HttpTestCommons {
public void testLogin() {
On.get("/user").json(() -> U.list(Current.username(), Current.roles()));

On.get("/profile").roles(Roles.LOGGED_IN).json(Current::username);

On.post("/mylogin").json((Resp resp, String user, String pass) -> {
boolean success = resp.login(user, pass);
return U.list(success, Current.username(), Current.roles());
Expand All @@ -49,51 +52,86 @@ public void testLogin() {
return U.list(Current.username(), Current.roles());
});

multiThreaded(100, 1000, () -> {
switch (Rnd.rnd(4)) {
case 0:
loginFlow("foo", "bar", U.list());
break;
case 1:
loginFlow("abc", "abc", U.list("guest"));
break;
case 2:
loginFlow("chuck", "chuck", U.list("moderator", "restarter"));
break;
case 3:
loginFlow("niko", "easy", U.list("owner", "administrator", "moderator"));
break;
default:
throw Err.notExpected();
}
});
multiThreaded(200, 1000, this::randomUserLogin);
}

private void loginFlow(String user, String pass, List<String> roles) {
HttpClient client = HTTP.keepCookies(true).dontClose();
private void randomUserLogin() {
switch (Rnd.rnd(4)) {
case 0:
loginFlow("foo", "bar", U.list());
break;
case 1:
loginFlow("abc", "abc", U.list("guest"));
break;
case 2:
loginFlow("chuck", "chuck", U.list("moderator", "restarter"));
break;
case 3:
loginFlow("niko", "easy", U.list("owner", "administrator", "moderator"));
break;
default:
throw Err.notExpected();
}
}

private void loginFlow(String user, String pass, List<String> expectedRoles) {
HttpClient client = HTTP.keepCookies(true).reuseConnections(true).dontClose();

List<Object> notLoggedIn = U.list(false, null, U.list());
List<Object> loggedIn = U.list(true, user, roles);
List<Object> loggedIn = U.list(true, user, expectedRoles);

eq(client.get(localhost("/user")).parse(), U.list(null, U.list()));

verifyAccessDenied(client);

eq(client.post(localhost("/mylogin?user=a1&pass=b")).parse(), notLoggedIn);
eq(client.post(localhost("/mylogin?user=a2&pass=b")).parse(), notLoggedIn);

verifyAccessDenied(client);

eq(client.post(localhost(U.frmt("/mylogin?user=%s&pass=%s", user, pass))).parse(), loggedIn);

eq(client.get(localhost("/user")).parse(), U.list(user, roles));
verifyAnonymousAccessDenied();
verifyAccessGranted(user, client);

eq(client.get(localhost("/user")).parse(), U.list(user, expectedRoles));

eq(client.post(localhost("/mylogin?user=a3&pass=b")).parse(), U.list(false, user, roles));
verifyAnonymousAccessDenied();
verifyAccessGranted(user, client);

eq(client.get(localhost("/user")).parse(), U.list(user, roles));
eq(client.post(localhost("/mylogin?user=a3&pass=b")).parse(), U.list(false, user, expectedRoles));

verifyAnonymousAccessDenied();
verifyAccessGranted(user, client);

eq(client.get(localhost("/user")).parse(), U.list(user, expectedRoles));
eq(client.post(localhost("/mylogout")).parse(), U.list(null, U.list()));

verifyAnonymousAccessDenied();
verifyLoggedOut(client);

eq(client.get(localhost("/user")).parse(), U.list(null, U.list()));
eq(client.get(localhost("/user")).parse(), U.list(null, U.list()));

verifyLoggedOut(client);

client.close();
}

private void verifyAnonymousAccessDenied() {
onlyGet("/profile");
}

private void verifyAccessGranted(String user, HttpClient client) {
verify("granted-" + user, fetch(client, "get", "/profile"));
}

private void verifyAccessDenied(HttpClient client) {
verify("denied", fetch(client, "get", "/profile"));
}

private void verifyLoggedOut(HttpClient client) {
verify("logout", fetch(client, "get", "/profile"));
}

}
Expand Up @@ -243,20 +243,37 @@ private void testReq(int port, String verb, String uri, Map<String, ?> data) {
verifyCase(port + " " + verb + " " + uri, resp, reqName);
}

private String fetch(int port, String verb, String uri, Map<String, ?> data) {
HttpClient client = HTTP.verb(HttpVerb.from(verb)).url(localhost(port, uri)).raw(true);
protected String fetch(int port, String verb, String uri, Map<String, ?> data) {
HttpClient client = HTTP.verb(HttpVerb.from(verb)).url(localhost(port, uri)).data(data);
String result = exec(client);
client.close();
return result;
}

if (data != null) {
client = client.data(data);
}
protected String fetch(HttpClient client, int port, String verb, String uri, Map<String, ?> data) {
client.verb(HttpVerb.from(verb)).url(localhost(port, uri)).data(data);
return exec(client);
}

private String exec(HttpClient client) {
client.raw(true);

byte[] res = client.execute();
String resp = new String(res);
resp = resp.replaceFirst("Date: .*? GMT", "Date: XXXXX GMT");

client.raw(false);
return resp;
}

protected String fetch(HttpClient client, String verb, String uri, Map<String, ?> data) {
return fetch(client, DEFAULT_PORT, verb, uri, data);
}

protected String fetch(HttpClient client, String verb, String uri) {
return fetch(client, DEFAULT_PORT, verb, uri, null);
}

protected String fetch(String verb, String uri) {
return fetch(DEFAULT_PORT, verb, uri, null);
}
Expand Down
@@ -0,0 +1,21 @@
secret: hard-to-guess

users:
niko:
email: niko@rapidoid.org.abcde
password: easy
roles:
- owner
- administrator
- moderator

chuck:
password: chuck
roles: ["moderator", "restarter"]

abc:
password: abc
roles: guest

foo:
password: bar

This file was deleted.

@@ -0,0 +1,8 @@
HTTP/1.1 403 Forbidden
Connection: keep-alive
Server: Rapidoid
Date: XXXXX GMT
Content-Type: application/json; charset=utf-8
Content-Length: 64

{"error":"The user doesn't have the required roles!","code":403}
@@ -0,0 +1,8 @@
HTTP/1.1 403 Forbidden
Connection: keep-alive
Server: Rapidoid
Date: XXXXX GMT
Content-Type: application/json; charset=utf-8
Content-Length: 64

{"error":"The user doesn't have the required roles!","code":403}
@@ -0,0 +1,9 @@
HTTP/1.1 200 OK
Connection: keep-alive
Server: Rapidoid
Date: XXXXX GMT
Content-Type: application/json; charset=utf-8
Set-Cookie: COOKIEPACK=jbYa2G13kN9Mr$sYj8DtKw==; path=/
Content-Length: 5

"abc"
@@ -0,0 +1,9 @@
HTTP/1.1 200 OK
Connection: keep-alive
Server: Rapidoid
Date: XXXXX GMT
Content-Type: application/json; charset=utf-8
Set-Cookie: COOKIEPACK=vEQMGrsV$JsjG5PW6ddbgz5I8nCQbj3vl17cbCifBx8=; path=/
Content-Length: 7

"chuck"
@@ -0,0 +1,9 @@
HTTP/1.1 200 OK
Connection: keep-alive
Server: Rapidoid
Date: XXXXX GMT
Content-Type: application/json; charset=utf-8
Set-Cookie: COOKIEPACK=bhsWehWUPJf1$fqy3ykdFg==; path=/
Content-Length: 5

"foo"
@@ -0,0 +1,9 @@
HTTP/1.1 200 OK
Connection: keep-alive
Server: Rapidoid
Date: XXXXX GMT
Content-Type: application/json; charset=utf-8
Set-Cookie: COOKIEPACK=mtwtUnzH1IsCSBFMmqyGnpZJW1qgchL$rAovPs9Sn1Y=; path=/
Content-Length: 6

"niko"
@@ -0,0 +1,9 @@
HTTP/1.1 403 Forbidden
Connection: keep-alive
Server: Rapidoid
Date: XXXXX GMT
Content-Type: application/json; charset=utf-8
Set-Cookie: COOKIEPACK=; path=/
Content-Length: 64

{"error":"The user doesn't have the required roles!","code":403}

0 comments on commit 6d4735a

Please sign in to comment.