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

sirius.biz.tenants.TenantUserManager Maven / Gradle / Ivy

There is a newer version: 9.6
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.biz.tenants;

import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.typesafe.config.Config;
import sirius.biz.model.LoginData;
import sirius.biz.statistics.AggregationLevel;
import sirius.biz.statistics.StatisticalEvent;
import sirius.biz.statistics.Statistics;
import sirius.biz.web.BizController;
import sirius.db.mixing.Entity;
import sirius.db.mixing.OMA;
import sirius.kernel.cache.Cache;
import sirius.kernel.cache.CacheManager;
import sirius.kernel.commons.Explain;
import sirius.kernel.commons.Strings;
import sirius.kernel.commons.Tuple;
import sirius.kernel.commons.Value;
import sirius.kernel.di.std.Part;
import sirius.kernel.di.std.Register;
import sirius.kernel.health.Exceptions;
import sirius.kernel.nls.NLS;
import sirius.kernel.settings.Extension;
import sirius.web.http.WebContext;
import sirius.web.security.GenericUserManager;
import sirius.web.security.ScopeInfo;
import sirius.web.security.UserInfo;
import sirius.web.security.UserManager;
import sirius.web.security.UserManagerFactory;
import sirius.web.security.UserSettings;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.Set;

/**
 * Provides a {@link UserManager} for {@link Tenant} and {@link UserAccount}.
 * 

* The user managed can be installed by setting the manager property of the scope to tenants * in the system config. *

* This is the default user manager for the default scope in sirius-biz. */ public class TenantUserManager extends GenericUserManager { /** * This flag permission is granted to all users which belong to the system tenant. *

* The id of the system tenant can be set in the scope config. The system tenant usually is the administrative * company which owns / runs the system. */ public static final String PERMISSION_SYSTEM_TENANT = "flag-system-tenant"; /** * Contains the permission required to manage the system. *

* If this permission is granted for user accounts that belong to the system tenant, the PERMISSION_SYSTEM_TENANT * flag is added to the users roles */ public static final String PERMISSION_MANAGE_SYSTEM = "permission-manage-system"; /** * This flag indicates that the current user either has taken control over another tenant or uses account. */ public static final String PERMISSION_SPY_USER = "flag-spy-user"; /** * Contains the permission required to switch the user account. */ public static final String PERMISSION_SELECT_USER_ACCOUNT = "permission-select-user-account"; /** * Contains the permission required to switch the tenant. */ public static final String PERMISSION_SELECT_TENANT = "permission-select-tenant"; /** * If a session-value named {@code UserContext.getCurrentScope().getScopeId() + * TenantUserManager.TENANT_SPY_ID_SUFFIX} * is present, the user will belong to the given tenant and not to his own one. *

* This is used by support and administrative tasks. Beware, that the id is not checked, so the one who installs * the * ID has to verify that the user is allowed to switch to this tenant. */ public static final String TENANT_SPY_ID_SUFFIX = "-tenant-spy-id"; /** * If a session-value named {@code UserContext.getCurrentUser().getUserId() + TenantUserManager.SPY_ID_SUFFIX} * is present, the user with the given ID will be used, instead of the current one. *

* This is used by support and administrative tasks. Beware, that the id is not checked, so the one who installs the * ID has to verify that the user is allowed to become this user. */ public static final String SPY_ID_SUFFIX = "-spy-id"; private final String systemTenant; private final String defaultSalt; private final boolean acceptApiTokens; private final boolean autocreateTenant; @Part private static OMA oma; @Part private static Statistics statistics; private static final StatisticalEvent LOGGED_IN_EVENT = StatisticalEvent.create("useraccount-login", AggregationLevel.MONTHS); private static Cache> rolesCache = CacheManager.createCache("tenants-roles"); private static Cache userAccountCache = CacheManager.createCache("tenants-users"); private static Cache tenantsCache = CacheManager.createCache("tenants-tenants"); private static Cache> configCache = CacheManager.createCache("tenants-configs"); protected TenantUserManager(ScopeInfo scope, Extension config) { super(scope, config); this.systemTenant = config.get("system-tenant").asString(); this.defaultSalt = config.get("default-salt").asString(""); this.acceptApiTokens = config.get("accept-api-tokens").asBoolean(true); this.autocreateTenant = config.get("autocreate-tenant").asBoolean(true); } /** * Creates a new user manager for the given scope and configuration. */ @Register(name = "tenants", framework = "biz.tenants") public static class Factory implements UserManagerFactory { @Nonnull @Override public UserManager createManager(@Nonnull ScopeInfo scope, @Nonnull Extension config) { return new TenantUserManager(scope, config); } } /** * Flushes all caches for the given account. * * @param account the account to flush */ public static void flushCacheForUserAccount(UserAccount account) { rolesCache.remove(account.getUniqueName()); userAccountCache.remove(account.getUniqueName()); configCache.remove(account.getUniqueName()); } /** * Flushes all cahes for the given tenant. * * @param tenant the tenant to flush */ public static void flushCacheForTenant(Tenant tenant) { tenantsCache.remove(tenant.getIdAsString()); configCache.remove(tenant.getUniqueName()); configCache.removeIf(cachedValue -> Strings.areEqual(cachedValue.getValue().getSecond(), tenant.getUniqueName())); } @Override protected UserInfo findUserInSession(WebContext ctx) { UserInfo rootUser = super.findUserInSession(ctx); if (rootUser == null || defaultUser.equals(rootUser)) { return rootUser; } String spyId = ctx.getSessionValue(scope.getScopeId() + SPY_ID_SUFFIX).asString(); if (Strings.isFilled(spyId)) { UserInfo spy = becomeSpyUser(spyId, rootUser); if (spy != null) { return spy; } } String tenantSpyId = ctx.getSessionValue(scope.getScopeId() + TENANT_SPY_ID_SUFFIX).asString(); if (Strings.isFilled(tenantSpyId)) { return createUserWithTenant(rootUser, tenantSpyId); } return rootUser; } private UserInfo becomeSpyUser(String spyId, UserInfo rootUser) { UserAccount spyUser = fetchAccount(spyId, null); if (spyUser == null) { return null; } List extraRoles = Lists.newArrayList(); extraRoles.add(PERMISSION_SPY_USER); extraRoles.add(PERMISSION_SELECT_USER_ACCOUNT); if (rootUser.hasPermission(PERMISSION_SYSTEM_TENANT)) { extraRoles.add(PERMISSION_SYSTEM_TENANT); } return asUser(spyUser, extraRoles); } /** * Creates a copy of the given UserInfo with a new tenant id. *

* As a user can switch to other tenants, we must be able to create a "fake" user info, which contains the given * tenant data. * * @param originalUser the user which is actually logged in * @param tenantId the id of the tenant to become * @return a new user object, with the original user data but a modified tenant id and object */ public UserInfo createUserWithTenant(UserInfo originalUser, String tenantId) { if (Strings.isEmpty(tenantId) || Strings.areEqual(originalUser.getTenantId(), tenantId)) { return originalUser; } Tenant tenant = fetchTenant(tenantId); if (tenant == null) { return originalUser; } // Copy all relevant data into a new object (outside of the cache)... UserAccount currentUser = originalUser.getUserObject(UserAccount.class); UserAccount modifiedUser = new UserAccount(); modifiedUser.setId(currentUser.getId()); modifiedUser.getLogin().setUsername(currentUser.getLogin().getUsername()); modifiedUser.setEmail(currentUser.getEmail()); modifiedUser.getPermissions().setConfigString(currentUser.getPermissions().getConfigString()); modifiedUser.getPermissions().getPermissions().addAll(currentUser.getPermissions().getPermissions()); // And overwrite with the new tenant... modifiedUser.getTenant().setValue(tenant); Set roles = computeRoles(modifiedUser, tenant, originalUser.hasPermission(PERMISSION_SYSTEM_TENANT)); roles.add(PERMISSION_SPY_USER); roles.add(PERMISSION_SELECT_TENANT); return asUserWithRoles(modifiedUser, roles); } @Nullable private Tenant fetchTenant(String tenantId) { return tenantsCache.get(tenantId, i -> oma.find(Tenant.class, i).orElse(null)); } public String getOriginalTenantId(WebContext ctx) { return ctx.getSessionValue(this.scope.getScopeId() + "-tenant-id").asString(); } @Override public void attachToSession(@Nonnull UserInfo user, @Nonnull WebContext ctx) { // If we're being a spy user (pretending to have a different user id or tenant), // there is no need to update the session data - it would rather be error prone // as we would screw up the session data with the user data being spyed. if (user.hasPermission(PERMISSION_SPY_USER)) { return; } super.attachToSession(user, ctx); } @Override public UserInfo findUserByName(@Nullable WebContext ctx, String user) { if (Strings.isEmpty(user)) { return null; } Optional optionalAccount = oma.select(UserAccount.class).eq(UserAccount.LOGIN.inner(LoginData.USERNAME), user.toLowerCase()).one(); if (!optionalAccount.isPresent()) { optionalAccount = createSystemTenantIfNonExistent(); if (!optionalAccount.isPresent()) { return null; } } if (optionalAccount.get().getLogin().isAccountLocked()) { throw Exceptions.createHandled().withNLSKey("LoginData.accountIsLocked").handle(); } UserAccount account = fetchAccount(optionalAccount.get().getUniqueName(), optionalAccount.get()); if (account == null) { return null; } return asUser(account, null); } /** * Tries to find a {@link UserInfo} for the given unique object name of a {@link UserAccount}. * * @param accountId the unique object name of an UserAccount to resolve into a UserInfo * @return the UserInfo representing the given account (will utilize caches if available) or null * if no such user exists * @see Entity#getUniqueName() */ @Nullable public UserInfo findUserByUserId(String accountId) { UserAccount account = fetchAccount(accountId, null); if (account == null) { return null; } return asUser(account, null); } /** * Tries to fetch the requested account from the cache. *

* If a new account from the database is present (during login) this can be passed in as accountFromDB. If * not, the value can be left null and a lookup will be performed if necessary. This ensures, that the * cache is updated if a stale entry is detected during login. * * @param accountId the id of the account to fetch * @param givenAccount a fresh version from the database to check cache integrity * @return the most current version from the cache to re-use computed fields if possible */ @Nullable private UserAccount fetchAccount(@Nonnull String accountId, @Nullable UserAccount givenAccount) { UserAccount account; UserAccount accountFromDB = givenAccount; UserAccount accountFromCache = userAccountCache.get(accountId); if (accountFromCache == null || (accountFromDB != null && accountFromDB.getVersion() > accountFromCache.getVersion())) { if (accountFromDB == null) { accountFromDB = (UserAccount) oma.resolve(accountId).orElse(null); if (accountFromDB == null) { return null; } } userAccountCache.put(accountFromDB.getUniqueName(), accountFromDB); rolesCache.remove(accountFromDB.getUniqueName()); configCache.remove(accountFromDB.getUniqueName()); account = accountFromDB; } else { account = accountFromCache; } account.getTenant().setValue(fetchTenant(account, accountFromDB)); return account; } private Tenant fetchTenant(UserAccount account, @Nullable UserAccount accountFromDB) { Tenant tenantFromCache = tenantsCache.get(String.valueOf(account.getTenant().getId())); // if we found a tenant and no database object is present - we don't need // to check for updates or changes, just return the cached value. if (tenantFromCache != null && accountFromDB == null) { return tenantFromCache; } // ...otherwise, let's check if our cached instance is stil up to date Tenant tenantFromDB = oma.find(Tenant.class, account.getTenant().getId()) .orElseThrow(() -> new IllegalStateException(Strings.apply( "Tenant %s for UserAccount %s vanished!", account.getTenant().getId(), account.getId()))); // Only actually use the instance from the database if we either have no cached value // or if it is outdated. Otherwise the cached instance is preferred, because it might contain // useful pre-computed values... if (tenantFromCache == null || tenantFromDB.getVersion() > tenantFromCache.getVersion()) { tenantsCache.put(tenantFromDB.getIdAsString(), tenantFromDB); // We also need to re-compute the roles and config of the user // as this is also determined by the tenant rolesCache.remove(account.getUniqueName()); configCache.remove(account.getUniqueName()); configCache.remove(tenantFromDB.getUniqueName()); return tenantFromDB; } return tenantFromCache; } private Optional createSystemTenantIfNonExistent() { try { if (autocreateTenant && !oma.select(Tenant.class).exists()) { BizController.LOG.INFO("No tenant is present, creating system tenant...."); Tenant tenant = new Tenant(); tenant.setName("System Tenant"); tenant.getTrace().setSilent(true); oma.update(tenant); BizController.LOG.INFO( "No user account is present, creating system / system - Please change the password now!"); UserAccount ua = new UserAccount(); ua.getTenant().setValue(oma.select(Tenant.class).orderAsc(Tenant.ID).queryFirst()); ua.setEmail("[email protected]"); ua.getLogin().setUsername("system"); ua.getLogin().setCleartextPassword("system"); ua.getTrace().setSilent(true); // This should be enough to grant us more roles via the UI ua.getPermissions().getPermissions().add("administrator"); ua.getPermissions().getPermissions().add("user-administrator"); oma.update(ua); return Optional.of(ua); } } catch (Exception e) { Exceptions.handle() .to(BizController.LOG) .error(e) .withSystemErrorMessage("Cannot initialize tenants or user accounts: %s (%s)") .handle(); } return Optional.empty(); } protected UserInfo asUser(UserAccount account, List extraRoles) { Set roles = computeRoles(null, account.getUniqueName()); if (extraRoles != null) { // Make a copy so that we do not modify the cached set... roles = Sets.newTreeSet(roles); roles.addAll(extraRoles); } return asUserWithRoles(account, roles); } private UserInfo asUserWithRoles(UserAccount account, Set roles) { return UserInfo.Builder.createUser(account.getUniqueName()) .withUsername(account.getLogin().getUsername()) .withTenantId(String.valueOf(account.getTenant().getId())) .withTenantName(account.getTenant().getValue().getName()) .withLang(NLS.getDefaultLanguage()) .withPermissions(roles) .withSettingsSupplier(ui -> getUserSettings(getScopeSettings(), ui)) .withUserSupplier(u -> account) .build(); } @Override public UserInfo findUserByCredentials(@Nullable WebContext ctx, String user, String password) { if (Strings.isEmpty(password)) { return null; } UserInfo result = findUserByName(ctx, user); if (result == null) { return null; } UserAccount account = result.getUserObject(UserAccount.class); if (account.isExternalLoginRequired() && !isWithinInterval(account.getLogin().getLastExternalLogin(), account.getTenant() .getValue() .getExternalLoginIntervalDays())) { throw Exceptions.createHandled().withNLSKey("UserAccount.externalLoginMustBePerformed").handle(); } LoginData loginData = account.getLogin(); if (acceptApiTokens && Strings.areEqual(password, loginData.getApiToken())) { return result; } String salt = Value.of(loginData.getSalt()).asString(defaultSalt); String givenPasswordHash = LoginData.hashPassword(salt, password); if (givenPasswordHash.equals(loginData.getPasswordHash())) { return result; } return null; } @Override protected void recordUserLogin(WebContext ctx, UserInfo user) { recordLogin(user, false); } /** * Records a login which has either happened within this user manager or externally. * * @param user the user which logged in * @param external true if the login was performed via an external system like SAML, false otherwise */ public void recordLogin(UserInfo user, boolean external) { try { UserAccount account = (UserAccount) getUserObject(user); statistics.incrementStatistic(LOGGED_IN_EVENT, account.getUniqueName()); // This should never happen (other than manually changed or tampered database data). // However, this would lead to an endless recursion, so we skip right here.... if (account.getTrace().getCreatedAt() == null) { return; } account.getTrace().setSilent(true); account.getJournal().setSilent(true); account.getLogin().setNumberOfLogins(account.getLogin().getNumberOfLogins() + 1); account.getLogin().setLastLogin(LocalDateTime.now()); if (external) { account.getLogin().setLastExternalLogin(LocalDateTime.now()); } oma.override(account); } catch (Exception e) { Exceptions.handle(BizController.LOG, e); } } @Override protected Object getUserObject(UserInfo userInfo) { return fetchAccount(userInfo.getUserId(), null); } @Override protected UserSettings getUserSettings(UserSettings scopeSettings, UserInfo userInfo) { UserAccount user = userInfo.getUserObject(UserAccount.class); if (user.getPermissions().getConfig() == null) { if (user.getTenant().getValue().getPermissions().getConfig() == null) { return scopeSettings; } return configCache.get(user.getTenant().getUniqueObjectName(), i -> { Config cfg = scopeSettings.getConfig(); cfg = user.getTenant().getValue().getPermissions().getConfig().withFallback(cfg); return Tuple.create(new UserSettings(cfg), user.getTenant().getUniqueObjectName()); }).getFirst(); } return configCache.get(user.getUniqueName(), i -> { Config cfg = scopeSettings.getConfig(); cfg = user.getTenant().getValue().getPermissions().getConfig().withFallback(cfg); cfg = user.getPermissions().getConfig().withFallback(cfg); return Tuple.create(new UserSettings(cfg), user.getTenant().getUniqueObjectName()); }).getFirst(); } @Override @SuppressWarnings({"squid:S1126", "RedundantIfStatement"}) @Explain("Using explicit abort conditions and a final true makes all checks obvious") protected boolean isUserStillValid(String userId) { UserAccount user = fetchAccount(userId, null); if (user == null) { return false; } if (user.getLogin().isAccountLocked()) { return false; } if (!isWithinInterval(user.getLogin().getLastLogin(), user.getTenant().getValue().getLoginIntervalDays())) { return false; } if (user.isExternalLoginRequired() && !isWithinInterval(user.getLogin().getLastExternalLogin(), user.getTenant() .getValue() .getExternalLoginIntervalDays())) { return false; } return true; } private boolean isWithinInterval(LocalDateTime dateTime, Integer requiredInterval) { if (requiredInterval == null) { return true; } if (dateTime == null) { return false; } long actualInterval = Duration.between(LocalDateTime.now(), dateTime).toDays(); return actualInterval < requiredInterval; } private Set computeRoles(UserAccount user, Tenant tenant, boolean isSystemTenant) { Set roles = Sets.newTreeSet(); roles.addAll(user.getPermissions().getPermissions()); roles.addAll(tenant.getPermissions().getPermissions()); roles.add(UserInfo.PERMISSION_LOGGED_IN); Set transformedRoles = transformRoles(roles, false); if (isSystemTenant && transformedRoles.contains(PERMISSION_MANAGE_SYSTEM)) { roles.add(PERMISSION_SYSTEM_TENANT); return transformRoles(roles, false); } return transformedRoles; } @Override protected Set computeRoles(WebContext ctx, String userId) { Set cachedRoles = rolesCache.get(userId); if (cachedRoles != null) { return cachedRoles; } UserAccount user = fetchAccount(userId, null); Set roles; if (user != null) { roles = computeRoles(user, user.getTenant().getValue(), Strings.areEqual(systemTenant, String.valueOf(user.getTenant().getValue().getId()))); } else { roles = Collections.emptySet(); } rolesCache.put(userId, roles); return roles; } @Nonnull @Override protected String computeUsername(@Nullable WebContext ctx, String userId) { UserAccount account = fetchAccount(userId, null); if (account == null) { return "(unknown)"; } else { return account.getLogin().getUsername(); } } @Nonnull @Override protected String computeTenantname(@Nullable WebContext ctx, String tenantId) { Tenant tenant = fetchTenant(tenantId); if (tenant == null) { return "(unknown)"; } else { return tenant.getName(); } } @Nonnull @Override protected String computeLang(WebContext ctx, String userId) { return NLS.getDefaultLanguage(); } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy