All Downloads are FREE. Search and download functionalities are using the official Maven repository.

sirius.web.security.UserContext Maven / Gradle / Ivy

There is a newer version: 22.2.3
Show newest version
/*
 * Made with all the love in the world
 * by scireum in Remshalden, Germany
 *
 * Copyright by scireum GmbH
 * http://www.scireum.de - [email protected]
 */

package sirius.web.security;

import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import sirius.kernel.Sirius;
import sirius.kernel.async.CallContext;
import sirius.kernel.async.SubContext;
import sirius.kernel.commons.Strings;
import sirius.kernel.di.std.Part;
import sirius.kernel.di.std.Parts;
import sirius.kernel.health.Log;
import sirius.kernel.nls.NLS;
import sirius.web.controller.Message;
import sirius.web.http.UserMessagesCache;
import sirius.web.http.WebContext;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;

/**
 * Used to access the current user and scope.
 * 

* An instance of this class is present in the {@link sirius.kernel.async.CallContext} and takes care of * picking the right user manager to authenticate users or store / load them from a session. *

* This class also manages messages shown to the user. */ public class UserContext implements SubContext { /** * The key used to store the current scope in the MDC */ public static final String MDC_SCOPE = "scope"; /** * The key used to store the current user id in the MDC */ public static final String MDC_USER_ID = "userId"; /** * The key used to store the current user name in the MDC */ public static final String MDC_USER_NAME = "username"; /** * Contains the logger user used by the auth framework */ public static final Log LOG = Log.get("user"); @Part private static ScopeDetector detector; @Part private static UserMessagesCache userMessagesCache; @Parts(MessageProvider.class) private static Collection messageProviders; private UserInfo currentUser = null; private boolean fetchingCurrentUser = false; /* * As getUserForScope will most probably only hit one other scope * we cache the last user and scope here to speed up almost all cases * without the need for a map. */ private String scopeIdOfCachedUser = null; private UserInfo cachedUser = null; private ScopeInfo currentScope = null; private boolean fetchingCurrentScope = false; private List msgList = Lists.newArrayList(); private Map fieldErrors = Maps.newHashMap(); private Map fieldErrorMessages = Maps.newHashMap(); /** * Retrieves the current UserContext from the {@link sirius.kernel.async.CallContext}. * * @return the current user context. */ public static UserContext get() { return CallContext.getCurrent().get(UserContext.class); } /** * Boilerplate method to quickly access the current user. * * @return the current user * @see #getUser() */ public static UserInfo getCurrentUser() { return get().getUser(); } /** * Returns the configuration with is specific to the current user. *

* This is boilerplate for {@code UserContext.getCurrentUser().getConfig()}. * * @return the config for the current user * @see UserInfo#getSettings() */ public static UserSettings getSettings() { return get().getUser().getSettings(); } /** * Returns the helper of the given class for the current scope. *

* NOTE: This helper is per {@link ScopeInfo} not per {@link UserInfo}! Therefore no user dependent data may be kept * in its state. * * @param helperType the type of the helper to fetch * @param the generic type of the helper * @return an instance of the given helper. If the helper can neither be found nor created, an exception will be * thrown. */ @Nonnull public static H getHelper(@Nonnull Class helperType) { return getCurrentScope().getHelper(helperType); } /** * Returns the helper with the given name for the current scope. *

* NOTE: This helper is per {@link ScopeInfo} not per {@link UserInfo}! Therefore no user dependent data may be kept * in its state. * * @param name the name of the helper to fetch * @param the generic type of the helper * @return an instance of the given helper. If the helper can neither be found nor created, an exception will be * thrown. */ @Nonnull public static H getHelper(@Nonnull String name) { return getCurrentScope().getHelper(name); } /** * Boilerplate method to quickly access the current scope. * * @return the currently active scope * @see #getScope() */ public static ScopeInfo getCurrentScope() { return get().getScope(); } /** * Handles the given exception by passing it to {@link sirius.kernel.health.Exceptions} and by creating an * appropriate message for the user. * * @param e the exception to handle. If the given exception is null nothing will happen. */ public static void handle(@Nullable Throwable e) { if (e == null) { return; } message(Message.error(e)); } /** * Adds a message to the current UserContext. * * @param msg the message to add */ public static void message(Message msg) { get().addMessage(msg); } /** * Adds a field error to the current UserContext. * * @param field the field for which an error occurred * @param value the value which was rejected */ public static void setFieldError(String field, Object value) { get().addFieldError(field, NLS.toUserString(value)); } /* * Loads the current scope from the given web context. */ private void bindScopeToRequest(WebContext ctx) { if (ctx != null && ctx.isValid() && detector != null) { ScopeInfo scope = detector.detectScope(ctx); setCurrentScope(scope); CallContext.getCurrent().setLangIfEmpty(scope.getLang()); } else { setCurrentScope(ScopeInfo.DEFAULT_SCOPE); } } /* * Loads the current user from the given web context. */ private void bindUserToRequest(WebContext ctx) { if (ctx != null && ctx.isValid()) { UserManager manager = getUserManager(); UserInfo user = manager.bindToRequest(ctx); setCurrentUser(user); } 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. *

* For generic web requests, this is not necessary, as the scope is auto-detected. * * @param scope the scope to set */ public void setCurrentScope(ScopeInfo scope) { this.currentScope = scope == null ? ScopeInfo.DEFAULT_SCOPE : scope; if (this.currentUser != null) { this.currentUser = null; CallContext.getCurrent().removeFromMDC(MDC_USER_ID); CallContext.getCurrent().removeFromMDC(MDC_USER_NAME); } CallContext.getCurrent().addToMDC(MDC_SCOPE, () -> currentScope.getScopeId()); } /** * Installs the given user as current user. *

* For generic web requests, this is not necessary, as the user is auto-detected. * * @param user the user to set */ public void setCurrentUser(@Nullable UserInfo user) { this.currentUser = user == null ? UserInfo.NOBODY : user; CallContext call = CallContext.getCurrent(); call.addToMDC(MDC_USER_ID, () -> currentUser.getUserId()); call.addToMDC(MDC_USER_NAME, () -> currentUser.getUserName()); call.setLangIfEmpty(currentUser.getLang()); } /** * Executes the given section as the given user. *

* Restores the previously active user once the section is left. * * @param user the user to install * @param section the section to execute as user */ public void runAs(@Nullable UserInfo user, @Nonnull Runnable section) { UserInfo lastUser = getCurrentUser(); try { setCurrentUser(user); section.run(); } finally { setCurrentUser(lastUser); } } /** * Adds a message to be shown to the user. * * @param msg the message to be shown to the user */ public void addMessage(Message msg) { msgList.add(msg); } /** * Returns all messages to be shown to the user. * * @return a list of messages to be shown to the user */ public List getMessages() { userMessagesCache.restoreCachedUserMessages(CallContext.getCurrent().get(WebContext.class)); if (!Sirius.isStartedAsTest()) { getScope().tryAs(MaintenanceInfo.class) .filter(info -> !info.isLocked()) .map(MaintenanceInfo::maintenanceMessage) .filter(Objects::nonNull) .ifPresent(this::addMessage); getScope().tryAs(MessageProvider.class).ifPresent(provider -> provider.addMessages(this::addMessage)); getUser().tryAs(MessageProvider.class).ifPresent(provider -> provider.addMessages(this::addMessage)); messageProviders.forEach(provider -> provider.addMessages(this::addMessage)); } return msgList; } /** * Returns all user specific messages without any globally or locally generated ones. * * @return a list of "real" messages which were created while processing the current request */ public List getUserSpecificMessages() { return msgList; } /** * Adds an error for a given field * * @param field the name of the field * @param value the value which was supplied and rejected */ public void addFieldError(String field, String value) { fieldErrors.put(field, value); } /** * Determines if there is an error or error message for the given field * * @param field the field to check for errors * @return true if an error was added for the field, false otherwise */ public boolean hasError(String field) { return fieldErrors.containsKey(field) || fieldErrorMessages.containsKey(field); } /** * Returns "has-error" if an error was added for the given field. * * @param field the field to check * @return "has-error" if an error was added for the given field, an empty string otherwise */ public String signalFieldError(String field) { return hasError(field) ? "has-error" : ""; } /** * Returns the originally submitted field value even if it was rejected due to an error. * * @param field the name of the form field * @param value the entity value (used if no error occurred) * @return the originally submitted value (if an error occurred), the given value otherwise */ public String getFieldValue(String field, Object value) { if (fieldErrors.containsKey(field)) { return fieldErrors.get(field); } return NLS.toUserString(value); } /** * Returns the originally submitted field value even if it was rejected due to an error. * * @param field the name of the form field * @return the originally submitted value (if an error occurred) or the parameter (using field as name) * from the current {@link WebContext} otherwise */ public String getFieldValue(String field) { if (fieldErrors.containsKey(field)) { return fieldErrors.get(field); } return CallContext.getCurrent().get(WebContext.class).get(field).getString(); } /** * Returns all values submitted for the given field * * @param field the name of the field which values should be extracted * @return a list of values submitted for the given field */ public Collection getFieldValues(String field) { return CallContext.getCurrent().get(WebContext.class).getParameters(field); } /** * Adds an error message for the given field * * @param field name of the form field * @param errorMessage value to be added */ public static void setErrorMessage(String field, String errorMessage) { get().addFieldErrorMessage(field, errorMessage); } /** * Adds an error message for the given field * * @param field name of the form field * @param errorMessage value to be added */ public void addFieldErrorMessage(String field, String errorMessage) { fieldErrorMessages.put(field, errorMessage); } /** * Returns an error message for the given field * * @param field name of the form field * @return error message if existant else an empty string */ public String getFieldErrorMessage(String field) { if (fieldErrorMessages.containsKey(field)) { return fieldErrorMessages.get(field); } return ""; } /** * Returns the current user. *

* 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 */ public UserInfo getUser() { if (currentUser == null) { if (fetchingCurrentUser) { return UserInfo.NOBODY; } try { fetchingCurrentUser = true; bindUserToRequest(CallContext.getCurrent().get(WebContext.class)); } finally { fetchingCurrentUser = false; } } return currentUser; } /** * Returns the used which would be the current user if the space with the given id would be active. *

* You can use {@link ScopeInfo#DEFAULT_SCOPE} to access the user of the default scope which will * most probably the administrative backend. *

* Note that this method will only check the session ({@link UserManager#findUserForRequest(WebContext)}) and will * not try to perform a login via credentials as given in the current request. * * @param scope the scope to fetch the user for * @return the user found for the given scope or {@link UserInfo#NOBODY} if no user was found */ public UserInfo getUserForScope(ScopeInfo scope) { if (cachedUser != null && Strings.areEqual(scope.getScopeId(), scopeIdOfCachedUser)) { return cachedUser; } cachedUser = scope.getUserManager().findUserForRequest(CallContext.getCurrent().get(WebContext.class)); scopeIdOfCachedUser = scope.getScopeId(); return cachedUser; } /** * Determines if the user is present. *

* This can be either direclty via a {@link #setCurrentUser(UserInfo)} or implicitely via {@link #getUser()} * * @return the currently active user */ public boolean isUserPresent() { return currentUser != null; } /** * Determines and returns the current user manager. *

* The user manager is determined by the current scope. * * @return the currently active user manager * @see #getCurrentScope() * @see ScopeDetector */ @Nonnull public UserManager getUserManager() { return getScope().getUserManager(); } /** * Removes the authentication and user identity from the session. *

* This can be considered a logout. */ public void detachUserFromSession() { WebContext ctx = CallContext.getCurrent().get(WebContext.class); if (!ctx.isValid()) { return; } UserManager manager = getUserManager(); manager.logout(ctx); } /** * Returns the currently active scope. *

* This is determined using the {@link ScopeDetector} or it will always be the {@link ScopeInfo#DEFAULT_SCOPE} if * no scope detector is present. * * @return the currently active scope */ public ScopeInfo getScope() { if (currentScope == null) { if (fetchingCurrentScope) { return ScopeInfo.DEFAULT_SCOPE; } try { fetchingCurrentScope = true; bindScopeToRequest(CallContext.getCurrent().get(WebContext.class)); } finally { fetchingCurrentScope = false; } } return currentScope; } @Override public SubContext fork() { // We return a copy which keeps the same user and scope - but which can be change independently. // Otherwise a UserContext.runAs(...) which forks a task, would run into trouble as the // context is immediatelly switched back. UserContext child = new UserContext(); child.currentUser = currentUser; child.currentScope = currentScope; child.msgList = msgList; child.fieldErrors = fieldErrors; return child; } @Override public void detach() { // No action needed when this is detached from the current thread... } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy