package com.sap.sse.security.impl; import java.io.IOException; import java.io.InputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; import java.io.UnsupportedEncodingException; import java.math.BigDecimal; import java.net.MalformedURLException; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Map.Entry; import java.util.Optional; import java.util.Set; import java.util.UUID; import java.util.concurrent.Callable; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedDeque; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ConcurrentSkipListSet; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.function.Consumer; import java.util.function.Function; import java.util.logging.Level; import java.util.logging.Logger; import java.util.stream.Collectors; import javax.servlet.Filter; import javax.servlet.ServletContext; import javax.servlet.http.HttpServletRequest; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.IncorrectCredentialsException; import org.apache.shiro.authc.LockedAccountException; import org.apache.shiro.authc.UnknownAccountException; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.authz.AuthorizationException; import org.apache.shiro.authz.UnauthorizedException; import org.apache.shiro.config.Ini; import org.apache.shiro.config.Ini.Section; import org.apache.shiro.crypto.RandomNumberGenerator; import org.apache.shiro.crypto.SecureRandomNumberGenerator; import org.apache.shiro.crypto.hash.Sha256Hash; import org.apache.shiro.env.BasicIniEnvironment; import org.apache.shiro.env.Environment; import org.apache.shiro.mgt.SecurityManager; import org.apache.shiro.realm.Realm; import org.apache.shiro.session.Session; import org.apache.shiro.subject.Subject; import org.apache.shiro.web.env.IniWebEnvironment; import org.apache.shiro.web.filter.mgt.FilterChainManager; import org.apache.shiro.web.filter.mgt.FilterChainResolver; import org.apache.shiro.web.filter.mgt.PathMatchingFilterChainResolver; import org.apache.shiro.web.util.SavedRequest; import org.apache.shiro.web.util.WebUtils; import org.osgi.util.tracker.ServiceTracker; import org.scribe.builder.ServiceBuilder; import org.scribe.builder.api.FacebookApi; import org.scribe.builder.api.FlickrApi; import org.scribe.builder.api.Foursquare2Api; import org.scribe.builder.api.GoogleApi; import org.scribe.builder.api.ImgUrApi; import org.scribe.builder.api.LinkedInApi; import org.scribe.builder.api.LiveApi; import org.scribe.builder.api.TumblrApi; import org.scribe.builder.api.TwitterApi; import org.scribe.builder.api.VimeoApi; import org.scribe.builder.api.YahooApi; import org.scribe.model.Token; import org.scribe.oauth.OAuthService; import com.nulabinc.zxcvbn.StandardDictionaries; import com.nulabinc.zxcvbn.StandardKeyboards; import com.nulabinc.zxcvbn.Strength; import com.nulabinc.zxcvbn.Zxcvbn; import com.nulabinc.zxcvbn.ZxcvbnBuilder; import com.nulabinc.zxcvbn.io.ClasspathResource; import com.nulabinc.zxcvbn.matchers.SlantedKeyboardLoader; import com.sap.sse.ServerInfo; import com.sap.sse.common.Duration; import com.sap.sse.common.TimePoint; import com.sap.sse.common.Util; import com.sap.sse.common.Util.Pair; import com.sap.sse.common.http.HttpHeaderUtil; import com.sap.sse.common.mail.MailException; import com.sap.sse.common.media.TakedownNoticeRequestContext; import com.sap.sse.concurrent.LockUtil; import com.sap.sse.concurrent.NamedReentrantReadWriteLock; import com.sap.sse.i18n.impl.ResourceBundleStringMessagesImpl; import com.sap.sse.mail.MailService; import com.sap.sse.replication.interfaces.impl.AbstractReplicableWithObjectInputStream; import com.sap.sse.rest.CORSFilterConfiguration; import com.sap.sse.security.Action; import com.sap.sse.security.ClientUtils; import com.sap.sse.security.GithubApi; import com.sap.sse.security.InstagramApi; import com.sap.sse.security.OAuthRealm; import com.sap.sse.security.PermissionChangeListener; import com.sap.sse.security.SecurityInitializationCustomizer; import com.sap.sse.security.SecurityService; import com.sap.sse.security.SessionCacheManager; import com.sap.sse.security.SessionUtils; import com.sap.sse.security.ShiroWildcardPermissionFromParts; import com.sap.sse.security.interfaces.AccessControlStore; import com.sap.sse.security.interfaces.Credential; import com.sap.sse.security.interfaces.OAuthToken; import com.sap.sse.security.interfaces.SocialSettingsKeys; import com.sap.sse.security.interfaces.UserImpl; import com.sap.sse.security.interfaces.UserStore; import com.sap.sse.security.operations.AclAddPermissionOperation; import com.sap.sse.security.operations.AclPutPermissionsOperation; import com.sap.sse.security.operations.AclRemovePermissionOperation; import com.sap.sse.security.operations.AddPermissionForUserOperation; import com.sap.sse.security.operations.AddRoleForUserOperation; import com.sap.sse.security.operations.AddSettingOperation; import com.sap.sse.security.operations.AddUserToUserGroupOperation; import com.sap.sse.security.operations.CreateRoleDefinitionOperation; import com.sap.sse.security.operations.CreateUserGroupOperation; import com.sap.sse.security.operations.CreateUserOperation; import com.sap.sse.security.operations.DeleteAclOperation; import com.sap.sse.security.operations.DeleteOwnershipOperation; import com.sap.sse.security.operations.DeleteRoleDefinitionOperation; import com.sap.sse.security.operations.DeleteUserGroupOperation; import com.sap.sse.security.operations.DeleteUserOperation; import com.sap.sse.security.operations.PutRoleDefinitionToUserGroupOperation; import com.sap.sse.security.operations.RemoveAccessTokenOperation; import com.sap.sse.security.operations.RemovePermissionForUserOperation; import com.sap.sse.security.operations.RemoveRoleDefinitionFromUserGroupOperation; import com.sap.sse.security.operations.RemoveRoleFromUserOperation; import com.sap.sse.security.operations.RemoveUserFromUserGroupOperation; import com.sap.sse.security.operations.ResetPasswordOperation; import com.sap.sse.security.operations.SecurityOperation; import com.sap.sse.security.operations.SetAccessTokenOperation; import com.sap.sse.security.operations.SetDefaultTenantForServerForUserOperation; import com.sap.sse.security.operations.SetEmptyAccessControlListOperation; import com.sap.sse.security.operations.SetOwnershipOperation; import com.sap.sse.security.operations.SetPreferenceOperation; import com.sap.sse.security.operations.SetSettingOperation; import com.sap.sse.security.operations.UnsetPreferenceOperation; import com.sap.sse.security.operations.UpdateItemPriceOperation; import com.sap.sse.security.operations.UpdateRoleDefinitionOperation; import com.sap.sse.security.operations.UpdateSimpleUserEmailOperation; import com.sap.sse.security.operations.UpdateSimpleUserPasswordOperation; import com.sap.sse.security.operations.UpdateUserPropertiesOperation; import com.sap.sse.security.operations.UpdateUserSubscriptionOperation; import com.sap.sse.security.operations.ValidateEmailOperation; import com.sap.sse.security.persistence.PersistenceFactory; import com.sap.sse.security.shared.AccessControlListAnnotation; import com.sap.sse.security.shared.Account; import com.sap.sse.security.shared.Account.AccountType; import com.sap.sse.security.shared.AdminRole; import com.sap.sse.security.shared.HasPermissions; import com.sap.sse.security.shared.HasPermissions.DefaultActions; import com.sap.sse.security.shared.HasPermissionsProvider; import com.sap.sse.security.shared.OwnershipAnnotation; import com.sap.sse.security.shared.PermissionChecker; import com.sap.sse.security.shared.PermissionChecker.AclResolver; import com.sap.sse.security.shared.PredefinedRoles; import com.sap.sse.security.shared.QualifiedObjectIdentifier; import com.sap.sse.security.shared.RoleDefinition; import com.sap.sse.security.shared.RolePrototype; import com.sap.sse.security.shared.SecurityAccessControlList; import com.sap.sse.security.shared.SubscriptionPlanProvider; import com.sap.sse.security.shared.TypeRelativeObjectIdentifier; import com.sap.sse.security.shared.UserGroupManagementException; import com.sap.sse.security.shared.UserManagementException; import com.sap.sse.security.shared.UserRole; import com.sap.sse.security.shared.UsernamePasswordAccount; import com.sap.sse.security.shared.WildcardPermission; import com.sap.sse.security.shared.WithQualifiedObjectIdentifier; import com.sap.sse.security.shared.impl.AccessControlList; import com.sap.sse.security.shared.impl.LockingAndBanning; import com.sap.sse.security.shared.impl.LockingAndBanningImpl; import com.sap.sse.security.shared.impl.Ownership; import com.sap.sse.security.shared.impl.PermissionAndRoleAssociation; import com.sap.sse.security.shared.impl.Role; import com.sap.sse.security.shared.impl.SecuredSecurityTypes; import com.sap.sse.security.shared.impl.SecuredSecurityTypes.ServerActions; import com.sap.sse.security.shared.impl.User; import com.sap.sse.security.shared.impl.UserGroup; import com.sap.sse.security.shared.subscription.Subscription; import com.sap.sse.security.shared.subscription.SubscriptionPlan; import com.sap.sse.security.shared.subscription.SubscriptionPlanRole; import com.sap.sse.security.shared.subscription.SubscriptionPrice; import com.sap.sse.security.util.RemoteServerUtil; import com.sap.sse.shared.classloading.ClassLoaderRegistry; import com.sap.sse.shared.util.impl.ApproximateTime; import com.sap.sse.util.ClearStateTestSupport; import com.sap.sse.util.ThreadPoolUtil; public class SecurityServiceImpl extends AbstractReplicableWithObjectInputStream> implements ReplicableSecurityService, ClearStateTestSupport { private static final Logger logger = Logger.getLogger(SecurityServiceImpl.class.getName()); private static final String ADMIN_DEFAULT_PASSWORD = "admin"; private final Set migratedHasPermissionTypes = new ConcurrentSkipListSet<>(); private SecurityManager securityManager; private static final String STRING_MESSAGES_BASE_NAME = "stringmessages/StringMessages"; private static final ResourceBundleStringMessagesImpl messages = new ResourceBundleStringMessagesImpl( SecurityServiceImpl.STRING_MESSAGES_BASE_NAME, SecurityServiceImpl.class.getClassLoader(), StandardCharsets.UTF_8.name()); /** * A cache manager that the {@link SessionCacheManager} delegates to. This way, multiple Shiro configurations can * share the cache manager provided as a singleton within this bundle instance. The cache manager is replicating, * forwarding changes to the caches to all replicas registered. */ private final ReplicatingCacheManager cacheManager; /** * Keys are the replica set's server names. Values are {@link Pair}s whose {@link Pair#getA() first} component is a * boolean telling whether the CORS filter for the replica set identified by the key uses the "wildcard" (*) to * allow REST requests from all possible origins, and the {@link Pair#getB() second} component lists the allowed * origins in case it's not a wildcard configuration. For wildcard configurations, the second component is ignored. */ private final ConcurrentMap>> corsFilterConfigurationsByReplicaSetName; private final UserStore store; private final AccessControlStore accessControlStore; private boolean isInitialOrMigration; private boolean isNewServer; private final ServiceTracker mailServiceTracker; private final ServiceTracker corsFilterConfigurationTracker; private ThreadLocal temporaryDefaultTenant = new InheritableThreadLocal<>(); private final static Ini shiroConfiguration; private final static Environment shiroEnvironment; private final HasPermissionsProvider hasPermissionsProvider; private final SubscriptionPlanProvider subscriptionPlanProvider; private String sharedAcrossSubdomainsOf; private String baseUrlForCrossDomainStorage; private final transient Set customizers = ConcurrentHashMap.newKeySet(); private final AclResolver aclResolver; private final PermissionChangeListeners permissionChangeListeners; private final ClassLoaderRegistry initialLoadClassLoaderRegistry = ClassLoaderRegistry.createInstance(); /** * Contains locking objects keyed by client IP addresses that describe which client IPs are currently locked * from bearer token-based authentication due to previously failing requests. Requests for which a client * IP address could not be determined are keyed with the {@link #CLIENT_IP_NULL_ESCAPE} key.

* * When entering values into this map, the method entering it is responsible for also scheduling a background * task that a while after lock expiry the record is expunged again from the map to avoid garbage piling up. * * @see #failedBearerTokenAuthentication(String) * @see #successfulBearerTokenAuthentication(String) * @see #isClientIPLockedForBearerTokenAuthentication(String) */ private final ConcurrentMap clientIPBasedLockingAndBanningForBearerTokenAuthentication; private final static String CLIENT_IP_NULL_ESCAPE = UUID.randomUUID().toString(); /** * Contains locking objects keyed by client IP addresses ({@code null} not allowed) that describe which client IPs * are locked for {@link User} creation, e.g., through the * {@link #createSimpleUser(String, String, String, String, String, Locale, String, UserGroup, String, boolean) * createSimpleUser} method.

* * When entering values into this map, the method entering it is responsible for also scheduling a background * task that a while after lock expiry the record is expunged again from the map to avoid garbage piling up. */ private final ConcurrentMap clientIPBasedLockingAndBanningForUserCreation; private final Zxcvbn passwordValidator; /** * When working with a user's subscriptions, such as first reading, then changing and updating a user's subscription * based on what was read, a user-specific write lock must be obtained to ensure that no writes can cut in between. * See also {@link #lockSubscriptionsForUser} and {@link #unlockSubscriptionsForUser}. */ private final static ConcurrentMap subscriptionLocksForUsers; static { shiroConfiguration = new Ini(); shiroConfiguration.loadFromPath("classpath:shiro.ini"); shiroEnvironment = new BasicIniEnvironment("classpath:shiro.ini"); subscriptionLocksForUsers = new ConcurrentHashMap<>(); } /** * Creates a security service that is not shared across subdomains, therefore leading to the use of the full * domain through which its services are requested for {@code Document.domain} and hence for the browser local * storage, session storage and the Shiro {@code JSESSIONID} cookie's domain. * @param setAsActivatorSecurityService * when true, the {@link Activator#setSecurityService(com.sap.sse.security.SecurityService)} * will be called with this new instance as argument so that the cache manager can already be accessed * when the security manager is created. {@link ReplicatingCacheManager#getCache(String)} fetches the * activator's security service and passes it to the cache entries created. They need it, in turn, for * replication. */ public SecurityServiceImpl(ServiceTracker mailServiceTracker, ServiceTracker corsFilterConfigurationTracker, UserStore userStore, AccessControlStore accessControlStore, HasPermissionsProvider hasPermissionsProvider, SubscriptionPlanProvider subscriptionPlanProvider) { this(mailServiceTracker, corsFilterConfigurationTracker, userStore, accessControlStore, hasPermissionsProvider, subscriptionPlanProvider, /* sharedAcrossSubdomainsOf */ null, /* baseUrlForCrossDomainStorage */ null); } /** * Like {@link #SecurityServiceImpl(ServiceTracker, UserStore, AccessControlStore, HasPermissionsProvider)}, only that additionally * a parent domain can be specified in {@code isSharedAcrossSubdomains}, such as, e.g., {@code "sapsailing.com"}, across which * the browser local and session store shall be shared and for which sessions identified by the {@code JSESSIONID} cookie shall * be shared as well. */ public SecurityServiceImpl(ServiceTracker mailServiceTracker, ServiceTracker corsFilterConfigurationTracker, UserStore userStore, AccessControlStore accessControlStore, HasPermissionsProvider hasPermissionsProvider, SubscriptionPlanProvider subscriptionPlanProvider, String sharedAcrossSubdomainsOf, String baseUrlForCrossDomainStorage) { initialLoadClassLoaderRegistry.addClassLoader(getClass().getClassLoader()); if (hasPermissionsProvider == null) { throw new IllegalArgumentException("No HasPermissionsProvider defined"); } logger.info("Initializing Security Service with user store " + userStore); this.clientIPBasedLockingAndBanningForBearerTokenAuthentication = new ConcurrentHashMap<>(); this.clientIPBasedLockingAndBanningForUserCreation = new ConcurrentHashMap<>(); this.permissionChangeListeners = new PermissionChangeListeners(this); this.sharedAcrossSubdomainsOf = sharedAcrossSubdomainsOf; this.subscriptionPlanProvider = subscriptionPlanProvider; this.baseUrlForCrossDomainStorage = baseUrlForCrossDomainStorage; this.store = userStore; this.accessControlStore = accessControlStore; this.mailServiceTracker = mailServiceTracker; this.corsFilterConfigurationTracker = corsFilterConfigurationTracker; this.hasPermissionsProvider = hasPermissionsProvider; this.cacheManager = loadReplicationCacheManagerContents(); this.corsFilterConfigurationsByReplicaSetName = loadCORSFilterConfigurations(); logger.info("Loaded shiro.ini file from: classpath:shiro.ini"); final StringBuilder logMessage = new StringBuilder("[urls] section from Shiro configuration:"); final Section urlsSection = shiroConfiguration.getSection("urls"); if (urlsSection != null) { for (Entry e : urlsSection.entrySet()) { logMessage.append("\n"); logMessage.append(e.getKey()); logMessage.append(": "); logMessage.append(e.getValue()); } } logger.info(logMessage.toString()); System.setProperty("java.net.useSystemProxies", "true"); final SecurityManager securityManager = shiroEnvironment.getSecurityManager(); logger.info("Created: " + securityManager); SecurityUtils.setSecurityManager(securityManager); this.securityManager = securityManager; aclResolver = new SecurityServiceAclResolver(accessControlStore); try { passwordValidator = createPasswordValidator(); } catch (IOException e) { throw new RuntimeException(e); } } private Zxcvbn createPasswordValidator() throws IOException { final ZxcvbnBuilder builder = new ZxcvbnBuilder(); builder.dictionaries(StandardDictionaries.loadAllDictionaries()); builder.keyboards(StandardKeyboards.loadAllKeyboards()); return builder .keyboard(new SlantedKeyboardLoader("qwertz", new ClasspathResource("qwertz-keyboard.txt")).load()) .build(); } @Override public ClassLoader getDeserializationClassLoader() { return initialLoadClassLoaderRegistry.getCombinedMasterDataClassLoader(); } @Override public ClassLoaderRegistry getInitialLoadClassLoaderRegistry() { return initialLoadClassLoaderRegistry; } private ReplicatingCacheManager loadReplicationCacheManagerContents() { logger.info("Loading session cache manager contents"); int count = 0; final ReplicatingCacheManager result = new ReplicatingCacheManager(); for (Entry> cacheNameAndSessions : PersistenceFactory.INSTANCE.getDefaultDomainObjectFactory().loadSessionsByCacheName().entrySet()) { final String cacheName = cacheNameAndSessions.getKey(); final ReplicatingCache cache = (ReplicatingCache) result.getCache(cacheName, this); for (final Session session : cacheNameAndSessions.getValue()) { cache.put(session.getId(), session, /* store */ false); count++; } } logger.info("Loaded "+count+" sessions"); return result; } private ConcurrentMap>> loadCORSFilterConfigurations() { logger.info("Loading CORS filter configurations"); final ConcurrentMap>> result = new ConcurrentHashMap<>(); result.putAll(PersistenceFactory.INSTANCE.getDefaultDomainObjectFactory().loadCORSFilterConfigurationsForReplicaSetNames()); if (result.containsKey(ServerInfo.getName())) { final Pair> thisServersCORSFilterConfig = result.get(ServerInfo.getName()); if (thisServersCORSFilterConfig.getA()) { getCORSFilterConfiguration().setWildcard(); } else { getCORSFilterConfiguration().setOrigins(thisServersCORSFilterConfig.getB()); } } return result; } @Override public Iterable getAllHasPermissions() { return hasPermissionsProvider.getAllHasPermissions(); } @Override public Map getAllSubscriptionPlans() { return subscriptionPlanProvider.getAllSubscriptionPlans(); } @Override public SubscriptionPlan getSubscriptionPlanById(String planId) { return subscriptionPlanProvider.getAllSubscriptionPlans().get(planId); } @Override public SubscriptionPlan getSubscriptionPlanByItemPriceId(String itemPriceId) { SubscriptionPlan result = null; final Map allSubscriptionPlans = getAllSubscriptionPlans(); for (SubscriptionPlan plan : allSubscriptionPlans.values()) { for (SubscriptionPrice price : plan.getPrices()) { if (itemPriceId.equals(price.getPriceId())) { result = plan; } } } return result; } @Override public void initialize() { initEmptyStore(); initEmptyAccessControlStore(); } /** * Creates a default "admin" user with initial password "admin" and initial role "admin" if the user store * is empty. */ private void initEmptyStore() { final AdminRole adminRolePrototype = AdminRole.getInstance(); RoleDefinition adminRoleDefinition = getRoleDefinition(adminRolePrototype.getId()); assert adminRoleDefinition != null; try { isInitialOrMigration = false; if (!store.hasUsers()) { isInitialOrMigration = true; logger.info("No users found, creating default user \""+UserStore.ADMIN_USERNAME+"\" with password \""+ADMIN_DEFAULT_PASSWORD+"\""); final User adminUser = createSimpleUser(UserStore.ADMIN_USERNAME, "nobody@sapsailing.com", ADMIN_DEFAULT_PASSWORD, /* fullName */ null, /* company */ null, Locale.ENGLISH, /* validationBaseURL */ null, null, /* clientIP */ null, /* enforce strong password */ false); setOwnership(adminUser.getIdentifier(), adminUser, null); Role adminRole = new Role(adminRoleDefinition, /* transitive */ true); addRoleForUserAndSetUserAsOwner(adminUser, adminRole); // add new admin user to server group and make server group the default creation group for the admin user: final UserGroup defaultTenant = getServerGroup(); addUserToUserGroup(defaultTenant, adminUser); setDefaultTenantForCurrentServerForUser(adminUser.getName(), defaultTenant.getId()); } if (store.getUserByName(SecurityService.ALL_USERNAME) == null) { isInitialOrMigration = true; isNewServer = true; logger.info(SecurityService.ALL_USERNAME + " not found -> creating it now"); User allUser = apply(new CreateUserOperation(SecurityService.ALL_USERNAME, null)); // user is explicitly not owned by itself because this would enable anybody to modify this user setOwnership(allUser.getIdentifier(), null, getServerGroup()); // The permission to create new users is initially added but not recreated on server start if the admin removed it in the meanwhile. // This allows servers to be configured to not permit self-registration of new users but only users being managed by an admin user. WildcardPermission createUserPermission = SecuredSecurityTypes.USER.getPermission(DefaultActions.CREATE); addPermissionForUser(ALL_USERNAME, createUserPermission); QualifiedObjectIdentifier qualifiedTypeIdentifierForPermission = SecuredSecurityTypes.PERMISSION_ASSOCIATION .getQualifiedObjectIdentifier(PermissionAndRoleAssociation.get(createUserPermission, allUser)); // Permission association is owned by the server tenant. // This typically ensures that the server admin is able to remove the association. setOwnership(qualifiedTypeIdentifierForPermission, null, getServerGroup()); } if (isInitialOrMigration) { // predefined roles are meant to be publicly readable for (UUID predefinedRoleId : getPredefinedRoleIds()) { final RoleDefinition predefinedRole = getRoleDefinition(predefinedRoleId); if (predefinedRole != null) { addToAccessControlList(predefinedRole.getIdentifier(), null, DefaultActions.READ.name()); } } } } catch (UserManagementException | MailException | UserGroupManagementException e) { logger.log(Level.SEVERE, "Exception while creating default " + UserStore.ADMIN_USERNAME + " and " + SecurityService.ALL_USERNAME + " user", e); } } private Iterable getPredefinedRoleIds() { final Set predefinedRoleIds = new HashSet<>(); predefinedRoleIds.add(AdminRole.getInstance().getId()); predefinedRoleIds.add(UserRole.getInstance().getId()); for (final PredefinedRoles otherPredefinedRole : PredefinedRoles.values()) { predefinedRoleIds.add(otherPredefinedRole.getId()); } return predefinedRoleIds; } private void initEmptyAccessControlStore() { } private MailService getMailService() { return mailServiceTracker == null ? null : mailServiceTracker.getService(); } private CORSFilterConfiguration getCORSFilterConfiguration() { return corsFilterConfigurationTracker == null ? null : corsFilterConfigurationTracker.getService(); } @Override public void sendMail(String username, String subject, String body) throws MailException { final User user = getUserByName(username); if (user != null) { final String toAddress = user.getEmail(); if (toAddress != null) { MailService mailService = getMailService(); if (mailService == null) { logger.warning(String.format("Could not send mail to user %s: no MailService found", username)); } else { getMailService().sendMail(toAddress, subject, body); } } } } @Override public void resetPassword(final String username, String passwordResetBaseURL) throws UserManagementException, MailException { final User user = store.getUserByName(username); if (user == null) { throw new UserManagementException(UserManagementException.USER_DOES_NOT_EXIST); } if (!user.isEmailValidated()) { throw new UserManagementException(UserManagementException.CANNOT_RESET_PASSWORD_WITHOUT_VALIDATED_EMAIL); } final String passwordResetSecret = user.createRandomSecret(); logger.info("Subject "+SecurityUtils.getSubject().getPrincipal()+" is starting password reset for user "+username); apply(new ResetPasswordOperation(username, passwordResetSecret)); Map urlParameters = new HashMap<>(); try { urlParameters.put("u", URLEncoder.encode(user.getName(), "UTF-8")); urlParameters.put("e", URLEncoder.encode(user.getEmail(), "UTF-8")); urlParameters.put("s", URLEncoder.encode(passwordResetSecret, "UTF-8")); final StringBuilder url = buildURL(passwordResetBaseURL, urlParameters); new Thread("sending password reset e-mail to user " + username) { @Override public void run() { try { sendMail(user.getName(), "Password Reset", "Please click on the link below to reset your password for user " + user.getName() + ".\n " + url.toString()); } catch (MailException e) { logger.log(Level.SEVERE, "Error sending mail for password reset of user " + user.getName() + " to address " + user.getEmail(), e); } } }.start(); } catch (UnsupportedEncodingException e) { logger.log(Level.SEVERE, "Internal error: encoding UTF-8 not found. Couldn't send e-mail to user " + user.getName() + " at e-mail address " + user.getEmail(), e); } } @Override public Void internalResetPassword(String username, String passwordResetSecret) { getUserByName(username).startPasswordReset(passwordResetSecret); return null; } @Override public SecurityManager getSecurityManager() { return this.securityManager; } @Override public OwnershipAnnotation getOwnership(QualifiedObjectIdentifier idOfOwnedObjectAsString) { return accessControlStore.getOwnership(idOfOwnedObjectAsString); } private UserGroup getDefaultTenantForUser(User user) { UserGroup specificTenant = temporaryDefaultTenant.get(); if (specificTenant == null) { specificTenant = user.getDefaultTenant(ServerInfo.getName()); if (specificTenant == null) { String defaultTenantName = getDefaultTenantNameForUsername(user.getName()); specificTenant = getUserGroupByName(defaultTenantName); } } return specificTenant; } @Override public UserGroup getDefaultTenantForCurrentUser() { if (SecurityUtils.getSecurityManager() != null && getCurrentUser() == null) { return null; } return getDefaultTenantForUser(getCurrentUser()); } @Override public void setTemporaryDefaultTenant(final UUID tenantGroupId) { if (tenantGroupId != null || getCurrentUser() == null) { final UserGroup tenantGroup = getUserGroup(tenantGroupId); if (tenantGroup == null) { temporaryDefaultTenant.remove(); } else { if (Util.contains(getUserGroupsOfUser(getCurrentUser()), tenantGroup)) { temporaryDefaultTenant.set(tenantGroup); } else { logger.warning("User " + getCurrentUser().getName() + " tried to set foreign temporary default tenant group " + tenantGroupId.toString()); } } } else { temporaryDefaultTenant.remove(); } } @Override public RoleDefinition getRoleDefinition(UUID idOfRoleDefinition) { return store.getRoleDefinition(idOfRoleDefinition); } /** * Returns a list of all existing access control lists. This is possibly not complete in the sense * that there is a access control list for every access controlled data object. */ @Override public Iterable getAccessControlLists() { return accessControlStore.getAccessControlLists(); } @Override public AccessControlListAnnotation getAccessControlList(QualifiedObjectIdentifier idOfAccessControlledObjectAsString) { return accessControlStore.getAccessControlList(idOfAccessControlledObjectAsString); } public AccessControlListAnnotation getOrCreateAccessControlList(QualifiedObjectIdentifier idOfAccessControlledObjectAsString) { return accessControlStore.getOrCreateAcl(idOfAccessControlledObjectAsString); } /** * @param id Has to be globally unique */ private SecurityService setEmptyAccessControlList(QualifiedObjectIdentifier idOfAccessControlledObjectAsString) { return setEmptyAccessControlList(idOfAccessControlledObjectAsString, /* display name of access-controlled object */ null); } /** * @param id Has to be globally unique */ private SecurityService setEmptyAccessControlList(QualifiedObjectIdentifier idOfAccessControlledObjectAsString, String displayNameOfAccessControlledObject) { logger.info("Subject "+SecurityUtils.getSubject().getPrincipal()+" is clearing ACL of object with ID "+idOfAccessControlledObjectAsString); apply(new SetEmptyAccessControlListOperation(idOfAccessControlledObjectAsString, displayNameOfAccessControlledObject)); return this; } @Override public Void internalSetEmptyAccessControlList(QualifiedObjectIdentifier idOfAccessControlledObject, String displayNameOfAccessControlledObject) { permissionChangeListeners.aclChanged(idOfAccessControlledObject); accessControlStore.setEmptyAccessControlList(idOfAccessControlledObject, displayNameOfAccessControlledObject); return null; } @Override public AccessControlList overrideAccessControlList(QualifiedObjectIdentifier idOfAccessControlledObject, Map> permissionMap) { return overrideAccessControlList(idOfAccessControlledObject, permissionMap, /* displayNameOfAccessControlledObject */ null); } @Override public AccessControlList overrideAccessControlList(QualifiedObjectIdentifier idOfAccessControlledObject, Map> permissionMap, String displayNameOfAccessControlledObject) { setEmptyAccessControlList(idOfAccessControlledObject, displayNameOfAccessControlledObject); for (Map.Entry> entry : permissionMap.entrySet()) { final UserGroup userGroup = entry.getKey(); final Set actionsToSet; if (userGroup == null) { // filter any denied action for anonymous user actionsToSet = entry.getValue().stream() .filter(action -> !SecurityAccessControlList.isDeniedAction(action)) .collect(Collectors.toSet()); } else { actionsToSet = entry.getValue(); } final UUID userGroupId = userGroup == null ? null : userGroup.getId(); // avoid the UserGroup object having to be serialized with the operation by using the ID logger.info("Subject "+SecurityUtils.getSubject().getPrincipal()+" is setting the ACL for "+idOfAccessControlledObject+" with actions "+actionsToSet); apply(new AclPutPermissionsOperation(idOfAccessControlledObject, userGroupId, actionsToSet)); } return accessControlStore.getAccessControlList(idOfAccessControlledObject).getAnnotation(); } @Override public Void internalAclPutPermissions(QualifiedObjectIdentifier idOfAccessControlledObject, UUID groupId, Set actions) { permissionChangeListeners.aclChanged(idOfAccessControlledObject); accessControlStore.setAclPermissions(idOfAccessControlledObject, getUserGroup(groupId), actions); return null; } /* * @param name The name of the user group to add */ @Override public AccessControlList addToAccessControlList(QualifiedObjectIdentifier idOfAccessControlledObject, UserGroup group, String action) { if (getAccessControlList(idOfAccessControlledObject) == null) { setEmptyAccessControlList(idOfAccessControlledObject); } final UUID groupId = group == null ? null : group.getId(); logger.info("Subject "+SecurityUtils.getSubject().getPrincipal()+" is adding permission "+action+" for group with ID "+groupId+" to the ACL for "+idOfAccessControlledObject); apply(new AclAddPermissionOperation(idOfAccessControlledObject, groupId, action)); return accessControlStore.getAccessControlList(idOfAccessControlledObject).getAnnotation(); } @Override public Void internalAclAddPermission(QualifiedObjectIdentifier idOfAccessControlledObject, UUID groupId, String action) { permissionChangeListeners.aclChanged(idOfAccessControlledObject); accessControlStore.addAclPermission(idOfAccessControlledObject, getUserGroup(groupId), action); return null; } /* * @param name The name of the user group to remove */ @Override public AccessControlList removeFromAccessControlList(QualifiedObjectIdentifier idOfAccessControlledObject, UserGroup group, String permission) { final AccessControlList result; if (getAccessControlList(idOfAccessControlledObject) != null) { final UUID groupId = group == null ? null : group.getId(); logger.info("Subject "+SecurityUtils.getSubject().getPrincipal()+" is removing permission "+permission+" for group with ID "+groupId+" from the ACL for "+idOfAccessControlledObject); apply(new AclRemovePermissionOperation(idOfAccessControlledObject, groupId, permission)); result = accessControlStore.getAccessControlList(idOfAccessControlledObject).getAnnotation(); } else { result = null; } return result; } @Override public Void internalAclRemovePermission(QualifiedObjectIdentifier idOfAccessControlledObject, UUID groupId, String permission) { permissionChangeListeners.aclChanged(idOfAccessControlledObject); accessControlStore.removeAclPermission(idOfAccessControlledObject, getUserGroup(groupId), permission); return null; } @Override public void deleteAccessControlList(QualifiedObjectIdentifier idOfAccessControlledObject) { if (getAccessControlList(idOfAccessControlledObject) != null) { logger.info("Subject "+SecurityUtils.getSubject().getPrincipal()+" is deleting the ACL of object "+idOfAccessControlledObject); apply(new DeleteAclOperation(idOfAccessControlledObject)); } } @Override public Void internalDeleteAcl(QualifiedObjectIdentifier idOfAccessControlledObject) { permissionChangeListeners.aclChanged(idOfAccessControlledObject); accessControlStore.removeAccessControlList(idOfAccessControlledObject); return null; } @Override public Ownership setOwnership(QualifiedObjectIdentifier objectId, User userOwner, UserGroup tenantOwner) { return setOwnership(objectId, userOwner, tenantOwner, /* displayNameOfOwnedObject */ null); } @Override public Ownership setOwnership(QualifiedObjectIdentifier objectId, User userOwner, UserGroup tenantOwner, String displayNameOfOwnedObject) { if (userOwner == null && tenantOwner == null) { throw new IllegalArgumentException("No owner is not valid, would create non changeable object"); } final UUID tenantId = tenantOwner == null ? null : tenantOwner.getId(); final String userOwnerName = userOwner == null ? null : userOwner.getName(); final OwnershipAnnotation existingOwnership = getOwnership(objectId); final User existingUserOwner; final UserGroup existingTenantOwner; final String existingDisplayNameOfOwnedObject; final Ownership result; if (existingOwnership == null || existingOwnership.getAnnotation() == null) { existingUserOwner = null; existingTenantOwner = null; } else { existingUserOwner = existingOwnership.getAnnotation().getUserOwner(); existingTenantOwner = existingOwnership.getAnnotation().getTenantOwner(); } if (existingOwnership == null) { existingDisplayNameOfOwnedObject = null; } else { existingDisplayNameOfOwnedObject = existingOwnership.getDisplayNameOfAnnotatedObject(); } if (Util.equalsWithNull(existingDisplayNameOfOwnedObject, displayNameOfOwnedObject) && existingUserOwner == userOwner && existingTenantOwner == tenantOwner) { result = existingOwnership.getAnnotation(); } else { logger.info("Subject "+SecurityUtils.getSubject().getPrincipal()+" is setting ownership of object "+objectId+" to group with ID "+tenantId+" and user "+userOwnerName); result = apply(new SetOwnershipOperation(objectId, userOwnerName, tenantId, displayNameOfOwnedObject)); } return result; } @Override public Ownership internalSetOwnership(QualifiedObjectIdentifier objectId, String userOwnerName, UUID tenantOwnerId, String displayName) { final Ownership result = accessControlStore.setOwnership(objectId, getUserByName(userOwnerName), getUserGroup(tenantOwnerId), displayName).getAnnotation(); permissionChangeListeners.ownershipChanged(objectId); return result; } @Override public void deleteOwnership(QualifiedObjectIdentifier objectId) { if (getOwnership(objectId) != null) { logger.info("Subject "+SecurityUtils.getSubject().getPrincipal()+" is deleting ownership information from object with ID "+objectId); apply(new DeleteOwnershipOperation(objectId)); } } @Override public Void internalDeleteOwnership(QualifiedObjectIdentifier objectId) { accessControlStore.removeOwnership(objectId); permissionChangeListeners.ownershipChanged(objectId); return null; } @Override public Iterable getUserGroupList() { return store.getUserGroups(); } @Override public Iterable getUserGroupsWithRoleDefinition(RoleDefinition roleDefinition) { return store.getUserGroupsWithRoleDefinition(roleDefinition); } @Override public UserGroup getUserGroup(UUID id) { return store.getUserGroup(id); } @Override public UserGroup getUserGroupByName(String name) { return store.getUserGroupByName(name); } @Override public Iterable getUserGroupsOfUser(User user) { return store.getUserGroupsOfUser(user); } @Override public UserGroup createUserGroup(UUID id, String name) throws UserGroupManagementException { return createUserGroupWithInitialUser(id, name, getCurrentUser()); } private UserGroup createUserGroupWithInitialUser(UUID id, String name, User initialUser) { logger.info("Subject "+SecurityUtils.getSubject().getPrincipal()+" is creating user group " + name + " with ID " + id); apply(new CreateUserGroupOperation(id, name)); final UserGroup userGroup = store.getUserGroup(id); if (initialUser != null) { logger.info("Adding initial user " + initialUser + " to group " + userGroup); addUserToUserGroup(userGroup, initialUser); addUserRoleForGroupToUser(userGroup, initialUser); } return userGroup; } @Override public Void internalCreateUserGroup(UUID id, String name) throws UserGroupManagementException { store.createUserGroup(id, name); return null; } @Override public void addUserToUserGroup(UserGroup userGroup, User user) { logger.info("Adding user "+user.getName()+" to group "+userGroup.getName()); userGroup.add(user); final UUID groupId = userGroup.getId(); final String username = user.getName(); apply(new AddUserToUserGroupOperation(groupId, username)); } @Override public Void internalAddUserToUserGroup(UUID groupId, String username) { final UserGroup userGroup = getUserGroup(groupId); final User user = getUserByName(username); userGroup.add(user); permissionChangeListeners.userAddedToOrRemovedFromGroup(user, userGroup); store.updateUserGroup(userGroup); return null; } @Override public Void internalRemoveUserFromUserGroup(UUID groupId, String username) { final UserGroup userGroup = getUserGroup(groupId); final User user = getUserByName(username); permissionChangeListeners.userAddedToOrRemovedFromGroup(user, userGroup); userGroup.remove(user); store.updateUserGroup(userGroup); return null; } @Override public void removeUserFromUserGroup(UserGroup userGroup, User user) { logger.info("Removing user "+user.getName()+" from group "+userGroup.getName()); userGroup.remove(user); final UUID userGroupId = userGroup.getId(); final String username = user.getName(); apply(new RemoveUserFromUserGroupOperation(userGroupId, username)); } @Override public void putRoleDefinitionToUserGroup(UserGroup userGroup, RoleDefinition roleDefinition, boolean forAll) { logger.info("Adding role definition " + roleDefinition.getName() + "(forAll = " + forAll + ") to group " + userGroup.getName()); apply(new PutRoleDefinitionToUserGroupOperation(userGroup.getId(), roleDefinition.getId(), forAll)); } @Override public Void internalPutRoleDefinitionToUserGroup(UUID groupId, UUID roleDefinitionId, boolean forAll) throws UserGroupManagementException { final UserGroup userGroup = getUserGroup(groupId); final RoleDefinition roleDefinition = getRoleDefinition(roleDefinitionId); permissionChangeListeners.roleAddedToOrRemovedFromGroup(userGroup, roleDefinition); userGroup.put(roleDefinition, forAll); store.updateUserGroup(userGroup); return null; } @Override public void removeRoleDefintionFromUserGroup(UserGroup userGroup, RoleDefinition roleDefinition) { logger.info("Removing role definition " + roleDefinition.getName() + " from group " + userGroup.getName()); apply(new RemoveRoleDefinitionFromUserGroupOperation(userGroup.getId(), roleDefinition.getId())); } @Override public Void internalRemoveRoleDefinitionFromUserGroup(UUID groupId, UUID roleDefinitionId) throws UserGroupManagementException { final UserGroup userGroup = getUserGroup(groupId); final RoleDefinition roleDefinition = getRoleDefinition(roleDefinitionId); permissionChangeListeners.roleAddedToOrRemovedFromGroup(userGroup, roleDefinition); userGroup.remove(roleDefinition); store.updateUserGroup(userGroup); return null; } @Override public void deleteUserGroup(UserGroup userGroup) throws UserGroupManagementException { logger.info("Removing user group "+userGroup.getName()); apply(new DeleteUserGroupOperation(userGroup.getId())); } @Override public Void internalDeleteUserGroup(UUID groupId) throws UserGroupManagementException { final UserGroup userGroup = getUserGroup(groupId); if (userGroup == null) { logger.warning("Strange: the user group with ID "+groupId+" which is about to be deleted couldn't be found"); } else { final Iterable ownerhipsWithGroupOwner = accessControlStore.getOwnerhipsWithGroupOwner(userGroup); accessControlStore.removeAllOwnershipsFor(userGroup); store.deleteUserGroup(userGroup); for (final OwnershipAnnotation ownershipWithGroupAsOwner : ownerhipsWithGroupOwner) { permissionChangeListeners.ownershipChanged(ownershipWithGroupAsOwner.getIdOfAnnotatedObject()); } } return null; } @Override public Iterable getUserList() { return store.getUsers(); } @Override public String login(String username, String password) throws AuthenticationException { String redirectUrl; UsernamePasswordToken token = new UsernamePasswordToken(username, password); logger.info("Trying to login: " + username); Subject subject = SecurityUtils.getSubject(); subject.login(token); HttpServletRequest httpRequest = WebUtils.getHttpRequest(subject); SavedRequest savedRequest = WebUtils.getSavedRequest(httpRequest); if (savedRequest != null) { redirectUrl = savedRequest.getRequestUrl(); } else { redirectUrl = ""; } logger.info("Redirecturl: " + redirectUrl); return redirectUrl; } @Override public void logout() { Subject subject = SecurityUtils.getSubject(); logger.info("Logging out"); subject.logout(); } @Override public User getUserByName(String name) { return store.getUserByName(name); } @Override public User getUserByAccessToken(String accessToken) { return store.getUserByAccessToken(accessToken); } @Override public User getUserByEmail(String email) { return store.getUserByEmail(email); } @Override public Iterable getUsersWithPermissions(WildcardPermission permission) { if (Util.size(permission.getQualifiedObjectIdentifiers()) != 1) { throw new IllegalArgumentException("Permission needs to specify exactly one object identifier"); } final ScheduledExecutorService foregroundExecutor = ThreadPoolUtil.INSTANCE.getDefaultForegroundTaskThreadPoolExecutor(); final int numberOfJobs = ThreadPoolUtil.INSTANCE.getReasonableThreadPoolSize(); final ConcurrentMap result = new ConcurrentHashMap<>(); final User allUser = getAllUser(); final ConcurrentLinkedDeque userList = new ConcurrentLinkedDeque<>(); Util.addAll(getUserList(), userList); final Set> futures = new HashSet<>(); final QualifiedObjectIdentifier objectIdentifier = permission.getQualifiedObjectIdentifiers().iterator().next(); final OwnershipAnnotation ownership = accessControlStore.getOwnership(objectIdentifier); final AccessControlListAnnotation acl = accessControlStore.getAccessControlList(objectIdentifier); // create as many jobs as we expect the thread pool size to be and have each of them // keep polling User objects and check whether that user has the permission sought; if so, // add the User to the result. // result and userList are thread-safe data structures for (int i=0; i{ User user; int usersHandled = 0; while ((user=userList.poll()) != null) { usersHandled++; if (PermissionChecker.isPermitted(permission, user, allUser, ownership == null ? null : ownership.getAnnotation(), acl == null ? null : acl.getAnnotation())) { result.put(user, true); } } final int finalUsersHandled = usersHandled; logger.fine(()->"Handled "+finalUsersHandled+" users in job "+this); })); } for (final Future future : futures) { try { future.get(); } catch (InterruptedException | ExecutionException e) { logger.log(Level.WARNING, "Exception while trying to wait for user permission check", e); } } return result.keySet(); } @Override public User createSimpleUser(final String username, final String email, String password, String fullName, String company, Locale locale, final String validationBaseURL, UserGroup groupOwningUser, String requestClientIP, boolean enforceStrongPassword) throws UserManagementException, MailException, UserGroupManagementException { if (requestClientIP != null) { checkAndRecordUserCreationFromClientIP(requestClientIP); } logger.info("Creating user "+username); if (store.getUserByName(username) != null) { logger.warning("User "+username+" already exists"); throw new UserManagementException(UserManagementException.USER_ALREADY_EXISTS); } if (username == null || username.length() < 3) { throw new UserManagementException(UserManagementException.USERNAME_DOES_NOT_MEET_REQUIREMENTS); } else if (enforceStrongPassword && !isPasswordGoodEnough(password)) { throw new UserManagementException(UserManagementException.PASSWORD_DOES_NOT_MEET_REQUIREMENTS); } RandomNumberGenerator rng = new SecureRandomNumberGenerator(); byte[] salt = rng.nextBytes().getBytes(); String hashedPasswordBase64 = hashPassword(password, salt); UsernamePasswordAccount upa = new UsernamePasswordAccount(username, hashedPasswordBase64, salt); final User result = apply(new CreateUserOperation(username, email, upa)); // This also replicated the user creation addUserRoleToUser(result); final UserGroup tenant = getOrCreateTenantForUser(result); setDefaultTenantForCurrentServerForUser(username, tenant.getId()); // the new user becomes its owner to ensure the user role is working correctly // the default tenant is the owning tenant to allow users having admin role for a specific server tenant to also be able to delete users apply(new SetOwnershipOperation(result.getIdentifier(), username, groupOwningUser==null?null:groupOwningUser.getId(), username)); updateUserProperties(username, fullName, company, locale); // email has been set during creation already; the following call will trigger the e-mail validation process updateSimpleUserEmail(username, email, validationBaseURL); return result; } private boolean isPasswordGoodEnough(String password) { final boolean result; if (password == null) { result = false; } else { final Strength strength = passwordValidator.measure(password); result = strength.getGuessesLog10() > 8; } return result; } /** * Checks if the {@code clientIP} is currently blocked for user creation, and if so, throws a * {@link UserManagementException}. If the check is successful, it records a locking record for * this client IP, similar to the locking/banning that happens for failer bearer token * authentication requests (see {@link #failedBearerTokenAuthentication(String)}). */ private void checkAndRecordUserCreationFromClientIP(String clientIP) throws UserManagementException { assert clientIP != null; // synchronize to ensure that no two threads can enter values into the map concurrently; // still the use of a ConcurrentMap is justified because there may be concurrent write access // through replication synchronized (clientIPBasedLockingAndBanningForUserCreation) { final LockingAndBanning lockingAndBanning = clientIPBasedLockingAndBanningForUserCreation.get(clientIP); if (lockingAndBanning == null || !lockingAndBanning.isAuthenticationLocked()) { apply(s->s.internalRecordUserCreationFromClientIP(clientIP)); } else { throw new UserManagementException(UserManagementException.CLIENT_CURRENTLY_LOCKED_FOR_USER_CREATION); } } } @Override public LockingAndBanning internalRecordUserCreationFromClientIP(String clientIP) { final LockingAndBanning result = new LockingAndBanningImpl(TimePoint.now().plus(DEFAULT_CLIENT_IP_BASED_USER_CREATION_LOCKING_DURATION), DEFAULT_CLIENT_IP_BASED_USER_CREATION_LOCKING_DURATION); clientIPBasedLockingAndBanningForUserCreation.put(clientIP, result); scheduleCleanUpTask(clientIP, result, clientIPBasedLockingAndBanningForUserCreation, "client IPs locked for user creation"); return result; } private void addUserRoleToUser(final User user) { addRoleForUserAndSetUserAsOwner(user, new Role(store.getRoleDefinitionByPrototype(UserRole.getInstance()), /* tenant qualifier */ null, /* user qualifier */ user, /* transitive */ true)); } private void addUserRoleForGroupToUser(final UserGroup group, final User user) { addRoleForUserAndSetUserAsOwner(user, new Role(store.getRoleDefinitionByPrototype(UserRole.getInstance()), /* tenant qualifier */ group, /* user qualifier */ null, /* transitive */ true)); } /** * A simple {@code synchronized} block synchronizing on the {@link #userGroupLock} monitor is used * in the {@link #getOrCreateTenantForUser(User)} method to avoid race conditions between multiple * threads, such as the migration code run in a background thread by the {@link Activator}'s {@code migrate} * method, and the regular creation of a user which also probes for the user's default group. */ private final static Object userGroupLock = new Object(); private UserGroup getOrCreateTenantForUser(User user) throws UserGroupManagementException { final String username = user.getName(); final String defaultTenantNameForUsername = getDefaultTenantNameForUsername(username); synchronized (userGroupLock) { UserGroup tenant = store.getUserGroupByName(defaultTenantNameForUsername); if (tenant != null) { logger.info("Found existing tenant "+defaultTenantNameForUsername+" to be used as default tenant for new user "+username); } else { logger.info("Creating user group "+defaultTenantNameForUsername+" as default tenant for new user "+username); tenant = createUserGroupWithInitialUser(UUID.randomUUID(), defaultTenantNameForUsername, user); setOwnership(tenant.getIdentifier(), user, tenant); } return tenant; } } @Override public User internalCreateUser(String username, String email, Account... accounts) throws UserManagementException { final User result = store.createUser(username, email, new LockingAndBanningImpl(), accounts); return result; } private String getDefaultTenantNameForUsername(final String username) { return username + TENANT_SUFFIX; } @Override public void updateSimpleUserPassword(String username, String newPassword) throws UserManagementException { final User user = store.getUserByName(username); if (user == null) { throw new UserManagementException(UserManagementException.USER_DOES_NOT_EXIST); } updateSimpleUserPassword(user, newPassword); } private void updateSimpleUserPassword(final User user, String newPassword) throws UserManagementException { if (!isPasswordGoodEnough(newPassword)) { throw new UserManagementException(UserManagementException.PASSWORD_DOES_NOT_MEET_REQUIREMENTS); } // for non-admins, check that the old password is correct RandomNumberGenerator rng = new SecureRandomNumberGenerator(); byte[] salt = rng.nextBytes().getBytes(); String hashedPasswordBase64 = hashPassword(newPassword, salt); apply(new UpdateSimpleUserPasswordOperation(user.getName(), salt, hashedPasswordBase64)); } @Override public Void internalUpdateSimpleUserPassword(String username, byte[] salt, String hashedPasswordBase64) { final User user = getUserByName(username); final UsernamePasswordAccount account = (UsernamePasswordAccount) user.getAccount(AccountType.USERNAME_PASSWORD); account.setSalt(salt); account.setSaltedPassword(hashedPasswordBase64); user.passwordWasReset(); store.updateUser(user); return null; } @Override public void updateUserProperties(String username, String fullName, String company, Locale locale) throws UserManagementException { final User user = store.getUserByName(username); if (user == null) { throw new UserManagementException(UserManagementException.USER_DOES_NOT_EXIST); } apply(new UpdateUserPropertiesOperation(username, fullName, company, locale)); } @Override public Void internalUpdateUserProperties(String username, String fullName, String company, Locale locale) { final User user = store.getUserByName(username); user.setFullName(fullName); user.setCompany(company); user.setLocale(locale); store.updateUser(user); return null; } @Override public boolean checkPassword(String username, String password) throws UserManagementException { final User user = store.getUserByName(username); if (user == null) { throw new UserManagementException(UserManagementException.USER_DOES_NOT_EXIST); } if (user.getLockingAndBanning().isAuthenticationLocked()) { throw new UserManagementException(UserManagementException.PASSWORD_AUTHENTICATION_CURRENTLY_LOCKED_FOR_USER); } final UsernamePasswordAccount account = (UsernamePasswordAccount) user.getAccount(AccountType.USERNAME_PASSWORD); String hashedOldPassword = hashPassword(password, account.getSalt()); final boolean result = Util.equalsWithNull(hashedOldPassword, account.getSaltedPassword()); if (!result) { logger.info("Failed password check for user "+username); apply(s->s.internalFailedPasswordAuthentication(username)); } else { apply(s->s.internalSuccessfulPasswordAuthentication(username)); } return result; } @Override public LockingAndBanning failedPasswordAuthentication(User user) { return apply(s->s.internalFailedPasswordAuthentication(user.getName())); } @Override public LockingAndBanning internalFailedPasswordAuthentication(String username) { final User user = getUserByName(username); final LockingAndBanning lockingAndBanning; if (user != null) { lockingAndBanning = user.getLockingAndBanning(); lockingAndBanning.failedPasswordAuthentication(); store.updateUser(user); logger.info("failed password authentication for user "+username+"; locking: "+lockingAndBanning); } else { lockingAndBanning = null; } return lockingAndBanning; } @Override public void successfulPasswordAuthentication(User user) { // replicate only if this really implied a change if (internalSuccessfulPasswordAuthentication(user.getName())) { replicate(s->s.internalSuccessfulPasswordAuthentication(user.getName())); } } @Override public Boolean internalSuccessfulPasswordAuthentication(String username) { final boolean changed; final User user = getUserByName(username); if (user != null) { changed = user.getLockingAndBanning().successfulPasswordAuthentication(); if (changed) { store.updateUser(user); } } else { changed = false; } return changed; } @Override public LockingAndBanning failedBearerTokenAuthentication(String clientIP) { return apply(s->s.internalFailedBearerTokenAuthentication(clientIP)); } @Override public LockingAndBanning internalFailedBearerTokenAuthentication(String clientIP) { final LockingAndBanning lockingAndBanning = clientIPBasedLockingAndBanningForBearerTokenAuthentication.computeIfAbsent(escapeNullClientIP(clientIP), key->new LockingAndBanningImpl()); lockingAndBanning.failedPasswordAuthentication(); logger.info("failed bearer token authentication from client IP "+clientIP+"; locking: "+lockingAndBanning); scheduleCleanUpTask(clientIP, lockingAndBanning, clientIPBasedLockingAndBanningForBearerTokenAuthentication, "client IPs locked for bearer token authentication"); return lockingAndBanning; } /** * Schedule a clean-up task to avoid leaking memory for the LockingAndBanning objects; schedule it in two times the * locking expiry of {@code lockingAndBanning}, but at least one hour, because if no authentication failure occurs * for that IP/user agent combination, we will entirely remove the {@link LockingAndBanning} from the map, * effectively resetting that IP to a short default locking duration again; this way, if during the double * expiration time another failed attempt is registered, we can still grow the locking duration because we have kept * the {@link LockingAndBanning} object available for a bit longer. Furthermore, for authentication requests, the * responsible {@link Realm} will let authentication requests get to here only if not locked, so if we were to * expunge entries immediately as they unlock, the locking duration could never grow.

* * With the minimum of one hour, we ensure that failing requests done at a slower rate still grow the locking * expiry duration. */ private void scheduleCleanUpTask(final String clientIPOrNull, final LockingAndBanning lockingAndBanning, final ConcurrentMap mapToRemoveFrom, final String nameOfMapForLog) { final long millisUntilLockingExpiry = Math.max( 2*ApproximateTime.approximateNow().until(lockingAndBanning.getLockedUntil()).asMillis(), Duration.ONE_HOUR.asMillis()); ThreadPoolUtil.INSTANCE.getDefaultBackgroundTaskThreadPoolExecutor().schedule( ()->{ final LockingAndBanning lab = mapToRemoveFrom.get(escapeNullClientIP(clientIPOrNull)); if (lab != null && !lab.isAuthenticationLocked()) { mapToRemoveFrom.remove(escapeNullClientIP(clientIPOrNull)); logger.info("Removed "+clientIPOrNull+" from "+nameOfMapForLog+"; " +mapToRemoveFrom.size() +" locked client IP(s) remaining"); } }, millisUntilLockingExpiry, TimeUnit.MILLISECONDS); } private String escapeNullClientIP(String clientIP) { return clientIP==null?CLIENT_IP_NULL_ESCAPE:clientIP; } @Override public void successfulBearerTokenAuthentication(String clientIP) { // replicate only if this truly caused a change in locking/banning: if (internalSuccessfulBearerTokenAuthentication(clientIP)) { replicate(s->s.internalSuccessfulBearerTokenAuthentication(clientIP)); } } @Override public Boolean internalSuccessfulBearerTokenAuthentication(String clientIP) { final boolean changed; final LockingAndBanning lockingAndBanning = clientIPBasedLockingAndBanningForBearerTokenAuthentication.remove(escapeNullClientIP(clientIP)); if (lockingAndBanning != null) { logger.info("Unlocked bearer token authentication from "+clientIP+"; last locking state was "+lockingAndBanning); changed = true; } else { changed = false; } return changed; } @Override public boolean isClientIPLockedForBearerTokenAuthentication(String clientIP) { final LockingAndBanning lockingAndBanning = clientIPBasedLockingAndBanningForBearerTokenAuthentication.get(escapeNullClientIP(clientIP)); return lockingAndBanning != null && lockingAndBanning.isAuthenticationLocked(); } @Override public boolean checkPasswordResetSecret(String username, String passwordResetSecret) throws UserManagementException { final User user = store.getUserByName(username); if (user == null) { throw new UserManagementException(UserManagementException.USER_DOES_NOT_EXIST); } return Util.equalsWithNull(user.getPasswordResetSecret(), passwordResetSecret); } @Override public void updateSimpleUserEmail(final String username, final String newEmail, final String validationBaseURL) throws UserManagementException { final User user = store.getUserByName(username); if (user == null) { throw new UserManagementException(UserManagementException.USER_DOES_NOT_EXIST); } logger.info("Subject "+SecurityUtils.getSubject().getPrincipal()+" is changing e-mail address of user " + username + " to " + newEmail); final String validationSecret = user.createRandomSecret(); apply(new UpdateSimpleUserEmailOperation(username, newEmail, validationSecret)); if (validationBaseURL != null && newEmail != null && !newEmail.trim().isEmpty()) { new Thread("e-mail validation after changing e-mail of user " + username + " to " + newEmail) { @Override public void run() { try { startEmailValidation(user, validationSecret, validationBaseURL); } catch (MailException e) { logger.log(Level.SEVERE, "Error sending mail to validate e-mail address change for user " + username + " to address " + newEmail, e); } } }.start(); } } @Override public Void internalUpdateSimpleUserEmail(final String username, final String newEmail, final String validationSecret) { final User user = getUserByName(username); user.setEmail(newEmail); user.startEmailValidation(validationSecret); store.updateUser(user); return null; } @Override public boolean validateEmail(String username, String validationSecret) throws UserManagementException { final User user = store.getUserByName(username); if (user == null) { throw new UserManagementException(UserManagementException.USER_DOES_NOT_EXIST); } return apply(new ValidateEmailOperation(username, validationSecret)); } @Override public Boolean internalValidateEmail(String username, String validationSecret) { final User user = store.getUserByName(username); final boolean result = user.validate(validationSecret); if (result) { store.updateUser(user); } return result; } /** * {@link UserImpl#startEmailValidation(String) Triggers} e-mail validation for the user object and sends out a * URL to the user's e-mail that has the validation secret ready for validation by clicking. * * @param validationSecret * the result of either {@link UserImpl#startEmailValidation(String)} or {@link UserImpl#setEmail(String)}. * @param baseURL * the URL under which the user can reach the e-mail validation service; this URL is required to assemble * a validation URL that is sent by e-mail to the user, to make the user return the validation secret to * the right server again. */ private void startEmailValidation(User user, String validationSecret, String baseURL) throws MailException { try { Map urlParameters = new HashMap<>(); urlParameters.put("u", URLEncoder.encode(user.getName(), "UTF-8")); urlParameters.put("v", URLEncoder.encode(validationSecret, "UTF-8")); StringBuilder url = buildURL(baseURL, urlParameters); sendMail(user.getName(), messages.get(user.getLocaleOrDefault(), "emailValidationSubject"), new StringBuilder() .append(messages.get(user.getLocaleOrDefault(), "emailValidationMessage", user.getName())) .append("\n").append(" ").append(url.toString()).toString()); } catch (UnsupportedEncodingException e) { logger.log(Level.SEVERE, "Internal error: encoding UTF-8 not found. Couldn't send e-mail to user " + user.getName() + " at e-mail address " + user.getEmail(), e); } } public StringBuilder buildURL(String baseURL, Map urlParameters) { StringBuilder url = new StringBuilder(baseURL==null?"":baseURL); // Potentially contained hash is checked to support place-based mail verification boolean first = baseURL == null || !baseURL.contains("?") || baseURL.contains("#"); for (Map.Entry e : urlParameters.entrySet()) { if (first) { url.append('?'); first = false; } else { url.append('&'); } url.append(e.getKey()); url.append('='); url.append(e.getValue()); } return url; } protected String hashPassword(String password, Object salt) { return new Sha256Hash(password, salt, 1024).toBase64(); } @Override public RoleDefinition createRoleDefinition(UUID roleId, String name) { logger.info("Subject "+SecurityUtils.getSubject().getPrincipal()+" created role "+name+" with ID "+roleId); return apply(new CreateRoleDefinitionOperation(roleId, name)); } @Override public RoleDefinition internalCreateRoleDefinition(UUID roleId, String name) { return store.createRoleDefinition(roleId, name, Collections.emptySet()); } @Override public void deleteRoleDefinition(RoleDefinition roleDefinition) { logger.info("Subject "+SecurityUtils.getSubject().getPrincipal()+" deleted role "+roleDefinition); final UUID roleId = roleDefinition.getId(); apply(new DeleteRoleDefinitionOperation(roleId)); } @Override public Void internalDeleteRoleDefinition(UUID roleId) { final RoleDefinition roleDefinition = store.getRoleDefinition(roleId); permissionChangeListeners.roleDefinitionRemoved(roleDefinition); store.removeRoleDefinition(roleDefinition); return null; } @Override public void updateRoleDefinition(RoleDefinition roleDefinitionWithNewProperties) { logger.info("Subject "+SecurityUtils.getSubject().getPrincipal()+" updated role "+roleDefinitionWithNewProperties); apply(new UpdateRoleDefinitionOperation(roleDefinitionWithNewProperties)); } @Override public Void internalUpdateRoleDefinition(RoleDefinition roleWithNewProperties) { final RoleDefinition role = store.getRoleDefinition(roleWithNewProperties.getId()); role.setName(roleWithNewProperties.getName()); store.setRoleDefinitionDisplayName(roleWithNewProperties.getId(), role.getName()); permissionChangeListeners.permissionAddedToOrRemovedFromRoleDefinition(role, role.getPermissions(), roleWithNewProperties.getPermissions()); role.setPermissions(roleWithNewProperties.getPermissions()); store.setRoleDefinitionPermissions(role.getId(), role.getPermissions()); return null; } @Override public Iterable getRoleDefinitions() { Collection result = new ArrayList<>(); filterObjectsWithPermissionForCurrentUser(DefaultActions.READ, store.getRoleDefinitions(), t -> result.add(t)); return result; } private void addRoleForUserAndSetUserAsOwner(User user, Role role) { addRoleForUser(user.getName(), role); TypeRelativeObjectIdentifier associationTypeIdentifier = PermissionAndRoleAssociation.get(role, user); QualifiedObjectIdentifier qualifiedTypeIdentifier = SecuredSecurityTypes.ROLE_ASSOCIATION .getQualifiedObjectIdentifier(associationTypeIdentifier); setOwnership(qualifiedTypeIdentifier, user, /* owning group */ null); } @Override public void addRoleForUser(User user, Role role) { addRoleForUser(user.getName(), role); } @Override public void addRoleForUser(String username, Role role) { logger.info("Subject "+SecurityUtils.getSubject().getPrincipal()+" added role "+role+" to user "+username); final UUID roleDefinitionId = role.getRoleDefinition().getId(); final UUID idOfTenantQualifyingRole = role.getQualifiedForTenant() == null ? null : role.getQualifiedForTenant().getId(); final String nameOfUserQualifyingRole = role.getQualifiedForUser() == null ? null : role.getQualifiedForUser().getName(); final Boolean transitive = role.isTransitive(); apply(new AddRoleForUserOperation(username, roleDefinitionId, idOfTenantQualifyingRole, nameOfUserQualifyingRole, transitive)); } @Override public Void internalAddRoleForUser(String username, UUID roleDefinitionId, UUID idOfTenantQualifyingRole, String nameOfUserQualifyingRole, Boolean transitive) throws UserManagementException { final Role role = new Role(getRoleDefinition(roleDefinitionId), getUserGroup(idOfTenantQualifyingRole), getUserByName(nameOfUserQualifyingRole), transitive); permissionChangeListeners.roleAddedToOrRemovedFromUser(getUserByName(username), role); store.addRoleForUser(username, role); return null; } @Override public void removeRoleFromUser(User user, Role role) { removeRoleFromUser(user.getName(), role); } @Override public void removeRoleFromUser(String username, Role role) { logger.info("Subject "+SecurityUtils.getSubject().getPrincipal()+" removed role "+role+" to user "+username); final UUID roleDefinitionId = role.getRoleDefinition().getId(); final UUID idOfTenantQualifyingRole = role.getQualifiedForTenant() == null ? null : role.getQualifiedForTenant().getId(); final String nameOfUserQualifyingRole = role.getQualifiedForUser() == null ? null : role.getQualifiedForUser().getName(); final Boolean transitive = role.isTransitive(); apply(new RemoveRoleFromUserOperation(username, roleDefinitionId, idOfTenantQualifyingRole, nameOfUserQualifyingRole, transitive)); } @Override public Void internalRemoveRoleFromUser(String username, UUID roleDefinitionId, UUID idOfTenantQualifyingRole, String nameOfUserQualifyingRole, Boolean transitive) throws UserManagementException { final Role role = new Role(getRoleDefinition(roleDefinitionId), getUserGroup(idOfTenantQualifyingRole), getUserByName(nameOfUserQualifyingRole), transitive); permissionChangeListeners.roleAddedToOrRemovedFromUser(getUserByName(username), role); store.removeRoleFromUser(username, role); return null; } @Override public void removePermissionFromUser(String username, WildcardPermission permissionToRemove) { logger.info("Subject "+SecurityUtils.getSubject().getPrincipal()+" is removing permission "+permissionToRemove+" from user "+username); apply(new RemovePermissionForUserOperation(username, permissionToRemove)); } @Override public Void internalRemovePermissionForUser(String username, WildcardPermission permissionToRemove) throws UserManagementException { permissionChangeListeners.permissionAddedToOrRemovedFromUser(getUserByName(username), permissionToRemove); store.removePermissionFromUser(username, permissionToRemove); return null; } @Override public void addPermissionForUser(String username, WildcardPermission permissionToAdd) { logger.info("Subject "+SecurityUtils.getSubject().getPrincipal()+" is adding permission "+permissionToAdd+" to user "+username); apply(new AddPermissionForUserOperation(username, permissionToAdd)); } @Override public Void internalAddPermissionForUser(String username, WildcardPermission permissionToAdd) throws UserManagementException { permissionChangeListeners.permissionAddedToOrRemovedFromUser(getUserByName(username), permissionToAdd); store.addPermissionForUser(username, permissionToAdd); return null; } @Override public void deleteUser(String username) throws UserManagementException { logger.info("Subject "+SecurityUtils.getSubject().getPrincipal()+" is deleting user "+username); final User userToDelete = store.getUserByName(username); if (userToDelete == null) { throw new UserManagementException(UserManagementException.USER_DOES_NOT_EXIST); } apply(new DeleteUserOperation(username)); } @Override public Void internalDeleteUser(String username) throws UserManagementException { User userToDelete = store.getUserByName(username); if (userToDelete != null) { permissionChangeListeners.userDeleted(userToDelete); // remove all permissions the user has accessControlStore.removeAllOwnershipsFor(userToDelete); store.deleteUser(username); final String defaultTenantNameForUsername = getDefaultTenantNameForUsername(username); final UserGroup defaultTenantUserGroup = getUserGroupByName(defaultTenantNameForUsername); if (defaultTenantUserGroup != null) { List usersInGroupList = Util.asList(defaultTenantUserGroup.getUsers()); if (usersInGroupList.isEmpty()) { // no other user is in group, delete it as well and remove Owenerships try { internalDeleteUserGroup(defaultTenantUserGroup.getId()); } catch (UserGroupManagementException e) { logger.log(Level.SEVERE, "Could not delete default tenant for user", e); } } } } return null; } @Override public boolean setSetting(String key, Object setting) { return apply(new SetSettingOperation(key, setting)); } @Override public Boolean internalSetSetting(String key, Object setting) { return store.setSetting(key, setting); } @Override public Map getAllSettings() { return store.getAllSettings(); } @Override public Map> getAllSettingTypes() { return store.getAllSettingTypes(); } @Override public User verifySocialUser(Credential credential) throws UserManagementException { OAuthToken otoken = new OAuthToken(credential, credential.getVerifier()); Subject subject = SecurityUtils.getSubject(); if (!subject.isAuthenticated()) { try { subject.login(otoken); logger.info("User [" + subject.getPrincipal().toString() + "] logged in successfully."); } catch (UnknownAccountException uae) { logger.info("There is no user with username of " + subject.getPrincipal()); throw new UserManagementException("Invalid credentials!"); } catch (IncorrectCredentialsException ice) { logger.info("Password for account " + subject.getPrincipal() + " was incorrect!"); throw new UserManagementException("Invalid credentials!"); } catch (LockedAccountException lae) { logger.info("The account for username " + subject.getPrincipal() + " is locked. " + "Please contact your administrator to unlock it."); throw new UserManagementException("Invalid credentials!"); } catch (AuthenticationException ae) { logger.log(Level.SEVERE, ae.getLocalizedMessage()); throw new UserManagementException("An error occured while authenticating the user!"); } } String username = subject.getPrincipal().toString(); if (username == null) { logger.info("Something went wrong while authneticating, check doGetAuthenticationInfo() in " + OAuthRealm.class.getName() + "."); throw new UserManagementException("An error occured while authenticating the user!"); } User user = store.getUserByName(username); if (user == null) { logger.info("Could not find user " + username); throw new UserManagementException("An error occured while authenticating the user!"); } return user; } @Override public User getCurrentUser() { final User result; Subject subject = SecurityUtils.getSubject(); if (subject == null || !subject.isAuthenticated()) { result = null; } else { Object principal = subject.getPrincipal(); if (principal == null) { result = null; } else { String username = principal.toString(); if (username == null || username.length() <= 0) { result = null; } else { result = store.getUserByName(username); } } } return result; } @Override public String getAuthenticationUrl(Credential credential) throws UserManagementException { Token requestToken = null; String authorizationUrl = null; int authProvider = credential.getAuthProvider(); OAuthService service = getOAuthService(authProvider); if (service == null) { throw new UserManagementException("Could not build OAuthService"); } if (authProvider == ClientUtils.TWITTER || authProvider == ClientUtils.YAHOO || authProvider == ClientUtils.LINKEDIN || authProvider == ClientUtils.FLICKR || authProvider == ClientUtils.IMGUR || authProvider == ClientUtils.TUMBLR || authProvider == ClientUtils.VIMEO || authProvider == ClientUtils.GOOGLE) { String authProviderName = ClientUtils.getAuthProviderName(authProvider); logger.info(authProviderName + " requires Request token first.. obtaining.."); try { requestToken = service.getRequestToken(); logger.info("Got request token: " + requestToken); // we must save in the session. It will be required to // get the access token SessionUtils.saveRequestTokenToSession(requestToken); } catch (Exception e) { throw new UserManagementException("Could not get request token for " + authProvider + " " + e.getMessage()); } } logger.info("Getting Authorization url..."); try { authorizationUrl = service.getAuthorizationUrl(requestToken); // Facebook has optional state var to protect against CSFR. // We'll use it if (authProvider == ClientUtils.FACEBOOK || authProvider == ClientUtils.GITHUB || authProvider == ClientUtils.INSTAGRAM) { String state = UUID.randomUUID().toString(); authorizationUrl += "&state=" + state; SessionUtils.saveStateToSession(state); } } catch (Exception e) { e.printStackTrace(); throw new UserManagementException("Could not get Authorization url: "); } if (authProvider == ClientUtils.FLICKR) { authorizationUrl += "&perms=read"; } if (authProvider == ClientUtils.FACEBOOK) { authorizationUrl += "&scope=email"; } logger.info("Authorization url: " + authorizationUrl); return authorizationUrl; } private OAuthService getOAuthService(int authProvider) { OAuthService service = null; switch (authProvider) { case ClientUtils.FACEBOOK: { service = new ServiceBuilder().provider(FacebookApi.class) .apiKey(store.getSetting(SocialSettingsKeys.OAUTH_FACEBOOK_APP_ID.name(), String.class)) .apiSecret(store.getSetting(SocialSettingsKeys.OAUTH_FACEBOOK_APP_SECRET.name(), String.class)) .callback(ClientUtils.getCallbackUrl()).build(); break; } case ClientUtils.GOOGLE: { service = new ServiceBuilder().provider(GoogleApi.class) .apiKey(store.getSetting(SocialSettingsKeys.OAUTH_GOOGLE_APP_ID.name(), String.class)) .apiSecret(store.getSetting(SocialSettingsKeys.OAUTH_GOOGLE_APP_SECRET.name(), String.class)) .scope(store.getSetting(SocialSettingsKeys.OAUTH_GOOGLE_SCOPE.name(), String.class)) .callback(ClientUtils.getCallbackUrl()).build(); break; } case ClientUtils.TWITTER: { service = new ServiceBuilder().provider(TwitterApi.class) .apiKey(store.getSetting(SocialSettingsKeys.OAUTH_TWITTER_APP_ID.name(), String.class)) .apiSecret(store.getSetting(SocialSettingsKeys.OAUTH_TWITTER_APP_SECRET.name(), String.class)) .callback(ClientUtils.getCallbackUrl()).build(); break; } case ClientUtils.YAHOO: { service = new ServiceBuilder().provider(YahooApi.class) .apiKey(store.getSetting(SocialSettingsKeys.OAUTH_YAHOO_APP_ID.name(), String.class)) .apiSecret(store.getSetting(SocialSettingsKeys.OAUTH_YAHOO_APP_SECRET.name(), String.class)) .callback(ClientUtils.getCallbackUrl()).build(); break; } case ClientUtils.LINKEDIN: { service = new ServiceBuilder().provider(LinkedInApi.class) .apiKey(store.getSetting(SocialSettingsKeys.OAUTH_LINKEDIN_APP_ID.name(), String.class)) .apiSecret(store.getSetting(SocialSettingsKeys.OAUTH_LINKEDIN_APP_SECRET.name(), String.class)) .callback(ClientUtils.getCallbackUrl()).build(); break; } case ClientUtils.INSTAGRAM: { service = new ServiceBuilder().provider(InstagramApi.class) .apiKey(store.getSetting(SocialSettingsKeys.OAUTH_INSTAGRAM_APP_ID.name(), String.class)) .apiSecret(store.getSetting(SocialSettingsKeys.OAUTH_INSTAGRAM_APP_SECRET.name(), String.class)) .callback(ClientUtils.getCallbackUrl()).build(); break; } case ClientUtils.GITHUB: { service = new ServiceBuilder().provider(GithubApi.class) .apiKey(store.getSetting(SocialSettingsKeys.OAUTH_GITHUB_APP_ID.name(), String.class)) .apiSecret(store.getSetting(SocialSettingsKeys.OAUTH_GITHUB_APP_SECRET.name(), String.class)) .callback(ClientUtils.getCallbackUrl()).build(); break; } case ClientUtils.IMGUR: { service = new ServiceBuilder().provider(ImgUrApi.class) .apiKey(store.getSetting(SocialSettingsKeys.OAUTH_IMGUR_APP_ID.name(), String.class)) .apiSecret(store.getSetting(SocialSettingsKeys.OAUTH_IMGUR_APP_SECRET.name(), String.class)) .callback(ClientUtils.getCallbackUrl()).build(); break; } case ClientUtils.FLICKR: { service = new ServiceBuilder().provider(FlickrApi.class) .apiKey(store.getSetting(SocialSettingsKeys.OAUTH_FLICKR_APP_ID.name(), String.class)) .apiSecret(store.getSetting(SocialSettingsKeys.OAUTH_FLICKR_APP_SECRET.name(), String.class)) .callback(ClientUtils.getCallbackUrl()).build(); break; } case ClientUtils.VIMEO: { service = new ServiceBuilder().provider(VimeoApi.class) .apiKey(store.getSetting(SocialSettingsKeys.OAUTH_VIMEO_APP_ID.name(), String.class)) .apiSecret(store.getSetting(SocialSettingsKeys.OAUTH_VIMEO_APP_SECRET.name(), String.class)) .callback(ClientUtils.getCallbackUrl()).build(); break; } case ClientUtils.WINDOWS_LIVE: { // a Scope must be specified service = new ServiceBuilder().provider(LiveApi.class) .apiKey(store.getSetting(SocialSettingsKeys.OAUTH_WINDOWS_LIVE_APP_ID.name(), String.class)) .apiSecret(store.getSetting(SocialSettingsKeys.OAUTH_WINDOWS_LIVE_APP_SECRET.name(), String.class)) .callback(ClientUtils.getCallbackUrl()).scope("wl.basic").build(); break; } case ClientUtils.TUMBLR: { service = new ServiceBuilder().provider(TumblrApi.class) .apiKey(store.getSetting(SocialSettingsKeys.OAUTH_TUMBLR_LIVE_APP_ID.name(), String.class)) .apiSecret(store.getSetting(SocialSettingsKeys.OAUTH_TUMBLR_LIVE_APP_SECRET.name(), String.class)) .callback(ClientUtils.getCallbackUrl()).build(); break; } case ClientUtils.FOURSQUARE: { service = new ServiceBuilder().provider(Foursquare2Api.class) .apiKey(store.getSetting(SocialSettingsKeys.OAUTH_FOURSQUARE_APP_ID.name(), String.class)) .apiSecret(store.getSetting(SocialSettingsKeys.OAUTH_FOURSQUARE_APP_SECRET.name(), String.class)) .callback(ClientUtils.getCallbackUrl()).build(); break; } default: { return null; } } return service; } @Override public void addSetting(String key, Class clazz) throws UserManagementException { if (!isValidSettingsKey(key)) { throw new UserManagementException("Invalid key!"); } apply(new AddSettingOperation(key, clazz)); } @Override public Void internalAddSetting(String key, Class clazz) { store.addSetting(key, clazz); return null; } public static boolean isValidSettingsKey(String key) { char[] characters = key.toCharArray(); for (char c : characters) { if (!Character.isLetter(c) && c != '_') { return false; } } return true; } @Override public void refreshSecurityConfig(ServletContext context) { logger.info("Refreshing security configuration!"); IniWebEnvironment env = (IniWebEnvironment) WebUtils.getRequiredWebEnvironment(context); System.out.println("Env: " + env); FilterChainResolver resolver = env.getFilterChainResolver(); System.out.println("Resolver: " + resolver); if (resolver instanceof PathMatchingFilterChainResolver) { PathMatchingFilterChainResolver pmfcr = (PathMatchingFilterChainResolver) resolver; FilterChainManager filterChainManager = pmfcr.getFilterChainManager(); System.out.println(filterChainManager); Set chainNames = filterChainManager.getChainNames(); System.out.println("Chains:"); for (String s : chainNames) { System.out.println(s + ": " + Arrays.toString(filterChainManager.getChain(s).toArray(new Filter[0]))); } } } @Override public ReplicatingCacheManager getCacheManager() { return cacheManager; } @Override public void setPreference(final String username, final String key, final String value) { apply(new SetPreferenceOperation(username, key, value)); } @Override public void setPreferenceObject(final String username, final String key, final Object value) { final String preferenceObjectAsString = internalSetPreferenceObject(username, key, value); apply(new SetPreferenceOperation(username, key, preferenceObjectAsString)); } @Override public Void internalSetPreference(final String username, final String key, final String value) { store.setPreference(username, key, value); return null; } @Override public String internalSetPreferenceObject(final String username, final String key, final Object value) { return store.setPreferenceObject(username, key, value); } @Override public void unsetPreference(String username, String key) { apply(new UnsetPreferenceOperation(username, key)); } @Override public Void internalUnsetPreference(String username, String key) { store.unsetPreference(username, key); return null; } @Override public Void internalSetAccessToken(String username, String accessToken) { store.setAccessToken(username, accessToken); return null; } @Override public String getAccessToken(String username) { return store.getAccessToken(username); } @Override public String getOrCreateAccessToken(String username) { String result = store.getAccessToken(username); if (result == null) { result = createAccessToken(username); } return result; } @Override public String getOrCreateTargetServerBearerToken(String targetServerUrlAsString, String targetServerUsername, String targetServerPassword, String targetServerBearerToken) { if ((Util.hasLength(targetServerUsername) || Util.hasLength(targetServerPassword)) && Util.hasLength(targetServerBearerToken)) { final IllegalArgumentException e = new IllegalArgumentException("Please use either username/password or bearer token, not both."); logger.log(Level.WARNING, e.getMessage(), e); throw e; } final User user = getCurrentUser(); // Default to current user's token final String effectiveTargetServerBearerToken; if (!Util.hasLength(targetServerUsername) && !Util.hasLength(targetServerPassword) && !Util.hasLength(targetServerBearerToken)) { effectiveTargetServerBearerToken = user == null ? null : getOrCreateAccessToken(user.getName()); } else { effectiveTargetServerBearerToken = targetServerBearerToken; } final String token = (!Util.hasLength(effectiveTargetServerBearerToken) ? targetServerUsername != null ? RemoteServerUtil.resolveBearerTokenForRemoteServer(targetServerUrlAsString, targetServerUsername, targetServerPassword) : null // in case no effective bearer token has been provided but no user name either : effectiveTargetServerBearerToken); return token; } @Override public Void internalRemoveAccessToken(String username) { store.removeAccessToken(username); return null; } @Override public String getPreference(String username, String key) { return store.getPreference(username, key); } @Override public Map getAllPreferences(String username) { return store.getAllPreferences(username); } @Override public String createAccessToken(String username) { logger.info("Subject "+SecurityUtils.getSubject().getPrincipal()+" is requesting a new access token for user "+username); User user = getUserByName(username); final String token; if (user != null) { RandomNumberGenerator rng = new SecureRandomNumberGenerator(); byte[] salt = rng.nextBytes().getBytes(); token = hashPassword(new String(rng.nextBytes().getBytes()), salt); apply(new SetAccessTokenOperation(user.getName(), token)); } else { token = null; } return token; } @Override public void removeAccessToken(String username) { final Subject subject = SecurityUtils.getSubject(); if (hasCurrentUserUpdatePermission(getUserByName(username))) { logger.info("Subject "+subject.getPrincipal()+" is removing the access token for user "+username); apply(new RemoveAccessTokenOperation(username)); } else { throw new org.apache.shiro.authz.AuthorizationException("User " + subject.getPrincipal().toString() + " does not have permission to remove access token of user " + username); } } @Override public UserGroup getServerGroup() { return store.getServerGroup(); } @Override public T setOwnershipCheckPermissionForObjectCreationAndRevertOnError( HasPermissions type, TypeRelativeObjectIdentifier typeIdentifier, String securityDisplayName, Callable actionWithResult) { return setOwnershipCheckPermissionForObjectCreationAndRevertOnError(type, typeIdentifier, securityDisplayName, actionWithResult, /* check for SERVER:CREATE_OBJECT */ true); } /** * For the current session's {@link Subject} an ownership for an object of type {@code type} and with * type-relative object identifier {@code typeRelativeIdentifier} is created with the subject's * default creation group if no ownership for that object is found yet. Otherwise, the existing ownership * is left untouched.

* * If {@code doServerCreateObjectCheck} is {@code true}, the {@link ServerActions#CREATE_OBJECT} permission * is checked for the executing server.

* * Then, the {@link DefaultActions#CREATE} permission is checked for the {@code type}/{@code typeRelativeIdentifier} * object.

* * If any of these permission checks fails and the ownership has not been found but created, the ownership * is removed again, and the authorization exception is thrown by this method. Otherwise, the subject is * considered to have the permission to create the object, the {@code actionWithResult} is invoked, and * the object returned by the action is the result of this method. */ private T setOwnershipCheckPermissionForObjectCreationAndRevertOnError(HasPermissions type, TypeRelativeObjectIdentifier typeRelativeIdentifier, String securityDisplayName, Callable actionWithResult, boolean doServerCreateObjectCheck) { QualifiedObjectIdentifier identifier = type.getQualifiedObjectIdentifier(typeRelativeIdentifier); T result = null; boolean didSetOwnership = false; try { final OwnershipAnnotation preexistingOwnership = getOwnership(identifier); if (preexistingOwnership == null) { didSetOwnership = true; setDefaultOwnership(identifier, securityDisplayName); } else { logger.fine("Preexisting ownership found for " + identifier + ": " + preexistingOwnership); } if (doServerCreateObjectCheck) { checkCurrentUserServerPermission(ServerActions.CREATE_OBJECT); } try { SecurityUtils.getSubject().checkPermission(identifier.getStringPermission(DefaultActions.CREATE)); } catch (AuthorizationException e) { if (didSetOwnership) { throw e; } else { // An ownership for this ID already exists and the user is not permitted // -> a nicer error is produced to explain the user that a name clash is most probably the cause throw new UnauthorizedException(MessageFormat.format( "You are not permitted to create a \"{0}\" with name or identifier \"{1}\". " + "This is most probably caused by an already existing entry with the same name/identifier. " + "Please try to use a different name.", identifier.getTypeIdentifier(), identifier.getTypeRelativeObjectIdentifier().toString()), e); } } result = actionWithResult.call(); } catch (AuthorizationException e) { if (didSetOwnership) { deleteOwnership(identifier); } throw e; } catch (Exception e) { throw new RuntimeException(e); } return result; } @Override public boolean hasCurrentUserServerPermission(ServerActions action) { return SecurityUtils.getSubject().isPermitted(constructServerPermissionString(action)); } @Override public void checkCurrentUserServerPermission(ServerActions action) { SecurityUtils.getSubject().checkPermission(constructServerPermissionString(action)); } private String constructServerPermissionString(ServerActions action) { return SecuredSecurityTypes.SERVER.getStringPermissionForTypeRelativeIdentifier( action, new TypeRelativeObjectIdentifier(ServerInfo.getName())); } @Override public T setOwnershipWithoutCheckPermissionForObjectCreationAndRevertOnError(HasPermissions type, TypeRelativeObjectIdentifier typeIdentifier, String securityDisplayName, Callable actionWithResult) { return setOwnershipCheckPermissionForObjectCreationAndRevertOnError(type, typeIdentifier, securityDisplayName, actionWithResult, false); } @Override public void setDefaultOwnership(QualifiedObjectIdentifier identifier, String description) { setOwnership(identifier, getCurrentUser(), getDefaultTenantForCurrentUser(), description); } @Override public void setDefaultOwnershipIfNotSet(QualifiedObjectIdentifier identifier) { final OwnershipAnnotation preexistingOwnership = getOwnership(identifier); if (preexistingOwnership == null || (preexistingOwnership.getAnnotation() == null || (preexistingOwnership.getAnnotation().getTenantOwner() == null && preexistingOwnership.getAnnotation().getUserOwner() == null))) { setDefaultOwnership(identifier, identifier.toString()); } } @Override public void setOwnershipCheckPermissionForObjectCreationAndRevertOnError(HasPermissions type, TypeRelativeObjectIdentifier typeRelativeObjectIdentifier, String securityDisplayName, Action actionToCreateObject) { setOwnershipCheckPermissionForObjectCreationAndRevertOnError(type, typeRelativeObjectIdentifier, securityDisplayName, () -> { actionToCreateObject.run(); return null; }); } @Override public void setOwnershipWithoutCheckPermissionForObjectCreationAndRevertOnError(HasPermissions type, TypeRelativeObjectIdentifier typeRelativeObjectIdentifier, String securityDisplayName, Action actionToCreateObject) { setOwnershipWithoutCheckPermissionForObjectCreationAndRevertOnError(type, typeRelativeObjectIdentifier, securityDisplayName, () -> { actionToCreateObject.run(); return null; }); } @Override public void setOwnershipIfNotSet(QualifiedObjectIdentifier identifier, User user, UserGroup tenantOwner) { final OwnershipAnnotation preexistingOwnership = getOwnership(identifier); if (preexistingOwnership == null || (preexistingOwnership.getAnnotation() == null || (preexistingOwnership.getAnnotation().getTenantOwner() == null && preexistingOwnership.getAnnotation().getUserOwner() == null))) { setOwnership(identifier, user, tenantOwner, identifier.toString()); } } /** * Special case for user creation, as no currentUser might exist when registering anonymous, and since a user always * should own itself as userOwner */ @Override public User checkPermissionForUserCreationAndRevertOnErrorForUserCreation(String username, Callable createActionReturningCreatedObject) throws UserManagementException { QualifiedObjectIdentifier identifier = SecuredSecurityTypes.USER .getQualifiedObjectIdentifier(UserImpl.getTypeRelativeObjectIdentifier(username)); User result = null; try { SecurityUtils.getSubject().checkPermission(identifier.getStringPermission(DefaultActions.CREATE)); result = createActionReturningCreatedObject.call(); } catch (AuthorizationException e) { logger.warning("Unauthorized request to create user with name \""+username+"\": "+e.getMessage()); throw e; } catch (UserManagementException e) { throw e; } catch (Exception e) { throw new RuntimeException(e); } return result; } @Override public void checkPermissionAndDeleteOwnershipForObjectRemoval(WithQualifiedObjectIdentifier object, Action actionToDeleteObject) { checkPermissionAndDeleteOwnershipForObjectRemoval(object, () -> { actionToDeleteObject.run(); return null; }); } @Override public T checkPermissionAndDeleteOwnershipForObjectRemoval(WithQualifiedObjectIdentifier object, Callable actionToDeleteObject) { QualifiedObjectIdentifier identifier = object.getIdentifier(); return checkPermissionAndDeleteOwnershipForObjectRemoval(identifier, actionToDeleteObject); } @Override public T checkPermissionAndDeleteOwnershipForObjectRemoval(QualifiedObjectIdentifier identifier, Callable actionToDeleteObject) { try { SecurityUtils.getSubject().checkPermission(identifier.getStringPermission(DefaultActions.DELETE)); final T result = actionToDeleteObject.call(); deleteAllDataForRemovedObject(identifier); return result; } catch (UnauthorizedException e) { throw e; } catch (Exception e) { throw new RuntimeException(e); } } @Override public void checkPermissionAndDeleteOwnershipForObjectRemoval(QualifiedObjectIdentifier identifier, Action actionToDeleteObject) { checkPermissionAndDeleteOwnershipForObjectRemoval(identifier, () -> { actionToDeleteObject.run(); return null; }); } @Override public void deleteAllDataForRemovedObject(QualifiedObjectIdentifier identifier) { deleteOwnership(identifier); deleteAccessControlList(identifier); } @Override public void filterObjectsWithPermissionForCurrentUser( HasPermissions.Action action, Iterable objectsToFilter, Consumer filteredObjectsConsumer) { objectsToFilter.forEach(objectToCheck -> { if (SecurityUtils.getSubject().isPermitted(new ShiroWildcardPermissionFromParts( objectToCheck.getIdentifier().getPermission(action)))) { filteredObjectsConsumer.accept(objectToCheck); } }); } /** Filters the objects with any of the given permissions for the current user */ @Override public void filterObjectsWithAnyPermissionForCurrentUser( HasPermissions.Action[] actions, Iterable objectsToFilter, Consumer filteredObjectsConsumer) { objectsToFilter.forEach(objectToCheck -> { boolean isPermitted = false; for (int i = 0; i < actions.length; i++) { if (SecurityUtils.getSubject() .isPermitted(new ShiroWildcardPermissionFromParts(objectToCheck.getIdentifier().getPermission(actions[i])))) { isPermitted = true; break; } } if (isPermitted) { filteredObjectsConsumer.accept(objectToCheck); } }); } @Override public List mapAndFilterByReadPermissionForCurrentUser( Iterable objectsToFilter, Function filteredObjectsMapper) { final List result = new ArrayList<>(); filterObjectsWithPermissionForCurrentUser(DefaultActions.READ, objectsToFilter, filteredObject -> result.add(filteredObjectsMapper.apply(filteredObject))); return result; } @Override public List mapAndFilterByAnyExplicitPermissionForCurrentUser( HasPermissions permittedObject, HasPermissions.Action[] actions, Iterable objectsToFilter, Function filteredObjectsMapper) { final List result = new ArrayList<>(); filterObjectsWithAnyPermissionForCurrentUser(actions, objectsToFilter, filteredObject -> result.add(filteredObjectsMapper.apply(filteredObject))); return result; } @Override public User getAllUser() { return store.getUserByName(SecurityService.ALL_USERNAME); } @Override public boolean hasCurrentUserMetaPermission(WildcardPermission permissionToCheck, Ownership ownership) { return PermissionChecker.checkMetaPermission(permissionToCheck, hasPermissionsProvider.getAllHasPermissions(), getCurrentUser(), getAllUser(), ownership, aclResolver); } @Override public boolean hasCurrentUserMetaPermissionWithOwnershipLookup(WildcardPermission permissionToCheck) { return PermissionChecker.checkMetaPermissionWithOwnershipResolution(permissionToCheck, hasPermissionsProvider.getAllHasPermissions(), getCurrentUser(), getAllUser(), qualifiedObjectId -> { OwnershipAnnotation ownershipAnnotation = accessControlStore.getOwnership(qualifiedObjectId); return ownershipAnnotation == null ? null : ownershipAnnotation.getAnnotation(); }, aclResolver); } @Override public boolean hasCurrentUserAnyPermission(WildcardPermission permissionToCheck) { User currentUser = getCurrentUser(); return PermissionChecker.hasUserAnyPermission(permissionToCheck, hasPermissionsProvider.getAllHasPermissions(), currentUser, getAllUser(), /* ownership */ null); } @Override public boolean hasCurrentUserMetaPermissionsOfRoleDefinitionWithQualification(final RoleDefinition roleDefinition, final Ownership qualificationForGrantedPermissions) { boolean result = true; for (WildcardPermission permissionToCheck : roleDefinition.getPermissions()) { if (!hasCurrentUserMetaPermission(permissionToCheck, qualificationForGrantedPermissions)) { result = false; break; } } return result; } @Override public boolean hasCurrentUserMetaPermissionsOfRoleDefinitionsWithQualification(Set roleDefinitions, Ownership qualificationForGrantedPermissions) { boolean result = true; for (final RoleDefinition roleDefinition : roleDefinitions) { result &= hasCurrentUserMetaPermissionsOfRoleDefinitionWithQualification(roleDefinition, qualificationForGrantedPermissions); if (!result) { break; } } return result; } @Override public RoleDefinition getOrCreateRoleDefinitionFromPrototype(final RolePrototype rolePrototype, boolean makeReadableForAll) { final RoleDefinition potentiallyExistingRoleDefinition = store.getRoleDefinition(rolePrototype.getId()); final RoleDefinition result; if (potentiallyExistingRoleDefinition == null) { result = store.createRoleDefinition(rolePrototype.getId(), rolePrototype.getName(), rolePrototype.getPermissions()); setOwnership(result.getIdentifier(), null, getServerGroup()); } else if (rolePrototype.getPermissions() != null && !rolePrototype.getPermissions().equals(potentiallyExistingRoleDefinition.getPermissions())) { store.setRoleDefinitionPermissions(potentiallyExistingRoleDefinition.getId(), rolePrototype.getPermissions()); RoleDefinition roleDefinition = store.getRoleDefinition(rolePrototype.getId()); result = roleDefinition; } else { result = potentiallyExistingRoleDefinition; } if (makeReadableForAll) { AccessControlListAnnotation acl = getOrCreateAccessControlList(result.getIdentifier()); final Set allowedActions = acl.getAnnotation().getAllowedActions(null); if (!allowedActions.contains(DefaultActions.READ.name())) { // make role publicly readable addToAccessControlList(result.getIdentifier(), /* for all users */ null, DefaultActions.READ.name()); } } return result; } @Override public void setCORSFilterConfigurationToWildcard(String serverName) { apply(s->s.internalSetCORSFilterConfigurationToWildcard(serverName)); } @Override public Void internalSetCORSFilterConfigurationToWildcard(String serverName) { if (Util.equalsWithNull(serverName, ServerInfo.getName())) { getCORSFilterConfiguration().setWildcard(); } corsFilterConfigurationsByReplicaSetName.put(serverName, new Pair<>(true, Collections.emptySet())); PersistenceFactory.INSTANCE.getDefaultMongoObjectFactory().storeCORSFilterConfigurationIsWildcard(serverName); return null; } @Override public void setCORSFilterConfigurationAllowedOrigins(String serverName, String... allowedOrigins) throws IllegalArgumentException { for (final String allowedOrigin : allowedOrigins) { if (!HttpHeaderUtil.isValidOriginHeaderValue(allowedOrigin)) { throw new IllegalArgumentException("\""+allowedOrigin+"\" is not a valid format for a CORS origin"); } } apply(s->s.internalSetCORSFilterConfigurationAllowedOrigins(serverName, allowedOrigins)); } @Override public Void internalSetCORSFilterConfigurationAllowedOrigins(String serverName, String... allowedOrigins) { final Iterable allowedOriginsAsList = allowedOrigins == null ? Collections.emptyList() : Arrays.asList(allowedOrigins); if (Util.equalsWithNull(serverName, ServerInfo.getName())) { getCORSFilterConfiguration().setOrigins(allowedOriginsAsList); } corsFilterConfigurationsByReplicaSetName.put(serverName, new Pair<>(false, Util.asNewSet(allowedOriginsAsList))); PersistenceFactory.INSTANCE.getDefaultMongoObjectFactory().storeCORSFilterConfigurationAllowedOrigins(serverName, allowedOrigins); return null; } @Override public Pair> getCORSFilterConfiguration(String serverName) { return corsFilterConfigurationsByReplicaSetName.get(serverName); } // ----------------- Replication ------------- @Override public void clearReplicaState() throws MalformedURLException, IOException, InterruptedException { store.clear(); accessControlStore.clear(); corsFilterConfigurationsByReplicaSetName.clear(); clientIPBasedLockingAndBanningForBearerTokenAuthentication.clear(); clientIPBasedLockingAndBanningForUserCreation.clear(); } @Override public Serializable getId() { return getClass().getName(); } @Override public ObjectInputStream createObjectInputStreamResolvingAgainstCache(InputStream is, Map> classLoaderCache) throws IOException { return new ObjectInputStreamResolvingAgainstSecurityCache(is, store, null, classLoaderCache); } @Override public void initiallyFillFromInternal(ObjectInputStream is) throws IOException, ClassNotFoundException, InterruptedException { logger.info("Reading cache manager..."); ReplicatingCacheManager newCacheManager = (ReplicatingCacheManager) is.readObject(); cacheManager.replaceContentsFrom(newCacheManager); // overriding thread context class loader because the user store may be provided by a different bundle; // We're assuming here that the user store service is provided by the same bundle in the replica as on the master. ClassLoader oldCCL = Thread.currentThread().getContextClassLoader(); if (store != null) { Thread.currentThread().setContextClassLoader(store.getClass().getClassLoader()); } logger.info("Reading user store..."); boolean createdServerGroup = false; try { final UserStore newUserStore = (UserStore) is.readObject(); final UserGroup newServerGroup = newUserStore.getUserGroupByName(store.getServerGroupName()); // before replacing the local UserStore's contents, capture the server group: final UserGroup oldServerGroup = store.getServerGroup(); store.replaceContentsFrom(newUserStore); if (newServerGroup == null) { // create the server group in a replication-aware fashion, making sure it appears on the master final String serverGroupName = store.getServerGroupName(); final UUID serverGroupUuid = UUID.randomUUID(); createUserGroupWithInitialUser(serverGroupUuid, serverGroupName, /* initial user */ null); store.setServerGroup(store.getUserGroupByName(store.getServerGroupName())); createdServerGroup = true; } else if (newServerGroup != oldServerGroup) { store.setServerGroup(newServerGroup); } } finally { Thread.currentThread().setContextClassLoader(oldCCL); } if (accessControlStore != null) { Thread.currentThread().setContextClassLoader(accessControlStore.getClass().getClassLoader()); } logger.info("Reading access control store..."); try { AccessControlStore newAccessControlStore = (AccessControlStore) is.readObject(); accessControlStore.replaceContentsFrom(newAccessControlStore); } finally { Thread.currentThread().setContextClassLoader(oldCCL); } // now ensure that the SERVER object has a valid ownership; use the (potentially new) server group as the default: migrateServerObject(); if (createdServerGroup) { final UserGroup serverGroup = store.getServerGroup(); setOwnership(serverGroup.getIdentifier(), null, serverGroup, serverGroup.getName()); final User adminUserOrNull = store.getUserByName(UserStore.ADMIN_USERNAME); if (adminUserOrNull == null) { logger.info("User 'admin' does not exist on replicated central SecurityService. " + "'admin' will not be properly set up for this server."); } else { addUserToUserGroup(serverGroup, adminUserOrNull); setDefaultTenantForCurrentServerForUser(UserStore.ADMIN_USERNAME, serverGroup.getId()); } isNewServer = true; } isInitialOrMigration = false; logger.info("Reading isSharedAcrossSubdomains..."); sharedAcrossSubdomainsOf = (String) is.readObject(); logger.info("...as "+sharedAcrossSubdomainsOf); logger.info("Reading baseUrlForCrossDomainStorage..."); baseUrlForCrossDomainStorage = (String) is.readObject(); logger.info("...as "+baseUrlForCrossDomainStorage); logger.info("Reading CORS filter configurations and possibly more..."); final SecurityServiceInitialLoadExtensionsDTO initialLoadExtensions = (SecurityServiceInitialLoadExtensionsDTO) is.readObject(); final ConcurrentMap>> newCORSFilterConfigurations = initialLoadExtensions.getCorsFilterConfigurationsByReplicaSetName(); corsFilterConfigurationsByReplicaSetName.putAll(newCORSFilterConfigurations); if (initialLoadExtensions.getClientIPBasedLockingAndBanningForBearerTokenAuthentication() != null) { // checking for null for backward compatibility; an older primary/master may not have known this field yet clientIPBasedLockingAndBanningForBearerTokenAuthentication.putAll(initialLoadExtensions.getClientIPBasedLockingAndBanningForBearerTokenAuthentication()); } if (initialLoadExtensions.getClientIPBasedLockingAndBanningForUserCreation() != null) { // checking for null for backward compatibility; an older primary/master may not have known this field yet clientIPBasedLockingAndBanningForUserCreation.putAll(initialLoadExtensions.getClientIPBasedLockingAndBanningForUserCreation()); } logger.info("Triggering SecurityInitializationCustomizers upon replication ..."); customizers.forEach(c -> c.customizeSecurityService(this)); logger.info("Done filling SecurityService"); } @Override public void serializeForInitialReplicationInternal(ObjectOutputStream objectOutputStream) throws IOException { objectOutputStream.writeObject(cacheManager); objectOutputStream.writeObject(store); objectOutputStream.writeObject(accessControlStore); objectOutputStream.writeObject(sharedAcrossSubdomainsOf); objectOutputStream.writeObject(baseUrlForCrossDomainStorage); objectOutputStream.writeObject(new SecurityServiceInitialLoadExtensionsDTO( corsFilterConfigurationsByReplicaSetName, clientIPBasedLockingAndBanningForBearerTokenAuthentication, clientIPBasedLockingAndBanningForUserCreation)); } @Override public boolean migrateOwnership(WithQualifiedObjectIdentifier identifier) { return migrateOwnership(identifier.getIdentifier(), identifier.getName()); } @Override public boolean migrateOwnership(final QualifiedObjectIdentifier identifier, final String displayName) { return this.migrateOwnership(identifier, null, /* setServerGroupAsOwner */ true, displayName); } @Override public void migrateUser(final User user) { // If no ownership migration was necessary, this is not a migration if (migrateOwnership(user.getIdentifier(), user, /* setServerGroupAsOwner */ false, user.getName())) { final String tenantNameForUsername = getDefaultTenantNameForUsername(user.getName()); // if there is already a default creation tenant set for the user, this is not a migration // If the user's tenant already exists, this is no migration if (user.getDefaultTenant(ServerInfo.getName()) == null && getUserGroupByName(tenantNameForUsername) == null) { try { final UserGroup tenantForUser = getOrCreateTenantForUser(user); setDefaultTenantForCurrentServerForUser(user.getName(), tenantForUser.getId()); } catch (UserGroupManagementException e) { logger.log(Level.SEVERE, "Error during migration while creating tenant for user: " + user, e); } // Only adding user role if it is most probably a migration case // In case an admin removes/changes the user role, it should not be recreated automatically addUserRoleToUser(user); final RoleDefinition adminRoleDefinition = getRoleDefinition(AdminRole.getInstance().getId()); for (Role roleOfUser : user.getRoles()) { if (roleOfUser.getRoleDefinition().equals(adminRoleDefinition)) { final UserGroup serverGroup = getServerGroup(); if (roleOfUser.getQualifiedForTenant() == null || roleOfUser.getQualifiedForTenant().equals(serverGroup)) { // The user is a server admin -> Add it to the server group to allow setting the server // group as default creation group addUserToUserGroup(serverGroup, user); if (UserStore.ADMIN_USERNAME.equals(user.getName())) { // For the "admin" user the server group is initially set as // default creation group. This is consistent to a newly created server // and in most cases this is the group, the admin is meant to work with. setDefaultTenantForCurrentServerForUser(user.getName(), getServerGroup().getId()); } } } } } } } @Override public void migratePermission(final User user, final WildcardPermission permissionToMigrate, final Function permissionReplacement) { final WildcardPermission replacementPermissionOrNull = permissionReplacement.apply(permissionToMigrate); final WildcardPermission effectivePermission; if (replacementPermissionOrNull != null) { // replacing legacy permission with a replacement String username = user.getName(); removePermissionFromUser(username, permissionToMigrate); addPermissionForUser(username, replacementPermissionOrNull); effectivePermission = replacementPermissionOrNull; } else { effectivePermission = permissionToMigrate; } final TypeRelativeObjectIdentifier associationTypeIdentifier = PermissionAndRoleAssociation.get(effectivePermission, user); final QualifiedObjectIdentifier associationQualifiedIdentifier = SecuredSecurityTypes.PERMISSION_ASSOCIATION .getQualifiedObjectIdentifier(associationTypeIdentifier); migrateOwnership(associationQualifiedIdentifier, associationQualifiedIdentifier.toString()); } private boolean migrateOwnership(final QualifiedObjectIdentifier identifier, User userOwnerToSet, boolean setServerGroupAsOwner, final String displayName) { boolean wasNecessaryToMigrate = false; final OwnershipAnnotation owner = this.getOwnership(identifier); // initialize ownerships on migration and fix objects that were orphaned by setting the owning user/group if (owner == null || owner.getAnnotation().getTenantOwner() == null && owner.getAnnotation().getUserOwner() == null) { final UserGroup tenantOwnerToSet = setServerGroupAsOwner ? this.getServerGroup() : null; logger.info("missing Ownership fixed: Setting ownership for: " + identifier + " to tenant: " + tenantOwnerToSet + "; user: " + userOwnerToSet); this.setOwnership(identifier, userOwnerToSet, tenantOwnerToSet, displayName); wasNecessaryToMigrate = true; } migratedHasPermissionTypes.add(identifier.getTypeIdentifier()); return wasNecessaryToMigrate; } @Override public void migrateServerObject() { final QualifiedObjectIdentifier serverIdentifier = SecuredSecurityTypes.SERVER .getQualifiedObjectIdentifier( new TypeRelativeObjectIdentifier(ServerInfo.getName())); migrateOwnership(serverIdentifier, serverIdentifier.toString()); } @Override public void checkMigration(Iterable allInstances) { Class clazz = Util.first(allInstances).getClass(); boolean allChecksSucessful = true; for (HasPermissions shouldBeMigrated : allInstances) { if (!migratedHasPermissionTypes.contains(shouldBeMigrated.getName())) { logger.severe("ensure Ownership failed: Did not check Ownerships for " + clazz.getName() + " missing: " + shouldBeMigrated); allChecksSucessful = false; } } if (allChecksSucessful) { logger.info("Ownership checks finished: Sucessfully checked all types in " + clazz.getName()); } } @Override public boolean hasCurrentUserReadPermission(WithQualifiedObjectIdentifier object) { return object == null ? true : SecurityUtils.getSubject().isPermitted( object.getPermissionType().getStringPermissionForObject(DefaultActions.READ, object)); } @Override public boolean hasCurrentUserUpdatePermission(WithQualifiedObjectIdentifier object) { return object == null ? true : SecurityUtils.getSubject().isPermitted( object.getPermissionType().getStringPermissionForObject(DefaultActions.UPDATE, object)); } @Override public boolean hasCurrentUserDeletePermission(WithQualifiedObjectIdentifier object) { return object == null ? true : SecurityUtils.getSubject().isPermitted( object.getPermissionType().getStringPermissionForObject(DefaultActions.DELETE, object)); } @Override public boolean hasCurrentUserExplicitPermissions(WithQualifiedObjectIdentifier object, HasPermissions.Action... actions) { boolean isPermitted = true; if (object != null) { for (int i = 0; i < actions.length; i++) { isPermitted &= SecurityUtils.getSubject() .isPermitted(object.getPermissionType().getStringPermissionForObject(actions[i], object)); } } return isPermitted; } @Override public boolean hasCurrentUserOneOfExplicitPermissions(WithQualifiedObjectIdentifier object, HasPermissions.Action... actions) { boolean result = object == null; if (object != null) { for (com.sap.sse.security.shared.HasPermissions.Action action : actions) { if (hasCurrentUserExplicitPermissions(object, action)) { result = true; break; } } } return result; } @Override public void checkCurrentUserReadPermission(WithQualifiedObjectIdentifier object) { if (object != null) { SecurityUtils.getSubject().checkPermission( object.getPermissionType().getStringPermissionForObject(DefaultActions.READ, object)); } } @Override public void checkCurrentUserUpdatePermission(WithQualifiedObjectIdentifier object) { if (object != null) { SecurityUtils.getSubject().checkPermission( object.getPermissionType().getStringPermissionForObject(DefaultActions.UPDATE, object)); } } @Override public void checkCurrentUserDeletePermission(WithQualifiedObjectIdentifier object) { if (object != null) { SecurityUtils.getSubject().checkPermission( object.getPermissionType().getStringPermissionForObject(DefaultActions.DELETE, object)); } } @Override public void checkCurrentUserDeletePermission(QualifiedObjectIdentifier identifier) { if (identifier != null) { SecurityUtils.getSubject().checkPermission(identifier.getStringPermission(DefaultActions.DELETE)); } } @Override public void checkCurrentUserExplicitPermissions(WithQualifiedObjectIdentifier object, HasPermissions.Action... actions) { if (object != null) { for (int i = 0; i < actions.length; i++) { SecurityUtils.getSubject() .checkPermission(object.getPermissionType().getStringPermissionForObject(actions[i], object)); } } } @Override public void checkCurrentUserHasOneOfExplicitPermissions(WithQualifiedObjectIdentifier object, HasPermissions.Action... actions) { if (object != null) { boolean isPermitted = false; for (int i = 0; i < actions.length; i++) { if (SecurityUtils.getSubject() .isPermitted(object.getPermissionType().getStringPermissionForObject(actions[i], object))) { isPermitted = true; break; } } if (!isPermitted) { throw new AuthorizationException(); } } } @Override public void assumeOwnershipMigrated(String typeName) { migratedHasPermissionTypes.add(typeName); } @Override public boolean hasUserAllWildcardPermissionsForAlreadyRealizedQualifications(RoleDefinition role, Iterable permissionsToCheck) { Pair> qualificationsToCheck = store.getExistingQualificationsForRoleDefinition(role); final Iterable effectiveQualificationsToCheck = Boolean.TRUE.equals(qualificationsToCheck.getA()) ? Collections.singletonList(null) : qualificationsToCheck.getB(); for (WildcardPermission permission : permissionsToCheck) { for (Ownership ownership : effectiveQualificationsToCheck) { if (!hasCurrentUserMetaPermission(permission, ownership)) { return false; } } } return true; } @Override public T getPreferenceObject(String username, String key) { return store.getPreferenceObject(username, key); } @Override public Map getPreferenceObjectsByKey(String key) { return store.getPreferenceObjectsByKey(key); } @Override public void setDefaultTenantForCurrentServerForUser(String username, UUID defaultTenantId) { final String serverName = ServerInfo.getName(); apply(new SetDefaultTenantForServerForUserOperation(username, defaultTenantId, serverName)); } @Override public Void internalSetDefaultTenantForServerForUser(String username, UUID defaultTenantId, String serverName) { User user = getUserByName(username); UserGroup newDefaultTenant = getUserGroup(defaultTenantId); store.setDefaultTennantForUserAndUpdate(user, newDefaultTenant, serverName); return null; } @Override /** * This method does not handle RoleAssociationOwnerships! this must be done via the callback */ public void copyUsersAndRoleAssociations(UserGroup source, UserGroup destination, RoleCopyListener callback) { for (User user : source.getUsers()) { addUserToUserGroup(destination, user); } for (Map.Entry entr : source.getRoleDefinitionMap().entrySet()) { putRoleDefinitionToUserGroup(destination, entr.getKey(), entr.getValue()); } for (Pair userAndRole : store.getRolesQualifiedByUserGroup(source)) { final Role existingRole = userAndRole.getB(); final Role copyRole = new Role(existingRole.getRoleDefinition(), destination, existingRole.getQualifiedForUser(), existingRole.isTransitive()); addRoleForUser(userAndRole.getA(), copyRole); callback.onRoleCopy(userAndRole.getA(), existingRole, copyRole); } } @Override public T doWithTemporaryDefaultTenant(UserGroup tenant, Callable action) { final UserGroup previousValue = temporaryDefaultTenant.get(); temporaryDefaultTenant.set(tenant); try { return action.call(); } catch (RuntimeException e) { throw e; } catch (Exception e) { throw new RuntimeException(e); } finally { temporaryDefaultTenant.set(previousValue); } } @Override // See com.sap.sse.security.impl.Activator.clearState(), moved due to required reinitialisation sequence for // permission-vertical public void clearState() throws Exception { clientIPBasedLockingAndBanningForBearerTokenAuthentication.clear(); clientIPBasedLockingAndBanningForUserCreation.clear(); } @Override public void storeSession(String cacheName, Session session) { PersistenceFactory.INSTANCE.getDefaultMongoObjectFactory().storeSession(cacheName, session); } @Override public void removeSession(String cacheName, Session session) { PersistenceFactory.INSTANCE.getDefaultMongoObjectFactory().removeSession(cacheName, session); } @Override public void removeAllSessions(String cacheName) { PersistenceFactory.INSTANCE.getDefaultMongoObjectFactory().removeAllSessions(cacheName); } @Override public boolean isInitialOrMigration() { return isInitialOrMigration; } @Override public boolean isNewServer() { return isNewServer; } @Override public String getSharedAcrossSubdomainsOf() { return sharedAcrossSubdomainsOf; } @Override public String getBaseUrlForCrossDomainStorage() { return baseUrlForCrossDomainStorage; } @Override public void registerCustomizer(SecurityInitializationCustomizer customizer) { customizers.add(customizer); customizer.customizeSecurityService(this); } @Override public void updateUserSubscription(String username, Subscription newSubscription) throws UserManagementException { final User user = getUserByName(username); if (user != null) { apply(new UpdateUserSubscriptionOperation(username, newSubscription)); } else { throw new UserManagementException(UserManagementException.USER_DOES_NOT_EXIST); } } @Override public Void internalUpdateSubscription(String username, Subscription newSubscription) throws UserManagementException { final User user = getUserByName(username); if (user != null) { lockSubscriptionsForUser(user); try { final String newSubscriptionPlanId = newSubscription.getPlanId(); final Subscription currentSubscription = user.getSubscriptionByPlan(newSubscriptionPlanId); if (shouldProcessNewSubscription(currentSubscription, newSubscription)) { logger.info(() -> "Update user subscription for plan " + newSubscriptionPlanId); logger.info(() -> "Current user plan subscription: " + (currentSubscription != null ? currentSubscription.toString() : "null")); logger.info(() -> "New plan subscription: " + (newSubscription != null ? newSubscription.toString() : "null")); // In some cases there is no invoice or transaction information. E.g. if the subscription has // been cancelled. // To ensure information about previous payments is preserved, the subscription is patched. if (currentSubscription != null && newSubscription != null && newSubscription.getSubscriptionId() != null && newSubscription.getSubscriptionId().equals(currentSubscription.getSubscriptionId())) { if (currentSubscription.getTransactionStatus() != null && newSubscription.getTransactionStatus() == null) { newSubscription.patchTransactionData(currentSubscription); } if (currentSubscription.getInvoiceId() != null && newSubscription.getInvoiceId() == null) { newSubscription.patchInvoiceData(currentSubscription); } } if (shouldUpdateUserRolesForSubscription(user, currentSubscription, newSubscription)) { updateUserRolesOnSubscriptionChange(user, currentSubscription, newSubscription); } final Subscription[] newSubscriptions = buildNewUserSubscriptions(user, newSubscription); if (newSubscriptions != null) { user.setSubscriptions(newSubscriptions); } store.updateUser(user); } else { logger.info(() -> "New subscription has been ignored: " + newSubscription); } return null; } finally { unlockSubscriptionsForUser(user); } } else { throw new UserManagementException(UserManagementException.USER_DOES_NOT_EXIST); } } /** * Check if new subscription should be processed, such as if it has a valid plan, or it's the most recent * subscription of a plan */ private boolean shouldProcessNewSubscription(Subscription currentSubscription, Subscription newSubscription) { final boolean shouldProcess; if (!newSubscription.hasPlan()) { // New subscription doesn't have plan id, that means it's an empty subscription model which is used for // clearing all user subscriptions shouldProcess = true; } else if (subscriptionPlanProvider.getAllSubscriptionPlans().get(newSubscription.getPlanId()) != null) { if (currentSubscription == null) { // New subscription plan is valid, but current subscription of the plan is empty shouldProcess = true; } else if (!newSubscription.hasSubscriptionId()) { // New subscription has plan id but doesn't have subscription id, this is an empty subscription for the // plan that holds updated dates data for the plan's subscription shouldProcess = newSubscription.isUpdatedMoreRecently(currentSubscription); } else { // Only process new subscription if it is the most recently created subscription shouldProcess = newSubscription.getSubscriptionCreatedAt().asMillis() >= currentSubscription .getSubscriptionCreatedAt().asMillis(); } } else { // New subscription doesn't have a valid plan shouldProcess = false; } return shouldProcess; } /** * Build new subscription list for user from new subscription. This might update a subscription model, or add new * one to user's subscription list. In case no updates for current user subscriptions then null will be returned */ private Subscription[] buildNewUserSubscriptions(User user, Subscription newSubscription) { Subscription[] newUserSubscriptions = null; Iterable subscriptions = user.getSubscriptions(); if (newSubscription != null) { if (subscriptions == null || !subscriptions.iterator().hasNext()) { newUserSubscriptions = new Subscription[] { newSubscription }; } else if (!newSubscription.hasPlan()) { // New subscription has no plan, that means new subscription is just an empty one with some meta data // for updated dates, and user has been deleted from provider. In this case we need to remove all // current subscriptions of the user for this provider List newSubscriptionList = new ArrayList(); for (Subscription subscription : subscriptions) { if (!subscription.getProviderName().equals(newSubscription.getProviderName())) { newSubscriptionList.add(subscription); } } newSubscriptionList.add(newSubscription); newUserSubscriptions = newSubscriptionList.toArray(new Subscription[] {}); } else { List newSubscriptionList = new ArrayList(); boolean foundCurrentSubscription = false; for (Subscription subscription : subscriptions) { if (!foundCurrentSubscription && ((!subscription.hasPlan() && subscription.getProviderName().equals(newSubscription.getProviderName())) || subscription.getPlanId().equals(newSubscription.getPlanId()))) { newSubscriptionList.add(newSubscription); foundCurrentSubscription = true; } else { newSubscriptionList.add(subscription); } } if (!foundCurrentSubscription) { newSubscriptionList.add(newSubscription); } newUserSubscriptions = newSubscriptionList.toArray(new Subscription[] {}); } } return newUserSubscriptions; } /** * Add or remove subscription plan's roles for user */ private void updateUserRolesOnSubscriptionChange(User user, Subscription currentSubscription, Subscription newSubscription) throws UserManagementException { logger.info(() -> "Update user subscription roles for user " + user.getName()); // in case new subscription has no planId, it means the subscription is an empty one with just meta data for // update times, and user doesn't subscribe to any plans, so all plan's roles assigned to the user must be // removed, but only if no other plan that the user still is subscribed to implies an equal role: Map allSubscriptionPlans = subscriptionPlanProvider.getAllSubscriptionPlans(); if (newSubscription != null && !newSubscription.hasPlan()) { Iterable plans = allSubscriptionPlans.values(); for (SubscriptionPlan plan : plans) { removeUserPlanRoles(user, plan, /* checkOverlappingRoles */ true); } } else { assert currentSubscription == null || newSubscription == null || currentSubscription.getPlanId().equals(newSubscription.getPlanId()); if (currentSubscription != null && currentSubscription.hasPlan() && currentSubscription.isActiveSubscription() && newSubscription != null && !newSubscription.isActiveSubscription()) { SubscriptionPlan currentPlan = allSubscriptionPlans.get(currentSubscription.getPlanId()); removeUserPlanRoles(user, currentPlan, /* checkOverlappingRoles */ true); } if (newSubscription != null && newSubscription.hasPlan() && newSubscription.isActiveSubscription()) { SubscriptionPlan newPlan = allSubscriptionPlans.get(newSubscription.getPlanId()); addUserPlanRoles(user, newPlan); } } } /** * Remove user roles implied by a subscription plan * * @param checkOverlappingRoles * true if it needs to check overlapping roles with other active subscription plans of the same user, * otherwise roles will be removed without any overlap check */ private void removeUserPlanRoles(User user, SubscriptionPlan plan, boolean checkOverlappingRoles) throws UserManagementException { if (plan != null) { logger.info(() -> "Remove user roles of subscription plan " + plan.getId()); final Role[] rolesToRemove; if (checkOverlappingRoles) { rolesToRemove = getSubscriptionPlanUserRolesWithoutOverlapping(user, plan); } else { rolesToRemove = getSubscriptionPlanUserRoles(user, plan); } logger.info(() -> "Removing the following roles of subscription plan " + plan.getId()+" from user "+user.getName()+": "+ Arrays.asList(rolesToRemove)); for (Role role : rolesToRemove) { store.removeRoleFromUser(user.getName(), role); } } } private void addUserPlanRoles(User user, SubscriptionPlan plan) throws UserManagementException { if (plan != null) { logger.info(() -> "Add user roles for subscription plan " + plan.getId()); Role[] roles = getSubscriptionPlanUserRoles(user, plan); logger.info(() -> "Adding the following roles of subscription plan " + plan.getId()+" to user "+user.getName()+": "+ Arrays.asList(roles)); for (Role role : roles) { addRoleForUserAndSetUserAsOwner(user, role); } } } @Override public Role[] getSubscriptionPlanUserRoles(User user, SubscriptionPlan plan) { final List roles = new ArrayList(); for (SubscriptionPlanRole planRole : plan.getRoles()) { roles.add(getSubscriptionPlanUserRole(user, planRole)); } return roles.toArray(new Role[] {}); } /** * Get user roles of a plan that are not overlapping with roles of other user active subscription plan */ private Role[] getSubscriptionPlanUserRolesWithoutOverlapping(User user, SubscriptionPlan plan) { final Set otherPlanRoles = new HashSet(); Iterable subscriptions = user.getSubscriptions(); for (Subscription subscription : subscriptions) { if (subscription.isActiveSubscription() && !subscription.getPlanId().equals(plan.getId())) { SubscriptionPlan otherPlan = subscriptionPlanProvider.getAllSubscriptionPlans().get(subscription.getPlanId()); for (SubscriptionPlanRole planRole : otherPlan.getRoles()) { otherPlanRoles.add(getSubscriptionPlanUserRole(user, planRole)); } } } final List roles = new ArrayList(); for (SubscriptionPlanRole planRole : plan.getRoles()) { Role role = getSubscriptionPlanUserRole(user, planRole); if (!otherPlanRoles.contains(role)) { roles.add(role); } } return roles.toArray(new Role[] {}); } /** * Get a role {@code Role} for a subscription plan role definition {@code SubscriptionPlanRole}. * These roles are non transitive, hence they can not be granted to other users. */ private Role getSubscriptionPlanUserRole(User user, SubscriptionPlanRole planRole) { final Role result; final User qualifiedUser = getSubscriptionPlanRoleQualifiedUser(user, planRole); final UserGroup qualifiedTenant = getSubscriptionPlanRoleQualifiedTenant(user, qualifiedUser, planRole); final RoleDefinition roleDefinition = getRoleDefinition(planRole.getRoleId()); if (roleDefinition == null) { logger.severe("Role with ID "+planRole.getRoleId()+" for user "+user.getName()+" not found."); result = null; } else { result = new Role(roleDefinition, qualifiedTenant, qualifiedUser, /* roles acquired through subscription are non-transitive, meaning the user cannot pass them on */ false); } return result; } /** * Check and return role qualified user {@code User} for a subscription plan role {@code SubscriptionPlanRole} * definition. If the {@link SubscriptionPlanRole#getUserQualificationMode() user qualification mode} is set to * {@link SubscriptionPlanRole.UserQualificationMode#SUBSCRIBING_USER} then {@code user} is returned. Else, if the * subscription plan role specifies an {@link SubscriptionPlanRole#getExplicitUserQualification() explicit user * qualification}, its user is returned. Otherwise, this method returns {@code null}. */ private User getSubscriptionPlanRoleQualifiedUser(User user, SubscriptionPlanRole planRole) { final User qualifiedUser; if (planRole.getUserQualificationMode() != null && planRole.getUserQualificationMode() == SubscriptionPlanRole.UserQualificationMode.SUBSCRIBING_USER) { qualifiedUser = user; } else if (planRole.getExplicitUserQualification() != null && !planRole.getExplicitUserQualification().isEmpty()) { qualifiedUser = getUserByName(planRole.getExplicitUserQualification()); } else { qualifiedUser = null; } return qualifiedUser; } /** * Check and return role qualified tenant {@code UserGroup} for a subscription plan role * {@code SubscriptionPlanRole} definition. * * @param qualifiedUser * qualified user from * {@code SecurityServiceImpl#getSubscriptionPlanRoleQualifiedUser(User, SubscriptionPlanRole)} */ private UserGroup getSubscriptionPlanRoleQualifiedTenant(User subscriptionUser, User qualifiedUser, SubscriptionPlanRole planRole) { final UserGroup qualifiedForGroup; final SubscriptionPlanRole.GroupQualificationMode groupQualificationMode = planRole.getGroupQualificationMode(); if (groupQualificationMode != null && groupQualificationMode != SubscriptionPlanRole.GroupQualificationMode.NONE) { final User u; switch (groupQualificationMode) { case DEFAULT_QUALIFIED_USER_TENANT: if (qualifiedUser != null) { u = qualifiedUser; } else { u = null; } break; case SUBSCRIBING_USER_DEFAULT_TENANT: if (subscriptionUser != null) { u = subscriptionUser; } else { u = null; } break; default: u = null; break; } if (u != null) { // don't use the default tenant but the default tenant name to resolve the user's own default group; // example: the user may have set a default object creation group (e.g., "kielerwoche2020-server") for // the current server that is different from the user's own default group ("{username}-tenant"). Yet, // when assigning a role based on a subscription, the "default tenant/group" has to be the user's own // group, not the current object creation group that the user has currently set. Otherwise, those role // assignments would be specific to a server which they shall not. qualifiedForGroup = getUserGroupByName(getDefaultTenantNameForUsername(u.getName())); } else { qualifiedForGroup = null; } } else if (planRole.getIdOfExplicitGroupQualification() != null) { qualifiedForGroup = getUserGroup(planRole.getIdOfExplicitGroupQualification()); } else { qualifiedForGroup = null; } return qualifiedForGroup; } /** * Role assignments that are based on plans subscribed need to be adjusted if the * {@link Subscription#isActiveSubscription() is-active} status of the user's subscription has changed. */ private boolean shouldUpdateUserRolesForSubscription(User user, Subscription currentSubscription, Subscription newSubscription) { final boolean result; if (currentSubscription == null && newSubscription == null) { result = false; } else if (newSubscription == null) { // In case new subscription is null, user's subscriptions won't be changed result = false; } else if (!newSubscription.hasPlan()) { // New subscription doesn't have plan id, that means it's an empty subscription model which is used for // clearing all user subscriptions boolean isUserInPossessionOfRoles = false; for (SubscriptionPlan subscriptionPlan : getAllSubscriptionPlans().values()) { isUserInPossessionOfRoles = subscriptionPlan.isUserInPossessionOfRoles(user); if(isUserInPossessionOfRoles) { break; } } result = isUserInPossessionOfRoles; } else if (currentSubscription == null || currentSubscription.getPlanId() == null) { // A case when there's no current subscription for a plan, if the plan's new subscription is active then // user roles need to be updated with granted new roles. Further, if the user is // somehow in possession of roles he should not posess, the roles must be removed final SubscriptionPlan subscriptionPlanById = getSubscriptionPlanById(newSubscription.getPlanId()); if (subscriptionPlanById == null) { result = false; } else { result = newSubscription.isActiveSubscription() || subscriptionPlanById.isUserInPossessionOfRoles(user); } } else { assert currentSubscription.getPlanId().equals(newSubscription.getPlanId()); // in this case user roles will be needed to update only when subscription active status is changed if (newSubscription.isActiveSubscription() != currentSubscription.isActiveSubscription()) { result = true; } else { final SubscriptionPlan subscriptionPlanById = getSubscriptionPlanById(newSubscription.getPlanId()); result = !subscriptionPlanById.isUserInPossessionOfRoles(user); } } return result; } @Override public void addPermissionChangeListener(WildcardPermission permission, PermissionChangeListener listener) { permissionChangeListeners.addPermissionChangeListener(permission, listener); } @Override public void removePermissionChangeListener(WildcardPermission permission, PermissionChangeListener listener) { permissionChangeListeners.removePermissionChangeListener(permission, listener); } @Override public void updateSubscriptionPlanPrices(Map itemPrices) { apply(new UpdateItemPriceOperation(itemPrices)); } @Override public Void internalUpdateSubscriptionPlanPrices(Map updatedItemPrices) { final Map allSubscriptionPlans = getAllSubscriptionPlans(); for (SubscriptionPlan subscriptionPlan : allSubscriptionPlans.values()) { for (SubscriptionPrice subscriptionPrice : subscriptionPlan.getPrices()) { final BigDecimal updatedPrice = updatedItemPrices.get(subscriptionPrice.getPriceId()); if (updatedPrice != null) { logger.log(Level.INFO, "Setting ItemPrice for SubscriptionPrice " + subscriptionPrice.getPriceId()); subscriptionPrice.setPrice(updatedPrice); } } } return null; } @Override public Role createRoleFromIDs(UUID roleDefinitionId, UUID qualifyingTenantId, String qualifyingUsername, boolean transitive) throws UserManagementException { final User user; if (qualifyingUsername == null || qualifyingUsername.trim().isEmpty()) { user = null; } else { user = getUserByName(qualifyingUsername); if (user == null) { throw new UserManagementException("User "+qualifyingUsername+" not found for role qualification"); } } final UserGroup group; if (qualifyingTenantId == null) { group = null; } else { group = getUserGroup(qualifyingTenantId); if (group == null) { throw new UserManagementException("Group with ID "+qualifyingTenantId+" not found for role qualification"); } } final RoleDefinition roleDefinition = getRoleDefinition(roleDefinitionId); if (roleDefinition == null) { throw new UserManagementException("Role definition with ID "+roleDefinitionId+" not found"); } return new Role(roleDefinition, group, user, transitive); } /** * @return the role associated with the given IDs and qualifiers * @throws UserManagementException * if the current user does not have the meta permission to give this specific, qualified role in this * context. */ @Override public Role getOrThrowRoleFromIDsAndCheckMetaPermissions(UUID roleDefinitionId, UUID tenantId, String userQualifierName, boolean transitive) throws UserManagementException { final Role role = createRoleFromIDs(roleDefinitionId, tenantId, userQualifierName, transitive); if (!hasCurrentUserMetaPermissionsOfRoleDefinitionWithQualification( role.getRoleDefinition(), role.getQualificationAsOwnership())) { throw new UserManagementException("You are not allowed to take this role to the user."); } return role; } @Override public void lockSubscriptionsForUser(final User user) { LockUtil.lockForWrite(subscriptionLocksForUsers.computeIfAbsent(user, u->new NamedReentrantReadWriteLock("Subscriptions lock for user "+user.getName(), /* fair */ false))); } @Override public void unlockSubscriptionsForUser(final User user) { LockUtil.unlockAfterWrite(subscriptionLocksForUsers.computeIfAbsent(user, u->new NamedReentrantReadWriteLock("Subscriptions lock for user "+user.getName(), /* fair */ false))); } @Override public void fileTakedownNotice(TakedownNoticeRequestContext takedownNoticeRequestContext) throws MailException { final String SUPPORT_MAIL_ADDRESS = "support@sapsailing.com"; final User user = getUserByName(takedownNoticeRequestContext.getUsername()); final String email = user.getEmail(); final StringBuilder sb = new StringBuilder() .append("User ") .append(takedownNoticeRequestContext.getUsername()) .append(" with e-mail ") .append(email) .append(" requests that the media with URL ") .append(takedownNoticeRequestContext.getContentUrl()) .append(" used in context ") .append(messages.get(Locale.ENGLISH, takedownNoticeRequestContext.getContextDescriptionMessageKey(), takedownNoticeRequestContext.getContextDescriptionMessageParameter())) .append(" on page ") .append(takedownNoticeRequestContext.getPageUrl()) .append(" be removed from the site. The user provides the following comment:\n\n") .append(" \"") .append(takedownNoticeRequestContext.getReportingUserComment()) .append("\"\n\n") .append("The claim is of nature ") .append(takedownNoticeRequestContext.getNatureOfClaim()) .append("."); if (!Util.isEmpty(takedownNoticeRequestContext.getSupportingURLs())) { sb.append("\n\nThe user provided the following additional URLs to substantiate or prove the claim:\n"); for (final String url : takedownNoticeRequestContext.getSupportingURLs()) { sb.append(" - "); sb.append(url); sb.append("\n"); } } final String message = sb.toString(); getMailService().sendMail(SUPPORT_MAIL_ADDRESS, "Media Take-Down Request", message); getMailService().sendMail(email, "Media Take-Down Request Confirmation", messages.get(user.getLocaleOrDefault(), "takedownRequestConfirmation", Util.hasLength(user.getFullName()) ? user.getFullName() : user.getName(), SUPPORT_MAIL_ADDRESS, message)); } /** * For a {@link SecuredSecurityTypes#SERVER SERVER} object identified by {@code serverName}, determines the user set * as the server's owner, plus additional users that have the permission to execute * {@code alsoSendToAllUsersWithThisPermissionOnReplicaSet} on that server. * * @param serverName * identifies the server object; for the local server that would, e.g., be {@link ServerInfo#getName()}. * For replica sets, this is the name of the replica set. * @param alsoSendToAllUsersWithThisPermissionOnReplicaSet * when not empty, all users that have permission to this {@link SecuredSecurityTypes#SERVER SERVER} * action on the {@code replicaSet} will receive the e-mail in addition to the server owner. No user will * receive the e-mail twice. * @return */ @Override public Iterable getUsersToInformAboutReplicaSet(String serverName, Optional alsoSendToAllUsersWithThisPermissionOnReplicaSet) { final QualifiedObjectIdentifier serverIdentifier = SecuredSecurityTypes.SERVER.getQualifiedObjectIdentifier(new TypeRelativeObjectIdentifier(serverName)); final OwnershipAnnotation serverOwnership = getOwnership(serverIdentifier); final User serverOwner; final Set usersToSendMailTo = new HashSet<>(); if (serverOwnership != null && serverOwnership.getAnnotation() != null && (serverOwner = serverOwnership.getAnnotation().getUserOwner()) != null) { usersToSendMailTo.add(serverOwner); } alsoSendToAllUsersWithThisPermissionOnReplicaSet.ifPresent( serverAction -> getUsersWithPermissions(serverIdentifier.getPermission(serverAction)) .forEach(usersToSendMailTo::add)); return usersToSendMailTo; } }