Skip to content

Commit

Permalink
Introducing route-based scope control in the security token mechanism.
Browse files Browse the repository at this point in the history
  • Loading branch information
nmihajlovski committed Nov 8, 2016
1 parent 92b3289 commit 150a063
Show file tree
Hide file tree
Showing 15 changed files with 146 additions and 41 deletions.
2 changes: 1 addition & 1 deletion rapidoid-commons/src/main/java/org/rapidoid/ctx/Ctxs.java
Expand Up @@ -71,7 +71,7 @@ public static Ctx open(WithContext context) {

ctx.setExchange(context.exchange());
ctx.setPersister(context.persister());
ctx.setUser(new UserInfo(context.username(), context.roles()));
ctx.setUser(new UserInfo(context.username(), context.roles(), context.scope()));
Coll.assign(ctx.extras(), U.safe(context.extras()));

return ctx;
Expand Down
7 changes: 5 additions & 2 deletions rapidoid-commons/src/main/java/org/rapidoid/ctx/UserInfo.java
Expand Up @@ -36,14 +36,16 @@ public class UserInfo extends RapidoidThing implements Serializable {

private static final long serialVersionUID = 7062732348562440194L;

public static final UserInfo ANONYMOUS = new UserInfo(null, U.<String>set());
public static final UserInfo ANONYMOUS = new UserInfo(null, U.<String>set(), null);

public final String username;

public final Set<String> roles;

public final Map<String, Boolean> is;

public final Set<String> scope;

public volatile String email;

public volatile String name;
Expand All @@ -52,10 +54,11 @@ public class UserInfo extends RapidoidThing implements Serializable {

public volatile String oauthProvider;

public UserInfo(String username, Set<String> roles) {
public UserInfo(String username, Set<String> roles, Set<String> scope) {
this.username = username;
this.roles = Collections.unmodifiableSet(U.safe(roles));
this.is = rolesMap(roles);
this.scope = Collections.unmodifiableSet(U.safe(scope));
}

private static Map<String, Boolean> rolesMap(Set<String> roles) {
Expand Down
4 changes: 4 additions & 0 deletions rapidoid-commons/src/main/java/org/rapidoid/ctx/With.java
Expand Up @@ -43,6 +43,10 @@ public static WithContext roles(Set<String> roles) {
return new WithContext().roles(roles);
}

public static WithContext scope(Set<String> scope) {
return new WithContext().scope(scope);
}

public static WithContext persister(Object persister) {
return new WithContext().persister(persister);
}
Expand Down
11 changes: 11 additions & 0 deletions rapidoid-commons/src/main/java/org/rapidoid/ctx/WithContext.java
Expand Up @@ -38,6 +38,8 @@ public class WithContext extends RapidoidThing {

private volatile Set<String> roles;

private volatile Set<String> scope;

private volatile Object persister;

private volatile Object exchange;
Expand Down Expand Up @@ -71,6 +73,15 @@ public Set<String> roles() {
return this.roles;
}

public WithContext scope(Set<String> scope) {
this.scope = scope;
return this;
}

public Set<String> scope() {
return scope;
}

public WithContext persister(Object persister) {
this.persister = persister;
return this;
Expand Down
@@ -0,0 +1,45 @@
package org.rapidoid.util;

/*
* #%L
* rapidoid-commons
* %%
* Copyright (C) 2014 - 2016 Nikolche Mihajlovski and contributors
* %%
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* #L%
*/

import org.rapidoid.RapidoidThing;
import org.rapidoid.annotation.Authors;
import org.rapidoid.annotation.Since;

import java.util.Set;

@Authors("Nikolche Mihajlovski")
@Since("5.3.0")
public class TokenAuthData extends RapidoidThing {

public volatile String user;
public volatile Set<String> scope;
public volatile Long expires;

@Override
public String toString() {
return "TokenAuthData{" +
"user='" + user + '\'' +
", scope='" + scope + '\'' +
", expires=" + expires +
'}';
}
}
40 changes: 40 additions & 0 deletions rapidoid-commons/src/main/java/org/rapidoid/util/Tokens.java
Expand Up @@ -12,6 +12,7 @@
import java.nio.BufferOverflowException;
import java.util.Arrays;
import java.util.Map;
import java.util.Set;

/*
* #%L
Expand All @@ -37,6 +38,10 @@
@Since("5.3.0")
public class Tokens extends RapidoidThing {

public static final String _USER = "_user";
public static final String _SCOPE = "_scope";
public static final String _EXPIRES = "_expires";

public static String serialize(Map<String, Serializable> token) {
if (U.notEmpty(token)) {
byte[] tokenBytes = serializeToken(token);
Expand Down Expand Up @@ -73,4 +78,39 @@ public static Map<String, Serializable> deserialize(String token) {
}
}

public static TokenAuthData getAuth(Map<String, Serializable> token) {
TokenAuthData data = new TokenAuthData();

data.user = (String) token.get(_USER);
data.scope = scope((String) token.get(_SCOPE));
data.expires = (Long) token.get(_EXPIRES);

if (data.expires == null || data.expires > U.time()) {
return data;
} else {
return null; // expired
}
}

private static Set<String> scope(String scope) {
if (U.isEmpty(scope)) return null;

Set<String> scopes = U.set();

for (String sc : scope.split("\\,")) {
String[] parts = sc.trim().split("\\:");
String uri = parts[parts.length - 1];

if (parts.length == 1) {
scopes.add(uri);
} else {
for (int i = 0; i < parts.length - 1; i++) {
scopes.add(parts[i] + " " + uri);
}
}
}

return scopes;
}

}
2 changes: 2 additions & 0 deletions rapidoid-commons/src/main/resources/rapidoid-classes.txt
Expand Up @@ -671,6 +671,8 @@ org.rapidoid.util.SimpleList
org.rapidoid.util.SimpleMap
org.rapidoid.util.SimplePersisterProvider
org.rapidoid.util.StreamUtils
org.rapidoid.util.TokenAuthData
org.rapidoid.util.Tokens
org.rapidoid.util.Usage
org.rapidoid.u.U
org.rapidoid.validation.InvalidData
Expand Down
Expand Up @@ -42,7 +42,7 @@ public void testAppCtx() {
public void run() {
Ctxs.open("test");

UserInfo user = new UserInfo(rndStr(10), U.set("role1"));
UserInfo user = new UserInfo(rndStr(10), U.set("role1"), null);

Ctxs.required().setUser(user);

Expand Down
Expand Up @@ -47,7 +47,7 @@ public void testJobsExecution() {
public void run() {
Ctxs.open("test-job");

final UserInfo user = new UserInfo(rndStr(50), U.set("role1"));
final UserInfo user = new UserInfo(rndStr(50), U.set("role1"), null);

Ctxs.required().setUser(user);
ensureProperContext(user);
Expand Down
Expand Up @@ -56,6 +56,10 @@ public static Set<String> roles() {
return user().roles;
}

public static Set<String> scope() {
return user().scope;
}

public static Req request() {
return Ctxs.required().exchange();
}
Expand Down
19 changes: 11 additions & 8 deletions rapidoid-http-fast/src/main/java/org/rapidoid/http/HttpUtils.java
Expand Up @@ -16,9 +16,7 @@
import org.rapidoid.io.Res;
import org.rapidoid.lambda.Mapper;
import org.rapidoid.u.U;
import org.rapidoid.util.ErrCodeAndMsg;
import org.rapidoid.util.Msc;
import org.rapidoid.util.Tokens;
import org.rapidoid.util.*;

import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
Expand Down Expand Up @@ -61,9 +59,6 @@ public class HttpUtils extends RapidoidThing implements HttpMetadata {

public static volatile Pattern REGEX_VALID_HTTP_RESOURCE = Pattern.compile("(?:/[A-Za-z0-9_\\-\\.]+)*/?");

public static final String _USER = "_user";
public static final String _EXPIRES = "_expires";

private static final Mapper<String[], String> PATH_PARAM_EXTRACTOR = new Mapper<String[], String>() {
@Override
public String map(String[] src) throws Exception {
Expand Down Expand Up @@ -354,9 +349,17 @@ public static void clearUserData(Req req) {

if (req.hasToken()) {
Map<String, Serializable> token = req.token();
token.remove(_USER);
token.remove(_EXPIRES);
token.remove(Tokens._USER);
token.remove(Tokens._SCOPE);
}
}

public static TokenAuthData getAuth(Req req) {
TokenAuthData auth = req.hasToken() ? Tokens.getAuth(req.token()) : null;

// check if the route is outside of scope
if (auth != null && U.notEmpty(auth.scope) && !auth.scope.contains(req.verb() + " " + req.path())) auth = null;

return auth;
}
}
Expand Up @@ -48,6 +48,10 @@ public static Object err(Req req, String msg) {
return details(msg, false);
}

public static Object deny(Req req) {
return err(req, "Access denied!");
}

public static Object details(String msg, boolean success) {
return U.map("msg", msg, "success", success);
}
Expand Down
Expand Up @@ -15,6 +15,7 @@
import org.rapidoid.security.Secure;
import org.rapidoid.u.U;
import org.rapidoid.util.Msc;
import org.rapidoid.util.TokenAuthData;

import java.util.Collections;
import java.util.Set;
Expand Down Expand Up @@ -97,25 +98,6 @@ private HttpStatus handleDecorating(Channel ctx, boolean isKeepAlive, Req req, O
return HttpStatus.ASYNC;
}

private String getUser(Req req) {
if (req.hasToken()) {
String username = req.token(HttpUtils._USER, null);

if (username != null) {
long expiresOn = req.token(HttpUtils._EXPIRES);

if (expiresOn < U.time()) {
username = null; // expired
}
}

return username;

} else {
return null;
}
}

private Set<String> userRoles(Req req, String username) {
if (username != null) {
try {
Expand Down Expand Up @@ -152,17 +134,22 @@ private void execHandlerJob(final Channel channel, final boolean isKeepAlive, fi

volatile String username = null;
volatile Set<String> roles = null;
volatile Set<String> scope = null;

@Override
public void run() {
try {
username = getUser(req);

TokenAuthData auth = HttpUtils.getAuth(req);

if (auth != null) username = auth.user;

if (U.isEmpty(username)) {
HttpUtils.clearUserData(req);
}

roles = userRoles(req, username);
scope = auth != null ? auth.scope : null;

TransactionMode txMode = before(req, username, roles);
U.notNull(txMode, "txMode");
Expand All @@ -172,18 +159,18 @@ public void run() {
Runnable handleRequest = handlerWithWrappers(channel, isKeepAlive, contentType, req, extra, wrappers);
Runnable handleRequestMaybeInTx = txWrap(req, txMode, handleRequest);

With.tag(CTX_TAG_HANDLER).exchange(req).username(username).roles(roles).run(handleRequestMaybeInTx);
With.tag(CTX_TAG_HANDLER).exchange(req).username(username).roles(roles).scope(scope).run(handleRequestMaybeInTx);

} catch (Throwable e) {
// if there was an error in the job scheduling:
execErrorHandler(req, username, roles, e);
execErrorHandler(req, username, roles, scope, e);
}
}
});
}

private HttpStatus execErrorHandler(final Req req, String username, Set<String> roles, final Throwable error) {
With.tag(CTX_TAG_ERROR).exchange(req).username(username).roles(roles).run(new Runnable() {
private HttpStatus execErrorHandler(final Req req, String username, Set<String> roles, Set<String> scope, final Throwable error) {
With.tag(CTX_TAG_ERROR).exchange(req).username(username).roles(roles).scope(scope).run(new Runnable() {
@Override
public void run() {
handleError(req, error);
Expand Down
Expand Up @@ -18,6 +18,7 @@
import org.rapidoid.http.customize.RolesProvider;
import org.rapidoid.u.U;
import org.rapidoid.util.Msc;
import org.rapidoid.util.Tokens;
import org.rapidoid.web.Screen;
import org.rapidoid.web.ScreenBean;

Expand Down Expand Up @@ -366,10 +367,11 @@ public boolean login(String username, String password) {
long ttl = Conf.TOKEN.entry("ttl").or(0);
long expiresOn = ttl > 0 ? U.time() + ttl : Long.MAX_VALUE;

Ctxs.required().setUser(new UserInfo(username, roles));
UserInfo user = new UserInfo(username, roles, null);
Ctxs.required().setUser(user);

request().token().put(HttpUtils._USER, username);
request().token().put(HttpUtils._EXPIRES, expiresOn);
request().token().put(Tokens._USER, username);
request().token().put(Tokens._EXPIRES, expiresOn);
}

} catch (Throwable e) {
Expand Down
Expand Up @@ -125,7 +125,7 @@ public Object execute(Req req) throws Exception {
String username = email;
Set<String> roles = customization.rolesProvider().getRolesForUser(req, username);

UserInfo user = new UserInfo(username, roles);
UserInfo user = new UserInfo(username, roles, null);
user.name = name;
user.email = email;
user.oauthProvider = provider.getName();
Expand Down

0 comments on commit 150a063

Please sign in to comment.