PageRenderTime 56ms CodeModel.GetById 20ms RepoModel.GetById 1ms app.codeStats 0ms

/src/main/java/org/nrg/xnat/services/cache/DefaultGroupsAndPermissionsCache.java

https://bitbucket.org/radiologics/xnat-web-old-v2
Java | 2002 lines | 1631 code | 227 blank | 144 comment | 214 complexity | cd0aae515f9ce3d9be056923c204fa8b MD5 | raw file

Large files files are truncated, but you can click here to view the full file

  1. /*
  2. * web: org.nrg.xnat.services.cache.DefaultUserProjectCache
  3. * XNAT http://www.xnat.org
  4. * Copyright (c) 2017, Washington University School of Medicine
  5. * All Rights Reserved
  6. *
  7. * Released under the Simplified BSD.
  8. */
  9. package org.nrg.xnat.services.cache;
  10. import com.google.common.base.Function;
  11. import com.google.common.base.Joiner;
  12. import com.google.common.base.Predicate;
  13. import com.google.common.base.Predicates;
  14. import com.google.common.collect.*;
  15. import lombok.extern.slf4j.Slf4j;
  16. import net.sf.ehcache.CacheException;
  17. import net.sf.ehcache.Ehcache;
  18. import net.sf.ehcache.Element;
  19. import org.apache.commons.collections.CollectionUtils;
  20. import org.apache.commons.lang3.StringUtils;
  21. import org.apache.commons.lang3.time.DurationFormatUtils;
  22. import org.nrg.framework.exceptions.NrgServiceRuntimeException;
  23. import org.nrg.framework.orm.DatabaseHelper;
  24. import org.nrg.framework.utilities.LapStopWatch;
  25. import org.nrg.xdat.XDAT;
  26. import org.nrg.xdat.display.ElementDisplay;
  27. import org.nrg.xdat.om.*;
  28. import org.nrg.xdat.schema.SchemaElement;
  29. import org.nrg.xdat.security.SecurityManager;
  30. import org.nrg.xdat.security.*;
  31. import org.nrg.xdat.security.helpers.Permissions;
  32. import org.nrg.xdat.security.helpers.Users;
  33. import org.nrg.xdat.security.user.exceptions.UserInitException;
  34. import org.nrg.xdat.security.user.exceptions.UserNotFoundException;
  35. import org.nrg.xdat.services.Initializing;
  36. import org.nrg.xdat.services.cache.GroupsAndPermissionsCache;
  37. import org.nrg.xdat.servlet.XDATServlet;
  38. import org.nrg.xft.db.PoolDBUtils;
  39. import org.nrg.xft.event.XftItemEventI;
  40. import org.nrg.xft.event.methods.XftItemEventCriteria;
  41. import org.nrg.xft.exception.ElementNotFoundException;
  42. import org.nrg.xft.exception.FieldNotFoundException;
  43. import org.nrg.xft.exception.ItemNotFoundException;
  44. import org.nrg.xft.exception.XFTInitException;
  45. import org.nrg.xft.schema.XFTManager;
  46. import org.nrg.xft.security.UserI;
  47. import org.nrg.xnat.services.cache.jms.InitializeGroupRequest;
  48. import org.slf4j.event.Level;
  49. import org.springframework.beans.factory.annotation.Autowired;
  50. import org.springframework.cache.CacheManager;
  51. import org.springframework.dao.DataAccessException;
  52. import org.springframework.jdbc.core.JdbcTemplate;
  53. import org.springframework.jdbc.core.ResultSetExtractor;
  54. import org.springframework.jdbc.core.RowCallbackHandler;
  55. import org.springframework.jdbc.core.namedparam.EmptySqlParameterSource;
  56. import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
  57. import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
  58. import org.springframework.jms.core.JmsTemplate;
  59. import org.springframework.scheduling.annotation.Async;
  60. import org.springframework.scheduling.annotation.AsyncResult;
  61. import org.springframework.stereotype.Service;
  62. import javax.annotation.Nonnull;
  63. import javax.annotation.Nullable;
  64. import java.sql.ResultSet;
  65. import java.sql.SQLException;
  66. import java.text.DateFormat;
  67. import java.text.NumberFormat;
  68. import java.util.*;
  69. import java.util.concurrent.ConcurrentHashMap;
  70. import java.util.concurrent.Future;
  71. import java.util.concurrent.atomic.AtomicBoolean;
  72. import java.util.regex.Matcher;
  73. import java.util.regex.Pattern;
  74. import static org.nrg.framework.exceptions.NrgServiceError.ConfigurationError;
  75. import static org.nrg.xapi.rest.users.DataAccessApi.*;
  76. import static org.nrg.xdat.security.PermissionCriteria.dumpCriteriaList;
  77. import static org.nrg.xdat.security.helpers.Groups.*;
  78. import static org.nrg.xft.event.XftItemEventI.*;
  79. @SuppressWarnings("Duplicates")
  80. @Service
  81. @Slf4j
  82. public class DefaultGroupsAndPermissionsCache extends AbstractXftItemAndCacheEventHandlerMethod implements GroupsAndPermissionsCache, Initializing, GroupsAndPermissionsCache.Provider {
  83. @Autowired
  84. public DefaultGroupsAndPermissionsCache(final CacheManager cacheManager, final NamedParameterJdbcTemplate template, final JmsTemplate jmsTemplate) throws SQLException {
  85. super(cacheManager,
  86. XftItemEventCriteria.builder().xsiType(XnatProjectdata.SCHEMA_ELEMENT_NAME).actions(CREATE, UPDATE, DELETE).build(),
  87. XftItemEventCriteria.builder().xsiType(XnatSubjectdata.SCHEMA_ELEMENT_NAME).xsiType(XnatExperimentdata.SCHEMA_ELEMENT_NAME).actions(CREATE, XftItemEventI.DELETE, SHARE).build(),
  88. XftItemEventCriteria.getXsiTypeCriteria(XdatUsergroup.SCHEMA_ELEMENT_NAME),
  89. XftItemEventCriteria.getXsiTypeCriteria(XdatElementSecurity.SCHEMA_ELEMENT_NAME));
  90. _template = template;
  91. _jmsTemplate = jmsTemplate;
  92. _helper = new DatabaseHelper((JdbcTemplate) _template.getJdbcOperations());
  93. _totalCounts = new HashMap<>();
  94. _missingElements = new HashMap<>();
  95. _userChecks = new ConcurrentHashMap<>();
  96. _initialized = new AtomicBoolean(false);
  97. if (_helper.tableExists("xnat_projectdata")) {
  98. resetTotalCounts();
  99. }
  100. }
  101. /**
  102. * {@inheritDoc}
  103. */
  104. @Override
  105. public void notifyElementRemoved(final Ehcache cache, final Element element) throws CacheException {
  106. handleCacheRemoveEvent(cache, element, "removed");
  107. }
  108. /**
  109. * {@inheritDoc}
  110. */
  111. @Override
  112. public void notifyElementExpired(final Ehcache cache, final Element element) {
  113. handleCacheRemoveEvent(cache, element, "expired");
  114. }
  115. /**
  116. * {@inheritDoc}
  117. */
  118. @Override
  119. public void notifyElementEvicted(final Ehcache cache, final Element element) {
  120. handleCacheRemoveEvent(cache, element, "evicted");
  121. }
  122. /**
  123. * {@inheritDoc}
  124. */
  125. @Override
  126. public void notifyRemoveAll(final Ehcache cache) {
  127. handleCacheRemoveEvent(cache, null, "removed");
  128. }
  129. /**
  130. * {@inheritDoc}
  131. */
  132. @Override
  133. public String getCacheName() {
  134. return CACHE_NAME;
  135. }
  136. /**
  137. * Gets the specified project if the user has any access to it. Returns null otherwise.
  138. *
  139. * @param groupId The ID or alias of the project to retrieve.
  140. *
  141. * @return The project object if the user can access it, null otherwise.
  142. */
  143. @Override
  144. public UserGroupI get(final String groupId) {
  145. if (StringUtils.isBlank(groupId)) {
  146. throw new IllegalArgumentException("Can not request a group with a blank group ID");
  147. }
  148. // Check that the group is cached and, if so, return it.
  149. log.trace("Retrieving group through cache ID {}", groupId);
  150. final UserGroupI cachedGroup = getCachedGroup(groupId);
  151. if (cachedGroup != null) {
  152. log.debug("Found cached group entry for cache ID '{}'", groupId);
  153. return cachedGroup;
  154. }
  155. try {
  156. log.trace("Initializing group entry for cache ID '{}'", groupId);
  157. return initGroup(groupId);
  158. } catch (ItemNotFoundException e) {
  159. log.info("Can't find a group with the group ID '{}', returning null", groupId);
  160. return null;
  161. }
  162. }
  163. /**
  164. * {@inheritDoc}
  165. */
  166. @Override
  167. public Map<String, Long> getReadableCounts(final UserI user) {
  168. if (user == null) {
  169. return Collections.emptyMap();
  170. }
  171. final String username = user.getUsername();
  172. final String cacheId = getCacheIdForUserElements(username, READABLE);
  173. // Check whether the element types are cached and, if so, return that.
  174. log.trace("Retrieving readable counts for user {} through cache ID {}", username, cacheId);
  175. final Map<String, Long> cachedReadableCounts = getCachedMap(cacheId);
  176. if (cachedReadableCounts != null) {
  177. log.debug("Found cached readable counts entry for user '{}' with cache ID '{}' containing {} entries", username, cacheId, cachedReadableCounts.size());
  178. return cachedReadableCounts;
  179. }
  180. return initReadableCountsForUser(cacheId, user);
  181. }
  182. /**
  183. * {@inheritDoc}
  184. */
  185. @Override
  186. public Map<String, ElementDisplay> getBrowseableElementDisplays(final UserI user) {
  187. if (user == null) {
  188. return Collections.emptyMap();
  189. }
  190. final Map<String, ElementDisplay> guestBrowseableElementDisplays = getGuestBrowseableElementDisplays();
  191. if (user.isGuest()) {
  192. log.debug("Got a request for browseable element displays for the guest user, returning {} entries", guestBrowseableElementDisplays.size());
  193. return guestBrowseableElementDisplays;
  194. }
  195. final String username = user.getUsername();
  196. final String cacheId = getCacheIdForUserElements(username, BROWSEABLE);
  197. // Check whether the element types are cached and, if so, return that.
  198. log.trace("Retrieving browseable element displays for user {} through cache ID {}", username, cacheId);
  199. final Map<String, ElementDisplay> cachedUserEntry = getCachedMap(cacheId);
  200. if (cachedUserEntry != null) {
  201. @SuppressWarnings("unchecked") final Map<String, ElementDisplay> browseables = buildImmutableMap(cachedUserEntry, guestBrowseableElementDisplays);
  202. log.debug("Found a cached entry for user '{}' browseable element displays under cache ID '{}' with {} entries", username, cacheId, browseables.size());
  203. return browseables;
  204. }
  205. log.trace("Initializing browseable element displays for user '{}' with cache ID '{}'", username, cacheId);
  206. @SuppressWarnings("unchecked") final Map<String, ElementDisplay> browseables = buildImmutableMap(initBrowseableElementDisplaysForUser(cacheId, user), guestBrowseableElementDisplays);
  207. log.debug("Initialized browseable element displays for user '{}' with cache ID '{}' with {} entries (including guest browseable element displays)", username, cacheId, browseables.size());
  208. return browseables;
  209. }
  210. /**
  211. * {@inheritDoc}
  212. */
  213. @Override
  214. public List<ElementDisplay> getSearchableElementDisplays(final UserI user) {
  215. if (user == null) {
  216. return Collections.emptyList();
  217. }
  218. final String username = user.getUsername();
  219. final String cacheId = getCacheIdForUserElements(username, SEARCHABLE);
  220. log.debug("Retrieving searchable element displays for user {} through cache ID {}", username, cacheId);
  221. final Map<String, Long> counts = getReadableCounts(user);
  222. try {
  223. return Lists.newArrayList(Iterables.filter(getActionElementDisplays(username, SecurityManager.READ), new Predicate<ElementDisplay>() {
  224. @Override
  225. public boolean apply(@Nullable final ElementDisplay elementDisplay) {
  226. if (elementDisplay == null) {
  227. return false;
  228. }
  229. final String elementName = elementDisplay.getElementName();
  230. try {
  231. return ElementSecurity.IsSearchable(elementName) && counts.containsKey(elementName) && counts.get(elementName) > 0;
  232. } catch (Exception e) {
  233. return false;
  234. }
  235. }
  236. }));
  237. } catch (Exception e) {
  238. log.error("An unknown error occurred", e);
  239. }
  240. return Collections.emptyList();
  241. }
  242. /**
  243. * {@inheritDoc}
  244. */
  245. @Override
  246. public List<ElementDisplay> getActionElementDisplays(final UserI user, final String action) {
  247. return getActionElementDisplays(user.getUsername(), action);
  248. }
  249. @Override
  250. public List<ElementDisplay> getActionElementDisplays(final String username, final String action) {
  251. if (!ACTIONS.contains(action)) {
  252. throw new NrgServiceRuntimeException(ConfigurationError, "The action '" + action + "' is invalid, must be one of: " + StringUtils.join(ACTIONS, ", "));
  253. }
  254. final List<ElementDisplay> elementDisplays = getActionElementDisplays(username).get(action);
  255. if (log.isTraceEnabled()) {
  256. log.trace("Found {} element displays for user {} action {}: {}", elementDisplays.size(), username, action, formatElementDisplays(elementDisplays));
  257. }
  258. return elementDisplays;
  259. }
  260. /**
  261. * {@inheritDoc}
  262. */
  263. @Override
  264. public List<PermissionCriteriaI> getPermissionCriteria(final UserI user, final String dataType) {
  265. return getPermissionCriteria(user.getUsername(), dataType);
  266. }
  267. /**
  268. * {@inheritDoc}
  269. */
  270. @Override
  271. public List<PermissionCriteriaI> getPermissionCriteria(final String username, final String dataType) {
  272. try {
  273. PoolDBUtils.CheckSpecialSQLChars(dataType);
  274. } catch (Exception e) {
  275. return null;
  276. }
  277. try {
  278. final List<PermissionCriteriaI> criteria = new ArrayList<>();
  279. final Map<String, ElementAccessManager> managers = getElementAccessManagers(username);
  280. if (managers.isEmpty()) {
  281. log.info("Couldn't find element access managers for user {} trying to retrieve permissions for data type {}", username, dataType);
  282. } else {
  283. final ElementAccessManager manager = managers.get(dataType);
  284. if (manager == null) {
  285. log.info("Couldn't find element access manager for data type {} for user {} while trying to retrieve permissions ", dataType, username);
  286. } else {
  287. criteria.addAll(manager.getCriteria());
  288. if (criteria.isEmpty()) {
  289. log.debug("Couldn't find any permission criteria for data type {} for user {} while trying to retrieve permissions ", dataType, username);
  290. }
  291. }
  292. }
  293. final Map<String, UserGroupI> userGroups = getMutableGroupsForUser(username);
  294. if (log.isDebugEnabled()) {
  295. log.debug("Found {} user groups for the user {}", userGroups.size(), username, userGroups.isEmpty() ? "" : ": " + Joiner.on(", ").join(userGroups.keySet()));
  296. }
  297. final Set<String> groups = userGroups.keySet();
  298. if (CollectionUtils.containsAny(groups, ALL_DATA_GROUPS)) {
  299. if (groups.contains(ALL_DATA_ADMIN_GROUP)) {
  300. _template.query(QUERY_GET_ALL_MEMBER_GROUPS, new RowCallbackHandler() {
  301. @Override
  302. public void processRow(final ResultSet resultSet) throws SQLException {
  303. final String projectId = resultSet.getString("project_id");
  304. final String groupId = resultSet.getString("group_id");
  305. // If the user is a collaborator on a project, we're going to upgrade them to member,
  306. // so remove that collaborator nonsense, this is the big time.
  307. userGroups.remove(projectId + "_collaborator");
  308. // If the user is already a member of owner of a project, then don't bother: they already have
  309. // sufficient access to the project.
  310. if (!userGroups.containsKey(projectId + "_owner") && !userGroups.containsKey(projectId + "_member")) {
  311. userGroups.put(groupId, get(groupId));
  312. }
  313. }
  314. });
  315. } else if (userGroups.containsKey(ALL_DATA_ACCESS_GROUP)) {
  316. _template.query(QUERY_GET_ALL_COLLAB_GROUPS, new RowCallbackHandler() {
  317. @Override
  318. public void processRow(final ResultSet resultSet) throws SQLException {
  319. final String projectId = resultSet.getString("project_id");
  320. final String groupId = resultSet.getString("group_id");
  321. // If the user has no group membership, then add as a collaborator.
  322. if (!CollectionUtils.containsAny(groups, Arrays.asList(groupId, projectId + "_member", projectId + "_owner"))) {
  323. userGroups.put(groupId, get(groupId));
  324. }
  325. }
  326. });
  327. }
  328. }
  329. for (final UserGroupI group : userGroups.values()) {
  330. final List<PermissionCriteriaI> permissions = group.getPermissionsByDataType(dataType);
  331. if (permissions != null) {
  332. if (log.isTraceEnabled()) {
  333. log.trace("Searched for permission criteria for user {} on type {} in group {}: {}", username, dataType, group.getId(), dumpCriteriaList(permissions));
  334. } else {
  335. log.debug("Searched for permission criteria for user {} on type {} in group {}: {} permissions found", username, dataType, group.getId(), permissions.size());
  336. }
  337. criteria.addAll(permissions);
  338. } else {
  339. log.warn("Tried to retrieve permissions for data type {} for user {} in group {}, but this returned null.", dataType, username, group.getId());
  340. }
  341. }
  342. if (!isGuest(username)) {
  343. try {
  344. final List<PermissionCriteriaI> permissions = getPermissionCriteria(getGuest().getUsername(), dataType);
  345. if (permissions != null) {
  346. criteria.addAll(permissions);
  347. } else {
  348. log.warn("Tried to retrieve permissions for data type {} for the guest user, but this returned null.", dataType);
  349. }
  350. } catch (Exception e) {
  351. log.error("An error occurred trying to retrieve the guest user", e);
  352. }
  353. }
  354. if (log.isTraceEnabled()) {
  355. log.trace("Retrieved permission criteria for user {} on the data type {}: {}", username, dataType, dumpCriteriaList(criteria));
  356. } else {
  357. log.debug("Retrieved permission criteria for user {} on the data type {}: {} criteria found", username, dataType, criteria.size());
  358. }
  359. return ImmutableList.copyOf(criteria);
  360. } catch (UserNotFoundException e) {
  361. log.error("Couldn't find the indicated user");
  362. return Collections.emptyList();
  363. }
  364. }
  365. @Override
  366. public Map<String, Long> getTotalCounts() {
  367. if (_totalCounts.isEmpty()) {
  368. resetTotalCounts();
  369. }
  370. return ImmutableMap.copyOf(_totalCounts);
  371. }
  372. @Nonnull
  373. @Override
  374. public List<String> getProjectsForUser(final String username, final String access) {
  375. log.info("Getting projects with {} access for user {}", access, username);
  376. final String cacheId = getCacheIdForUserProjectAccess(username, access);
  377. final List<String> cachedUserProjects = getCachedList(cacheId);
  378. if (cachedUserProjects != null) {
  379. log.debug("Found a cache entry for user '{}' '{}' access with ID '{}' and {} elements", username, access, cacheId, cachedUserProjects.size());
  380. return cachedUserProjects;
  381. }
  382. return updateUserProjectAccess(username, access, cacheId);
  383. }
  384. /**
  385. * {@inheritDoc}
  386. */
  387. @Nonnull
  388. @Override
  389. public List<UserGroupI> getGroupsForTag(final String tag) {
  390. // Get the group IDs associated with the tag.
  391. log.info("Getting groups for tag {}", tag);
  392. final List<String> groupIds = getTagGroups(tag);
  393. return getUserGroupList(groupIds);
  394. }
  395. @Nonnull
  396. @Override
  397. public Map<String, UserGroupI> getGroupsForUser(final String username) throws UserNotFoundException {
  398. return ImmutableMap.copyOf(getMutableGroupsForUser(username));
  399. }
  400. /**
  401. * {@inheritDoc}
  402. */
  403. @Override
  404. public void refreshGroupsForUser(final String username) throws UserNotFoundException {
  405. initUserGroupIds(getCacheIdForUserGroups(username), username);
  406. }
  407. /**
  408. * {@inheritDoc}
  409. */
  410. @Override
  411. @Nullable
  412. public UserGroupI getGroupForUserAndTag(final String username, final String tag) throws UserNotFoundException {
  413. final String groupId = _template.query(QUERY_GET_GROUP_FOR_USER_AND_TAG, checkUser(username).addValue("tag", tag), new ResultSetExtractor<String>() {
  414. @Override
  415. public String extractData(final ResultSet results) throws DataAccessException, SQLException {
  416. return results.next() ? results.getString("id") : null;
  417. }
  418. });
  419. return StringUtils.isNotBlank(groupId) ? get(groupId) : null;
  420. }
  421. /**
  422. * {@inheritDoc}
  423. */
  424. @Override
  425. public List<String> getUserIdsForGroup(final String groupId) {
  426. final UserGroupI userGroup = get(groupId);
  427. if (userGroup == null) {
  428. return Collections.emptyList();
  429. }
  430. return ImmutableList.copyOf(userGroup.getUsernames());
  431. }
  432. @Override
  433. public void refreshGroup(final String groupId) throws ItemNotFoundException {
  434. final UserGroupI group = initGroup(groupId);
  435. for (final String username : group.getUsernames()) {
  436. try {
  437. if (!getGroupIdsForUser(username).contains(groupId)) {
  438. refreshGroupsForUser(username);
  439. }
  440. } catch (UserNotFoundException ignored) {
  441. //
  442. }
  443. }
  444. }
  445. @Override
  446. public Date getUserLastUpdateTime(final UserI user) {
  447. return getUserLastUpdateTime(user.getUsername());
  448. }
  449. @Override
  450. public Date getUserLastUpdateTime(final String username) {
  451. try {
  452. @SuppressWarnings("unchecked") final List<String> cacheIds = new ArrayList<>(buildImmutableSet(getGroupIdsForUser(username), getCacheIdsForUsername(username)));
  453. if (cacheIds.isEmpty()) {
  454. return new Date();
  455. }
  456. if (log.isDebugEnabled()) {
  457. log.debug("Found {} cache entries related to user {}: {}", cacheIds.size(), username, StringUtils.join(cacheIds, ", "));
  458. }
  459. final long lastUpdateTime = Collections.max(Lists.transform(cacheIds, new Function<String, Long>() {
  460. @Override
  461. public Long apply(@Nullable final String cacheId) {
  462. final Date lastUpdateTime = getCacheEntryLastUpdateTime(cacheId);
  463. log.trace("User {} cache entry '{}' last updated: {}", username, cacheId, lastUpdateTime == null ? "null" : lastUpdateTime.getTime());
  464. return lastUpdateTime == null ? 0L : lastUpdateTime.getTime();
  465. }
  466. }));
  467. log.debug("Found latest cache entry last updated time for user {}: {}", username, lastUpdateTime);
  468. return new Date(lastUpdateTime);
  469. } catch (UserNotFoundException ignored) {
  470. log.warn("Someone requested the cache entry last updated time for user {} but that user wasn't found", username);
  471. return new Date();
  472. }
  473. }
  474. /**
  475. * Finds all user element cache IDs for the specified user and evicts them from the cache.
  476. *
  477. * @param username The username to be cleared.
  478. */
  479. @Override
  480. public void clearUserCache(final String username) {
  481. final List<String> cacheIds = getCacheIdsForUserElements(username);
  482. if (log.isDebugEnabled()) {
  483. log.debug("Clearing caches for user '{}': {}", username, StringUtils.join(cacheIds, ", "));
  484. }
  485. evict(cacheIds);
  486. }
  487. @Override
  488. public boolean canInitialize() {
  489. try {
  490. if (_listener == null) {
  491. return false;
  492. }
  493. final boolean doesUserGroupTableExists = _helper.tableExists("xdat_usergroup");
  494. final boolean isXftManagerComplete = XFTManager.isComplete();
  495. final boolean isDatabasePopulateOrUpdateCompleted = XDATServlet.isDatabasePopulateOrUpdateCompleted();
  496. log.info("User group table {}, XFTManager initialization completed {}, database populate or updated completed {}", doesUserGroupTableExists, isXftManagerComplete, isDatabasePopulateOrUpdateCompleted);
  497. return doesUserGroupTableExists && isXftManagerComplete && isDatabasePopulateOrUpdateCompleted;
  498. } catch (SQLException e) {
  499. log.info("Got an SQL exception checking for xdat_usergroup table", e);
  500. return false;
  501. }
  502. }
  503. @Async
  504. @Override
  505. public Future<Boolean> initialize() {
  506. final LapStopWatch stopWatch = LapStopWatch.createStarted(log, Level.INFO);
  507. // This clears out any group initialization requests that may be left in the database from earlier starts.
  508. _template.update("DELETE FROM activemq_msgs WHERE container LIKE '%initializeGroupRequest'", EmptySqlParameterSource.INSTANCE);
  509. final int tags = initializeTags();
  510. stopWatch.lap("Processed {} tags", tags);
  511. final List<String> groupIds = _template.queryForList(QUERY_ALL_GROUPS, EmptySqlParameterSource.INSTANCE, String.class);
  512. _listener.setGroupIds(groupIds);
  513. stopWatch.lap("Initialized listener of type {} with {} tags", _listener.getClass().getName(), tags);
  514. try {
  515. final UserI adminUser = Users.getAdminUser();
  516. assert adminUser != null;
  517. stopWatch.lap("Found {} group IDs to run through, initializing cache with these as user {}", groupIds.size(), adminUser.getUsername());
  518. for (final String groupId : groupIds) {
  519. stopWatch.lap(Level.DEBUG, "Creating queue entry for group {}", groupId);
  520. XDAT.sendJmsRequest(_jmsTemplate, new InitializeGroupRequest(groupId));
  521. }
  522. } finally {
  523. if (stopWatch.isStarted()) {
  524. stopWatch.stop();
  525. }
  526. log.info("Total time to queue {} groups was {} ms", groupIds.size(), NUMBER_FORMAT.format(stopWatch.getTime()));
  527. if (log.isInfoEnabled()) {
  528. log.info(stopWatch.toTable());
  529. }
  530. }
  531. resetGuestBrowseableElementDisplays();
  532. _initialized.set(true);
  533. return new AsyncResult<>(true);
  534. }
  535. @Override
  536. public boolean isInitialized() {
  537. return _initialized.get();
  538. }
  539. @Override
  540. public Map<String, String> getInitializationStatus() {
  541. final Map<String, String> status = new HashMap<>();
  542. if (_listener == null) {
  543. status.put("message", "No listener registered, so no status to report.");
  544. return status;
  545. }
  546. final Set<String> processed = _listener.getProcessed();
  547. final int processedCount = processed.size();
  548. final Set<String> unprocessed = _listener.getUnprocessed();
  549. final Date start = _listener.getStart();
  550. status.put("start", DATE_FORMAT.format(start));
  551. status.put("processedCount", Integer.toString(processedCount));
  552. status.put("processed", StringUtils.join(processed, ", "));
  553. if (unprocessed.isEmpty()) {
  554. final Date completed = _listener.getCompleted();
  555. final String duration = DurationFormatUtils.formatPeriodISO(start.getTime(), completed.getTime());
  556. status.put("completed", DATE_FORMAT.format(completed));
  557. status.put("duration", duration);
  558. status.put("message", "Cache initialization is complete. Processed " + processedCount + " groups in " + duration);
  559. return status;
  560. }
  561. final Date now = new Date();
  562. final String duration = DurationFormatUtils.formatPeriodISO(start.getTime(), now.getTime());
  563. final int unprocessedCount = unprocessed.size();
  564. status.put("unprocessedCount", Integer.toString(unprocessedCount));
  565. status.put("unprocessed", StringUtils.join(unprocessed, ", "));
  566. status.put("current", DATE_FORMAT.format(now));
  567. status.put("duration", duration);
  568. status.put("message", "Cache initialization is on-going, with " + processedCount + " groups processed and " + unprocessedCount + " groups remaining, time elapsed so far is " + duration);
  569. return status;
  570. }
  571. @Override
  572. public void registerListener(final Listener listener) {
  573. _listener = listener;
  574. }
  575. @Override
  576. public Listener getListener() {
  577. return _listener;
  578. }
  579. @Override
  580. protected boolean handleEventImpl(final XftItemEventI event) {
  581. switch (event.getXsiType()) {
  582. case XnatProjectdata.SCHEMA_ELEMENT_NAME:
  583. return handleProjectEvents(event);
  584. case XnatSubjectdata.SCHEMA_ELEMENT_NAME:
  585. return handleSubjectEvents(event);
  586. case XdatUsergroup.SCHEMA_ELEMENT_NAME:
  587. return handleGroupRelatedEvents(event);
  588. case XdatElementSecurity.SCHEMA_ELEMENT_NAME:
  589. return handleElementSecurityEvents(event);
  590. default:
  591. // This is always some type of experiment.
  592. return handleExperimentEvents(event);
  593. }
  594. }
  595. private boolean handleProjectEvents(final XftItemEventI event) {
  596. final String xsiType = event.getXsiType();
  597. final String id = event.getId();
  598. final String action = event.getAction();
  599. final Map<String, ?> properties = event.getProperties();
  600. try {
  601. switch (action) {
  602. case CREATE:
  603. log.debug("New project created with ID {}, caching new instance", xsiType, id);
  604. for (final String owner : getProjectOwners(id)) {
  605. updateUserProjectAccess(owner);
  606. if (!Iterables.any(getActionElementDisplays(owner).get(SecurityManager.CREATE), CONTAINS_MR_SESSION)) {
  607. initActionElementDisplays(owner, true);
  608. }
  609. }
  610. final boolean created = !initGroups(getGroups(xsiType, id)).isEmpty();
  611. final String access = Permissions.getProjectAccess(_template, id);
  612. if (StringUtils.isNotBlank(access)) {
  613. switch (access) {
  614. case "private":
  615. clearAllDataUserProjectAccess();
  616. break;
  617. case "public":
  618. if (!Iterables.any(getActionElementDisplays(GUEST_USERNAME).get(SecurityManager.READ), CONTAINS_MR_SESSION)) {
  619. initActionElementDisplays(GUEST_USERNAME, true);
  620. }
  621. case "protected":
  622. updateProjectRelatedCaches(xsiType, id, false);
  623. break;
  624. }
  625. }
  626. return created;
  627. case UPDATE:
  628. log.debug("The {} object {} was updated, caching updated instance", xsiType, id);
  629. if (properties.containsKey("accessibility")) {
  630. final String accessibility = (String) properties.get("accessibility");
  631. switch (accessibility) {
  632. case "private":
  633. return updateProjectRelatedCaches(xsiType, id, true);
  634. case "public":
  635. if (!Iterables.any(getActionElementDisplays(GUEST_USERNAME).get(SecurityManager.READ), CONTAINS_MR_SESSION)) {
  636. initActionElementDisplays(GUEST_USERNAME, true);
  637. }
  638. case "protected":
  639. return updateProjectRelatedCaches(xsiType, id, true);
  640. default:
  641. log.warn("The project {}'s accessibility setting was updated to an invalid value: {}. Must be one of private, protected, or public.", id, accessibility);
  642. }
  643. }
  644. break;
  645. case XftItemEventI.DELETE:
  646. log.debug("The {} {} was deleted, removing related instances from cache", xsiType, id);
  647. final String cacheId = getCacheIdForProject(id);
  648. evict(cacheId);
  649. for (final String accessCacheId : Iterables.filter(getCacheIdsForUserElements(), Predicates.contains(REGEX_USER_PROJECT_ACCESS_CACHE_ID))) {
  650. final List<String> projectIds = getCachedList(accessCacheId);
  651. if (projectIds != null && projectIds.contains(id)) {
  652. final List<String> updated = new ArrayList<>(projectIds);
  653. updated.remove(id);
  654. forceCacheObject(accessCacheId, updated);
  655. }
  656. }
  657. resetGuestBrowseableElementDisplays();
  658. initReadableCountsForUsers(this.<String>getCachedSet(cacheId));
  659. resetTotalCounts();
  660. return true;
  661. default:
  662. log.warn("I was informed that the '{}' action happened to the project with ID '{}'. I don't know what to do with this action.", action, xsiType, id);
  663. break;
  664. }
  665. } catch (ItemNotFoundException e) {
  666. log.warn("While handling action {}, I couldn't find a group for type {} ID {}.", action, xsiType, id);
  667. }
  668. return false;
  669. }
  670. private void clearAllDataUserProjectAccess() {
  671. for (final String groupId : ALL_DATA_GROUPS) {
  672. final UserGroupI group = get(groupId);
  673. if (group != null) {
  674. for (final String user : group.getUsernames()) {
  675. for (final String projectAction : ACTIONS) {
  676. evict(getCacheIdForUserProjectAccess(user, projectAction));
  677. }
  678. }
  679. }
  680. }
  681. }
  682. private boolean updateProjectRelatedCaches(final String xsiType, final String id, final boolean affectsOtherDataTypes) throws ItemNotFoundException {
  683. final boolean cachedRelatedGroups = !initGroups(getGroups(xsiType, id)).isEmpty();
  684. evict(GUEST_CACHE_ID);
  685. evict(GUEST_ACTION_READ);
  686. resetGuestBrowseableElementDisplays();
  687. initActionElementDisplays(GUEST_USERNAME, true);
  688. final Set<String> readableCountCacheIds = new HashSet<>(getCacheIdsForUserReadableCounts());
  689. if (affectsOtherDataTypes) {
  690. for (final String cacheId : readableCountCacheIds) {
  691. evict(cacheId);
  692. }
  693. resetTotalCounts();
  694. } else {
  695. // Update existing user element displays
  696. final List<String> cacheIds = getCacheIdsForActions();
  697. cacheIds.addAll(getCacheIdsForUserElements());
  698. clearAllUserProjectAccess();
  699. initReadableCountsForUsers(Sets.newHashSet(Iterables.filter(Lists.transform(cacheIds, FUNCTION_CACHE_IDS_TO_USERNAMES), Predicates.notNull())));
  700. resetProjectCount();
  701. }
  702. return cachedRelatedGroups;
  703. }
  704. private boolean handleSubjectEvents(final XftItemEventI event) {
  705. final String action = event.getAction();
  706. log.debug("Handling subject {} event for {} {}", XftItemEventI.ACTIONS.get(action), event.getXsiType(), event.getId());
  707. final Set<String> projectIds = new HashSet<>();
  708. switch (action) {
  709. case CREATE:
  710. projectIds.add(_template.queryForObject(QUERY_GET_SUBJECT_PROJECT, new MapSqlParameterSource("subjectId", event.getId()), String.class));
  711. resetTotalCounts();
  712. break;
  713. case SHARE:
  714. projectIds.add((String) event.getProperties().get("target"));
  715. break;
  716. case MOVE:
  717. projectIds.add((String) event.getProperties().get("origin"));
  718. projectIds.add((String) event.getProperties().get("target"));
  719. break;
  720. case XftItemEventI.DELETE:
  721. projectIds.add((String) event.getProperties().get("target"));
  722. handleGroupRelatedEvents(event);
  723. resetTotalCounts();
  724. break;
  725. default:
  726. log.warn("I was informed that the '{}' action happened to subject '{}'. I don't know what to do with this action.", action, event.getId());
  727. }
  728. if (projectIds.isEmpty()) {
  729. return false;
  730. }
  731. final Set<String> users = getProjectUsers(projectIds);
  732. for (final String username : users) {
  733. initReadableCountsForUser(username);
  734. }
  735. return true;
  736. }
  737. private boolean handleGroupRelatedEvents(final XftItemEventI event) {
  738. final String xsiType = event.getXsiType();
  739. final String id = event.getId();
  740. final String action = event.getAction();
  741. final Map<String, ?> properties = event.getProperties();
  742. final Set<String> usernames = new HashSet<>();
  743. try {
  744. final List<UserGroupI> groups = getGroups(xsiType, id);
  745. switch (action) {
  746. case CREATE:
  747. log.debug("New {} created with ID {}, caching new instance", xsiType, id);
  748. for (final UserGroupI group : groups) {
  749. usernames.addAll(group.getUsernames());
  750. }
  751. log.debug("Handling create group event with ID '{}' for users: {}", id);
  752. return !initGroups(groups).isEmpty();
  753. case UPDATE:
  754. log.debug("The {} object {} was updated, caching updated instance", xsiType, id);
  755. for (final UserGroupI group : groups) {
  756. usernames.addAll(group.getUsernames());
  757. evict(group.getId());
  758. }
  759. if (properties.containsKey(OPERATION) && StringUtils.equals((String) properties.get(OPERATION), OPERATION_REMOVE_USERS)) {
  760. //noinspection unchecked
  761. usernames.addAll((Collection<? extends String>) properties.get(USERS));
  762. }
  763. log.debug("Handling update group event with ID '{}' for users: {}", id);
  764. return !initGroups(groups).isEmpty();
  765. case XftItemEventI.DELETE:
  766. if (StringUtils.equals(XnatProjectdata.SCHEMA_ELEMENT_NAME, xsiType)) {
  767. final List<String> groupIds = getTagGroups(id);
  768. if (CollectionUtils.isNotEmpty(groupIds)) {
  769. log.info("Found {} groups cached for deleted project {}", groupIds.size(), id);
  770. for (final String groupId : groupIds) {
  771. evictGroup(groupId, usernames);
  772. }
  773. }
  774. } else {
  775. evictGroup(id, usernames);
  776. }
  777. break;
  778. default:
  779. log.warn("I was informed that the '{}' action happened to the {} object with ID '{}'. I don't know what to do with this action.", action, xsiType, id);
  780. }
  781. } catch (ItemNotFoundException e) {
  782. log.warn("While handling action {}, I couldn't find a group for type {} ID {}.", action, xsiType, id);
  783. } finally {
  784. for (final String username : usernames) {
  785. try {
  786. final String cacheId = getCacheIdForUserGroups(username);
  787. initUserGroupIds(cacheId, username);
  788. initReadableCountsForUser(username);
  789. } catch (UserNotFoundException e) {
  790. log.warn("While handling action {} for type {} ID {}, I couldn't find a user with username {}.", action, xsiType, id, username);
  791. }
  792. }
  793. }
  794. return false;
  795. }
  796. private boolean handleElementSecurityEvents(final XftItemEventI event) {
  797. log.debug("Handling {} event for '{}' IDs {}. Updating guest browseable element displays...", event.getAction(), event.getXsiType(), event.getIds());
  798. final Map<String, ElementDisplay> displays = resetGuestBrowseableElementDisplays();
  799. if (log.isTraceEnabled()) {
  800. log.trace("Got back {} browseable element displays for guest user after refresh: {}", displays.size(), StringUtils.join(displays.keySet(), ", "));
  801. }
  802. log.debug("Evicting all action and user element cache IDs");
  803. for (final String cacheId : Iterables.concat(getCacheIdsForActions(), getCacheIdsForUserElements())) {
  804. log.trace("Evicting cache entry with ID '{}'", cacheId);
  805. evict(cacheId);
  806. }
  807. for (final String dataType : event.getIds()) {
  808. final List<String> groupIds = getGroupIdsForDataType(dataType);
  809. log.debug("Found {} groups that reference the '{}' data type, updating cache entries for: {}", groupIds.size(), dataType, StringUtils.join(groupIds, ", "));
  810. for (final String groupId : groupIds) {
  811. log.trace("Evicting group '{}' due to change in element securities for data type {}", groupId, dataType);
  812. evict(groupId);
  813. }
  814. }
  815. return true;
  816. }
  817. private boolean handleExperimentEvents(final XftItemEventI event) {
  818. final String action = event.getAction();
  819. final String xsiType = event.getXsiType();
  820. log.debug("Handling experiment {} event for {} {}", XftItemEventI.ACTIONS.get(action), xsiType, event.getId());
  821. final String target, origin;
  822. switch (action) {
  823. case CREATE:
  824. target = _template.queryForObject(QUERY_GET_EXPERIMENT_PROJECT, new MapSqlParameterSource("experimentId", event.getId()), String.class);
  825. origin = null;
  826. break;
  827. case SHARE:
  828. target = (String) event.getProperties().get("target");
  829. origin = null;
  830. break;
  831. case MOVE:
  832. origin = (String) event.getProperties().get("origin");
  833. target = (String) event.getProperties().get("target");
  834. break;
  835. case XftItemEventI.DELETE:
  836. target = (String) event.getProperties().get("target");
  837. origin = null;
  838. break;
  839. default:
  840. log.warn("I was informed that the '{}' action happened to experiment '{}' with ID '{}'. I don't know what to do with this action.", action, xsiType, event.getId());
  841. return false;
  842. }
  843. final Map<String, ElementDisplay> displays = getGuestBrowseableElementDisplays();
  844. log.debug("Found {} elements for guest user: {}", displays.size(), StringUtils.join(displays.keySet(), ", "));
  845. // If the data type of the experiment isn't in the guest list AND the target project is public,
  846. // OR if the origin project is both specified and public (meaning the data type might be REMOVED
  847. // from the guest browseable element displays), then we update the guest browseable element displays.
  848. final boolean hasEventXsiType = displays.containsKey(xsiType);
  849. final boolean isTargetProjectPublic = Permissions.isProjectPublic(_template, target);
  850. final boolean hasOriginProject = StringUtils.isNotBlank(origin);
  851. final boolean isMovedFromPublicToNon = !isTargetProjectPublic && hasOriginProject && Permissions.isProjectPublic(_template, origin);
  852. // We need to add the XSI type if guest doesn't already have it and the target project is public.
  853. final boolean needsPublicXsiTypeAdded = !hasEventXsiType && isTargetProjectPublic;
  854. // We need to check if the XSI type should be removed if guest has XSI type and item was moved from public to non-public.
  855. final boolean needsXsiTypeChecked = hasEventXsiType && isMovedFromPublicToNon;
  856. if (needsPublicXsiTypeAdded || needsXsiTypeChecked) {
  857. if (needsPublicXsiTypeAdded) {
  858. log.debug("Updating guest browseable element displays: guest doesn't have the event XSI type '{}' and the target project {} is public.", xsiType, target);
  859. } else {
  860. log.debug("Updating guest browseable element displays: guest has the event XSI type '{}' and item was moved from public project {} to non-public project {}.", xsiType, origin, target);
  861. }
  862. resetGuestBrowseableElementDisplays();
  863. } else {
  864. log.debug("Not updating guest browseable element displays: guest {} '{}' and {}",
  865. hasEventXsiType ? "already has the event XSI type " : "doesn't have the event XSI type",
  866. xsiType,
  867. isTargetProjectPublic ? "target project is public" : "target project is not public");
  868. }
  869. initReadableCountsForUsers(hasOriginProject ? getProjectUsers(target) : getProjectUsers(target, origin));
  870. if (StringUtils.equalsAny(action, CREATE, XftItemEventI.DELETE)) {
  871. resetTotalCounts();
  872. }
  873. return true;
  874. }
  875. private void handleCacheRemoveEvent(final Ehcache cache, final Element element, final String event) {
  876. if (isGroupsAndPermissionsCacheEvent(cache)) {
  877. if (element == null) {
  878. log.debug("Got a {} event for cache {}, no specific element affected", event, cache.getName());
  879. return;
  880. }
  881. final Object objectValue = element.getObjectValue();
  882. log.debug("Got a {} event for cache {} on ID {} with value of type {}", event, cache.getName(), element.getObjectKey(), objectValue != null ? objectValue.getClass().getName() : "<null>");
  883. }
  884. }
  885. @SuppressWarnings({"UnusedReturnValue", "SameParameterValue"})
  886. private synchronized List<String> updateUserProjectAccess(final String username) {
  887. final List<String> projectIds = new ArrayList<>();
  888. for (final String access : Arrays.asList(SecurityManager.READ, SecurityManager.EDIT, SecurityManager.DELETE)) {
  889. projectIds.addAll(updateUserProjectAccess(username, access));
  890. }
  891. return projectIds;
  892. }
  893. @SuppressWarnings({"UnusedReturnValue", "SameParameterValue"})
  894. private synchronized List<String> updateUserProjectAccess(final String username, final String access) {
  895. return updateUserProjectAccess(username, access, getCacheIdForUserProjectAccess(username, access));
  896. }
  897. private synchronized List<String> updateUserProjectAccess(final String username, final String access, final String cacheId) {
  898. final List<String> projectIds;
  899. switch (access) {
  900. case SecurityManager.READ:
  901. projectIds = getUserReadableProjects(username);
  902. break;
  903. case SecurityManager.EDIT:
  904. projectIds = getUserEditableProjects(username);
  905. break;
  906. case SecurityManager.DELETE:
  907. projectIds = getUserOwnedProjects(username);
  908. break;
  909. default:
  910. throw new RuntimeException("Unknown access level '" + access + "'. Must be one of " + SecurityManager.READ + ", " + SecurityManager.EDIT + ", or " + SecurityManager.DELETE + ".");
  911. }
  912. cacheObject(cacheId, projectIds);
  913. return ImmutableList.copyOf(projectIds);
  914. }
  915. private List<String> getProjectOwners(final String projectId) {
  916. return _template.queryForList(QUERY_PROJECT_OWNERS, new MapSqlParameterSource("projectId", projectId), String.class);
  917. }
  918. private ListMultimap<String, ElementDisplay> getActionElementDisplays(final String username) {
  919. final String cacheId = getCacheIdForActionElements(username);
  920. // Check whether the action elements are cached and, if so, return that.
  921. final ListMultimap<String, ElementDisplay> cachedActions = getCachedListMultimap(cacheId);
  922. if (cachedActions != null) {
  923. log.debug("Found a cache entry for user '{}' action elements by ID '{}'", username, cacheId);
  924. return cachedActions;
  925. }
  926. return initActionElementDisplays(username);
  927. }
  928. private Long getUserReadableWorkflowCount(final UserI user) {
  929. return _template.queryForObject(QUERY_USER_READABLE_WORKFLOW_COUNT, new MapSqlParameterSource("username", user.getUsername()), Long.class);
  930. }
  931. @Nonnull
  932. private Map<String, UserGroupI> getMutableGroupsForUser(final String username) throws UserNotFoundException {
  933. final List<String> groupIds = getGroupIdsForUser(username);
  934. final Map<String, UserGroupI> groups = new HashMap<>();
  935. for (final String groupId : groupIds) {
  936. final UserGroupI group = get(groupId);
  937. if (group != null) {
  938. log.trace("Adding group {} to groups for user {}", groupId, username);
  939. groups.put(groupId, group);
  940. } else {
  941. log.info("User '{}' is associated with the group ID '{}', but I couldn't find that actual group", username, groupId);
  942. }
  943. }
  944. return groups;
  945. }
  946. @Nonnull
  947. private Map<String, ElementAccessManager> getElementAccessManagers(final String username) {
  948. if (StringUtils.isBlank(username)) {
  949. return Collections.emptyMap();
  950. }
  951. final String cacheId = getCacheIdForUserElementAccessManagers(username);
  952. final Map<String, ElementAccessManager> cachedElementAccessManagers = getCachedMap(cache

Large files files are truncated, but you can click here to view the full file