allRoles = Sets.newTreeSet(roles);
allRoles.addAll(defaultRoles);
- if (trusted) {
- allRoles.addAll(trustedRoles);
- }
return Permissions.applyProfilesAndPublicRoles(allRoles);
}
@@ -399,6 +354,11 @@ private UserInfo loginViaUsernameAndPassword(WebContext ctx) {
protected UserInfo findUserInSession(WebContext ctx) {
Value userId = ctx.getSessionValue(scope.getScopeId() + SUFFIX_USER_ID);
String tenantId = ctx.getSessionValue(scope.getScopeId() + SUFFIX_TENANT_ID).asString();
+ Long ttl = ctx.getSessionValue(scope.getScopeId() + SUFFIX_TTL).getLong();
+
+ if (ttl != null && ttl < System.currentTimeMillis()) {
+ return null;
+ }
if (!userId.isFilled() || !isUserStillValid(userId.asString())) {
return null;
@@ -483,33 +443,16 @@ protected UserSettings getScopeSettings() {
@Nonnull
protected abstract String computeLang(WebContext ctx, String userId);
- /**
- * Attaches the given user to the current session.
- *
- * This will make the login persistent across requests (if session management is enabled).
- *
- * @param user the user to attach to the session
- * @param ctx the current request to attach the user to
- */
- @Override
- public void attachToSession(@Nonnull UserInfo user, @Nonnull WebContext ctx) {
- ctx.setSessionValue(scope.getScopeId() + SUFFIX_TENANT_ID, user.getTenantId());
- ctx.setSessionValue(scope.getScopeId() + SUFFIX_USER_ID, user.getUserId());
- }
-
/**
* Removes all stored user information from the current session.
*
- * @param user the current user - passed in, in case a cache etc. has to be cleared
- * @param ctx the request to remove all data from
+ * @param ctx the request to remove all data from
*/
@Override
- public void detachFromSession(@Nonnull UserInfo user, @Nonnull WebContext ctx) {
+ public void logout(@Nonnull WebContext ctx) {
ctx.setSessionValue(scope.getScopeId() + SUFFIX_TENANT_ID, null);
ctx.setSessionValue(scope.getScopeId() + SUFFIX_USER_ID, null);
-
- ctx.deleteCookie(scope.getScopeId() + USER_COOKIE_SUFFIX);
- ctx.deleteCookie(scope.getScopeId() + TOKEN_COOKIE_SUFFIX);
+ ctx.setSessionValue(scope.getScopeId() + SUFFIX_TTL, null);
}
@Override
diff --git a/src/main/java/sirius/web/security/PublicUserManager.java b/src/main/java/sirius/web/security/PublicUserManager.java
index 41a78ed88..18a99529f 100644
--- a/src/main/java/sirius/web/security/PublicUserManager.java
+++ b/src/main/java/sirius/web/security/PublicUserManager.java
@@ -27,9 +27,6 @@
* This roles granted can be controlled by two config entries. One is security.publicRoles which
* also affects all other user managers. The other is defaultRoles which has to be defined within
* the scope.
- *
- * Note that also trustedRoles can be defined to control roles which are only added to a trusted user
- * (i.e. from the local network).
*/
public class PublicUserManager extends GenericUserManager {
@@ -53,7 +50,7 @@ protected PublicUserManager(ScopeInfo scope, Extension config) {
super(scope, config);
this.user = UserInfo.Builder.createUser(PUBLIC_PLACEHOLDER)
.withUsername(PUBLIC_PLACEHOLDER)
- .withPermissions(transformRoles(Collections.emptyList(), false))
+ .withPermissions(transformRoles(Collections.emptyList()))
.build();
}
@@ -103,12 +100,7 @@ protected String computeLang(WebContext ctx, String userId) {
}
@Override
- public void attachToSession(@Nonnull UserInfo user, @Nonnull WebContext ctx) {
- // Not required - there is actually no user...
- }
-
- @Override
- public void detachFromSession(@Nonnull UserInfo user, @Nonnull WebContext ctx) {
+ public void logout(@Nonnull WebContext ctx) {
// Not required - there is actually no user...
}
diff --git a/src/main/java/sirius/web/security/UserContext.java b/src/main/java/sirius/web/security/UserContext.java
index b8b054717..26d4424bb 100644
--- a/src/main/java/sirius/web/security/UserContext.java
+++ b/src/main/java/sirius/web/security/UserContext.java
@@ -28,6 +28,7 @@
import java.util.List;
import java.util.Map;
import java.util.Objects;
+import java.util.Optional;
/**
* Used to access the current user and scope.
@@ -210,12 +211,39 @@ private void bindUserToRequest(WebContext ctx) {
UserManager manager = getUserManager();
UserInfo user = manager.bindToRequest(ctx);
setCurrentUser(user);
- CallContext.getCurrent().setLang(user.getLang());
} else {
setCurrentUser(UserInfo.NOBODY);
}
}
+ /**
+ * Bind a user from the session if available.
+ *
+ * If no user is available (currently logged in) nothing will happen. User {@link #getUser()}
+ * to fully bind a user and attempt a login.
+ *
+ * @param ctx the current web context to bind against
+ * @return the user which was found in the session or an empty optional if none is present
+ */
+ public Optional bindUserIfPresent(WebContext ctx) {
+ if (ctx == null || !ctx.isValid()) {
+ return Optional.empty();
+ }
+
+ if (currentUser != null) {
+ return Optional.of(currentUser);
+ }
+
+ UserManager manager = getUserManager();
+ UserInfo user = manager.findUserForRequest(ctx);
+ if (user.isLoggedIn()) {
+ setCurrentUser(user);
+ return Optional.of(user);
+ }
+
+ return Optional.empty();
+ }
+
/**
* Installs the given scope as current scope.
*
@@ -243,8 +271,10 @@ public void setCurrentScope(ScopeInfo scope) {
*/
public void setCurrentUser(@Nullable UserInfo user) {
this.currentUser = user == null ? UserInfo.NOBODY : user;
- CallContext.getCurrent().addToMDC(MDC_USER_ID, () -> currentUser.getUserId());
- CallContext.getCurrent().addToMDC(MDC_USER_NAME, () -> currentUser.getUserName());
+ CallContext call = CallContext.getCurrent();
+ call.addToMDC(MDC_USER_ID, () -> currentUser.getUserId());
+ call.addToMDC(MDC_USER_NAME, () -> currentUser.getUserName());
+ call.setLang(user.getLang());
}
/**
@@ -411,7 +441,7 @@ public String getFieldErrorMessage(String field) {
/**
* Returns the current user.
*
- * If no user is present yet, it tries to parse the current {@link WebContext} and retireve the user from the
+ * If no user is present yet, it tries to parse the current {@link WebContext} and retrieve the user from the
* session.
*
* @return the currently active user
@@ -465,23 +495,6 @@ public boolean isUserPresent() {
return currentUser != null;
}
- /**
- * Binds the currently active user to the session.
- *
- * This will make the authentication of a user persistent als long as the session remains
- */
- public void attachUserToSession() {
- WebContext ctx = CallContext.getCurrent().get(WebContext.class);
- if (!ctx.isValid()) {
- return;
- }
- if (!getUser().isLoggedIn()) {
- return;
- }
- UserManager manager = getUserManager();
- manager.attachToSession(getUser(), ctx);
- }
-
/**
* Determines and returns the current user manager.
*
@@ -507,7 +520,7 @@ public void detachUserFromSession() {
return;
}
UserManager manager = getUserManager();
- manager.detachFromSession(getCurrentUser(), ctx);
+ manager.logout(ctx);
}
/**
diff --git a/src/main/java/sirius/web/security/UserManager.java b/src/main/java/sirius/web/security/UserManager.java
index ccb8946fb..75d63f0ef 100644
--- a/src/main/java/sirius/web/security/UserManager.java
+++ b/src/main/java/sirius/web/security/UserManager.java
@@ -65,23 +65,14 @@ public interface UserManager {
@Nullable
UserInfo findUserByCredentials(@Nullable WebContext ctx, String user, String password);
- /**
- * Makes the currently authenticated user persistent by storing the required information in the session.
- *
- * @param user the user to store
- * @param ctx the request containing the session
- */
- void attachToSession(@Nonnull UserInfo user, @Nonnull WebContext ctx);
-
/**
* Removes all stored data from the session
*
* This can be considered a logout operation.
*
- * @param user the user to logout
* @param ctx the request containing the session
*/
- void detachFromSession(@Nonnull UserInfo user, @Nonnull WebContext ctx);
+ void logout(@Nonnull WebContext ctx);
/**
* Determines if a login via username and password is possible.
diff --git a/src/main/java/sirius/web/services/ServiceDispatcher.java b/src/main/java/sirius/web/services/ServiceDispatcher.java
index 7cc565ae1..4f70f5fd8 100644
--- a/src/main/java/sirius/web/services/ServiceDispatcher.java
+++ b/src/main/java/sirius/web/services/ServiceDispatcher.java
@@ -11,6 +11,7 @@
import io.netty.handler.codec.http.HttpResponseStatus;
import sirius.kernel.async.CallContext;
import sirius.kernel.async.TaskContext;
+import sirius.kernel.commons.CachingSupplier;
import sirius.kernel.commons.PriorityCollector;
import sirius.kernel.commons.Strings;
import sirius.kernel.commons.Tuple;
@@ -113,22 +114,10 @@ private void invokeService(WebContext ctx, ServiceCall call, StructuredService s
}
}
- // Install language
- CallContext.getCurrent().setLang(NLS.makeLang(ctx.getLang()));
-
- // Install user. This is forcefully called here to ensure that the ScopeDetetor
- // and the user manager are guaranteed to be invoked one we enter the service code...
- UserInfo user = UserContext.getCurrentUser();
-
- // If the underlying ScopeDetector made a redirect (for whatever reasons)
- // the response will be committed and we can (must) safely return...
- if (ctx.isResponseCommitted()) {
- return;
- }
-
// ... and check permissions
+ CachingSupplier userSupplier = new CachingSupplier<>(UserContext::getCurrentUser);
for (String p : Permissions.computePermissionsFromAnnotations(serv.getClass())) {
- if (!user.hasPermission(p)) {
+ if (!userSupplier.get().hasPermission(p)) {
ctx.respondWith().error(HttpResponseStatus.UNAUTHORIZED, "Missing permission: " + p);
return;
}
diff --git a/src/main/resources/component-web.conf b/src/main/resources/component-web.conf
index 7c813edf1..6498a5f11 100644
--- a/src/main/resources/component-web.conf
+++ b/src/main/resources/component-web.conf
@@ -435,9 +435,6 @@ security {
# Defines roles granted to all "users".
defaultRoles = [ "permission-system-state", "permission-system-tags", "permission-system-tags-state" ]
-
- # Defines roles granted to trusted users (Those whose IP match http.firewall.trustedIPs).
- trustedRoles = [ ]
}
}
diff --git a/src/test/java/sirius/web/controller/TestController.java b/src/test/java/sirius/web/controller/TestController.java
index fb4fcda77..3865965e6 100644
--- a/src/test/java/sirius/web/controller/TestController.java
+++ b/src/test/java/sirius/web/controller/TestController.java
@@ -53,6 +53,12 @@ public void testJSON(WebContext ctx, JSONStructuredOutput out) {
out.property("test", ctx.getParameter("test"));
}
+ @Routed("/test/cookieCacheTest")
+ public void testCookieCacheTest(WebContext ctx) {
+ ctx.setCookie("Test", "1", 3600);
+ ctx.respondWith().cached().direct(HttpResponseStatus.OK, "OK");
+ }
+
@Routed(value = "/test/json-param/:1", jsonCall = true)
public void testJSONParam(WebContext ctx, JSONStructuredOutput out, String param) {
out.property("test", param);
diff --git a/src/test/java/sirius/web/http/WebContextSpec.groovy b/src/test/java/sirius/web/http/WebContextSpec.groovy
index cafa71d67..57e8719b8 100644
--- a/src/test/java/sirius/web/http/WebContextSpec.groovy
+++ b/src/test/java/sirius/web/http/WebContextSpec.groovy
@@ -8,6 +8,8 @@
package sirius.web.http
+import io.netty.handler.codec.http.HttpHeaderNames
+import io.netty.handler.codec.http.HttpHeaderValues
import sirius.kernel.BaseSpecification
class WebContextSpec extends BaseSpecification {
@@ -64,4 +66,17 @@ class WebContextSpec extends BaseSpecification {
and:
r.get("a").isFilled()
}
+
+
+ def "parseAcceptLanguage works as expected"(header, lang) {
+ expect:
+ TestRequest.GET("/test?a=a").addHeader(HttpHeaderNames.ACCEPT_LANGUAGE, header).getLang() == lang
+ where:
+ header | lang
+ "de, en;q=0.8" | "de"
+ "en, de;q=0.8" | "en"
+ "xx, de;q=0.8, en-gb;q=0.7" | "de"
+ "xx, de;q=0.5, en-gb;q=0.7" | "en"
+ }
+
}
diff --git a/src/test/java/sirius/web/http/WebServerSpec.groovy b/src/test/java/sirius/web/http/WebServerSpec.groovy
index 6ac514c03..2b9017cc4 100644
--- a/src/test/java/sirius/web/http/WebServerSpec.groovy
+++ b/src/test/java/sirius/web/http/WebServerSpec.groovy
@@ -28,9 +28,6 @@ import sirius.kernel.health.LogHelper
*
* This ensures a basic performance profile and also makes sure no trivial race conditions or memory leaks
* are added.
- *
- * @author Andreas Haufler (aha@scireum.de)
- * @since 2015/05
*/
class WebServerSpec extends BaseSpecification {
@@ -40,7 +37,11 @@ class WebServerSpec extends BaseSpecification {
c.connect()
def result = new String(ByteStreams.toByteArray(c.getInputStream()), Charsets.UTF_8)
expectedHeaders.each { k, v ->
- if (!Strings.areEqual(c.getHeaderField(k), v)) {
+ if ("*" == v) {
+ if (Strings.isEmpty(c.getHeaderField(k))) {
+ throw new IllegalStateException("Header: " + k + " was expected, but not set")
+ }
+ } else if (!Strings.areEqual(c.getHeaderField(k), v)) {
throw new IllegalStateException("Header: " + k + " was " + c.getHeaderField(k) + " instead of " + v)
}
}
@@ -48,6 +49,21 @@ class WebServerSpec extends BaseSpecification {
return result
}
+ /**
+ * Ensures that set-cookie and caching headers aren't mixed.
+ */
+ def "Invoke /test/cookieCacheTest"() {
+ given:
+ def uri = "/test/cookieCacheTest"
+ def headers = ['accept-encoding': 'gzip']
+ // File is too small to be compressed!
+ def expectedHeaders = ['set-cookie': '*', 'expires': null, 'cache-control': 'no-cache, max-age=0']
+ when:
+ def data = callAndRead(uri, headers, expectedHeaders)
+ then:
+ notThrown(IllegalStateException)
+ }
+
def "Invoke /assets/test.css to test"() {
given:
def uri = "/assets/test.css"
@@ -314,7 +330,7 @@ class WebServerSpec extends BaseSpecification {
when:
u.setRequestMethod("POST")
u.setRequestProperty("Content-Type",
- "application/x-www-form-urlencoded")
+ "application/x-www-form-urlencoded")
u.setRequestProperty("Content-Length", Integer.toString(testString.getBytes().length))
u.setDoInput(true)
@@ -351,7 +367,7 @@ class WebServerSpec extends BaseSpecification {
when:
u.setRequestMethod("POST")
u.setRequestProperty("Content-Type",
- "application/x-www-form-urlencoded")
+ "application/x-www-form-urlencoded")
u.setRequestProperty("Content-Length", Integer.toString(testString.getBytes().length))
u.setDoInput(true)
@@ -400,11 +416,11 @@ class WebServerSpec extends BaseSpecification {
def counter = 0
def count = 0
def buffer = new byte[8192]
- while((count = input.read(buffer)) > 0) {
+ while ((count = input.read(buffer)) > 0) {
counter += count
}
- return counter;
+ return counter
}
def "HTTP pipelining is supported correctly"() {
diff --git a/src/test/resources/test.conf b/src/test/resources/test.conf
index a65d55cfb..2351c42e2 100644
--- a/src/test/resources/test.conf
+++ b/src/test/resources/test.conf
@@ -2,4 +2,6 @@ sirius.frameworks {
web.test-firewall: true
}
+nls.languages = [de, en]
+
sirius.customizations = ["customized"]