View Javadoc
1   /*
2    * Licensed to The Apereo Foundation under one or more contributor license
3    * agreements. See the NOTICE file distributed with this work for additional
4    * information regarding copyright ownership.
5    *
6    *
7    * The Apereo Foundation licenses this file to you under the Educational
8    * Community License, Version 2.0 (the "License"); you may not use this file
9    * except in compliance with the License. You may obtain a copy of the License
10   * at:
11   *
12   *   http://opensource.org/licenses/ecl2.txt
13   *
14   * Unless required by applicable law or agreed to in writing, software
15   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
16   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
17   * License for the specific language governing permissions and limitations under
18   * the License.
19   *
20   */
21  
22  package org.opencastproject.search.impl.persistence;
23  
24  import static org.opencastproject.db.Queries.namedQuery;
25  import static org.opencastproject.security.api.Permissions.Action.CONTRIBUTE;
26  import static org.opencastproject.security.api.Permissions.Action.READ;
27  import static org.opencastproject.security.api.Permissions.Action.WRITE;
28  import static org.opencastproject.security.api.SecurityConstants.GLOBAL_CAPTURE_AGENT_ROLE;
29  
30  import org.opencastproject.db.DBSession;
31  import org.opencastproject.db.DBSessionFactory;
32  import org.opencastproject.mediapackage.MediaPackage;
33  import org.opencastproject.mediapackage.MediaPackageException;
34  import org.opencastproject.mediapackage.MediaPackageParser;
35  import org.opencastproject.security.api.AccessControlList;
36  import org.opencastproject.security.api.AccessControlParser;
37  import org.opencastproject.security.api.AccessControlParsingException;
38  import org.opencastproject.security.api.AccessControlUtil;
39  import org.opencastproject.security.api.Organization;
40  import org.opencastproject.security.api.SecurityService;
41  import org.opencastproject.security.api.UnauthorizedException;
42  import org.opencastproject.security.api.User;
43  import org.opencastproject.util.NotFoundException;
44  import org.opencastproject.util.data.Tuple;
45  
46  import org.apache.commons.lang3.BooleanUtils;
47  import org.apache.commons.lang3.StringUtils;
48  import org.apache.commons.lang3.tuple.Pair;
49  import org.osgi.service.component.ComponentContext;
50  import org.osgi.service.component.annotations.Activate;
51  import org.osgi.service.component.annotations.Component;
52  import org.osgi.service.component.annotations.Reference;
53  import org.slf4j.Logger;
54  import org.slf4j.LoggerFactory;
55  
56  import java.io.IOException;
57  import java.util.ArrayList;
58  import java.util.Arrays;
59  import java.util.Collection;
60  import java.util.Date;
61  import java.util.List;
62  import java.util.Objects;
63  import java.util.Optional;
64  import java.util.function.Function;
65  import java.util.stream.Stream;
66  
67  import javax.persistence.EntityManager;
68  import javax.persistence.EntityManagerFactory;
69  import javax.persistence.TypedQuery;
70  
71  /**
72   * Implements {@link SearchServiceDatabase}. Defines permanent storage for series.
73   */
74  @Component(
75      immediate = true,
76      service = SearchServiceDatabase.class,
77      property = {
78          "service.description=Search Service Persistence"
79      }
80  )
81  public class SearchServiceDatabaseImpl implements SearchServiceDatabase {
82  
83    /** JPA persistence unit name */
84    public static final String PERSISTENCE_UNIT = "org.opencastproject.search.impl.persistence";
85  
86    private static final String CONFIG_EPISODE_ID_ROLE = "org.opencastproject.episode.id.role.access";
87  
88    /** Logging utilities */
89    private static final Logger logger = LoggerFactory.getLogger(SearchServiceDatabaseImpl.class);
90  
91    private boolean episodeRoleId = false;
92  
93    /** Factory used to create {@link EntityManager}s for transactions */
94    protected EntityManagerFactory emf;
95  
96    protected DBSessionFactory dbSessionFactory;
97  
98    protected DBSession db;
99  
100   /** The security service */
101   protected SecurityService securityService;
102 
103   /** OSGi DI */
104   @Reference(target = "(osgi.unit.name=org.opencastproject.search.impl.persistence)")
105   public void setEntityManagerFactory(EntityManagerFactory emf) {
106     this.emf = emf;
107   }
108 
109   @Reference
110   public void setDBSessionFactory(DBSessionFactory dbSessionFactory) {
111     this.dbSessionFactory = dbSessionFactory;
112   }
113 
114   /**
115    * Creates {@link EntityManagerFactory} using persistence provider and properties passed via OSGi.
116    *
117    * @param cc
118    * @throws SearchServiceDatabaseException
119    */
120   @Activate
121   public void activate(ComponentContext cc) throws SearchServiceDatabaseException {
122     logger.info("Activating persistence manager for search service");
123     db = dbSessionFactory.createSession(emf);
124     this.populateSeriesData();
125 
126     episodeRoleId = BooleanUtils.toBoolean(Objects.toString(
127         cc.getBundleContext().getProperty(CONFIG_EPISODE_ID_ROLE), "false"));
128     logger.debug("Usage of episode ID roles is set to {}", episodeRoleId);
129   }
130 
131   /**
132    * OSGi callback to set the security service.
133    *
134    * @param securityService
135    *          the securityService to set
136    */
137   @Reference
138   public void setSecurityService(SecurityService securityService) {
139     this.securityService = securityService;
140   }
141 
142   private void populateSeriesData() throws SearchServiceDatabaseException {
143     try {
144       db.execTxChecked(em -> {
145         TypedQuery<SearchEntity> q = em.createNamedQuery("Search.getNoSeries", SearchEntity.class);
146         List<SearchEntity> seriesList = q.getResultList();
147         for (SearchEntity series : seriesList) {
148           String mpSeriesId = MediaPackageParser.getFromXml(series.getMediaPackageXML()).getSeries();
149           if (StringUtils.isNotBlank(mpSeriesId) && !mpSeriesId.equals(series.getSeriesId())) {
150             logger.info("Fixing missing series ID for episode {}, series is {}", series.getMediaPackageId(),
151                 mpSeriesId);
152             series.setSeriesId(mpSeriesId);
153             em.merge(series);
154           }
155         }
156       });
157     } catch (Exception e) {
158       logger.error("Could not update media package: {}", e.getMessage());
159       throw new SearchServiceDatabaseException(e);
160     }
161   }
162 
163   /**
164    * {@inheritDoc}
165    *
166    * @see org.opencastproject.search.impl.persistence.SearchServiceDatabase#deleteMediaPackage(String, Date)
167    */
168   @Override
169   public void deleteMediaPackage(String mediaPackageId, Date deletionDate) throws SearchServiceDatabaseException,
170           NotFoundException, UnauthorizedException {
171     try {
172       db.execTxChecked(em -> {
173         Optional<SearchEntity> searchEntity = getSearchEntityQuery(mediaPackageId).apply(em);
174         if (searchEntity.isEmpty()) {
175           throw new NotFoundException("No media package with id=" + mediaPackageId + " exists");
176         }
177 
178         // Ensure this user is allowed to delete this episode
179         User currentUser = securityService.getUser();
180         Organization currentOrg = securityService.getOrganization();
181         MediaPackage searchMp = MediaPackageParser.getFromXml(searchEntity.get().getMediaPackageXML());
182         String accessControlXml = searchEntity.get().getAccessControl();
183 
184         // allow ca users to retract live publications without putting them into the ACL
185         if (!(searchMp.isLive() && currentUser.hasRole(GLOBAL_CAPTURE_AGENT_ROLE)) && accessControlXml != null) {
186           AccessControlList acl = AccessControlParser.parseAcl(accessControlXml);
187           if (!AccessControlUtil.isAuthorized(acl, currentUser, currentOrg, WRITE.toString(), mediaPackageId)) {
188             throw new UnauthorizedException(
189                 currentUser + " is not authorized to delete media package " + mediaPackageId);
190           }
191         }
192 
193         searchEntity.get().setDeletionDate(deletionDate);
194         searchEntity.get().setModificationDate(deletionDate);
195         em.merge(searchEntity.get());
196       });
197     } catch (NotFoundException | UnauthorizedException e) {
198       throw e;
199     } catch (Exception e) {
200       logger.error("Could not delete episode {}: {}", mediaPackageId, e.getMessage());
201       throw new SearchServiceDatabaseException(e);
202     }
203   }
204 
205   /**
206    * {@inheritDoc}
207    *
208    * @see org.opencastproject.search.impl.persistence.SearchServiceDatabase#countMediaPackages()
209    */
210   @Override
211   public int countMediaPackages() throws SearchServiceDatabaseException {
212     try {
213       return db.exec(namedQuery.find("Search.getCount", Long.class)).intValue();
214     } catch (Exception e) {
215       logger.error("Could not find number of mediapackages", e);
216       throw new SearchServiceDatabaseException(e);
217     }
218   }
219 
220   /**
221    * {@inheritDoc}
222    *
223    * @see org.opencastproject.search.impl.persistence.SearchServiceDatabase#getAllMediaPackages(int, int)
224    */
225   @Override
226   public Stream<Tuple<MediaPackage, String>> getAllMediaPackages(int pagesize, int offset)
227           throws SearchServiceDatabaseException {
228     List<SearchEntity> searchEntities;
229     try {
230       int firstResult = pagesize * offset;
231       searchEntities = db.exec(namedQuery.findSome("Search.findAll", firstResult, pagesize, SearchEntity.class));
232     } catch (Exception e) {
233       logger.error("Could not retrieve all episodes: {}", e.getMessage());
234       throw new SearchServiceDatabaseException(e);
235     }
236 
237     try {
238       return searchEntities.stream()
239             .map(entity -> {
240               try {
241                 MediaPackage mediaPackage = MediaPackageParser.getFromXml(entity.getMediaPackageXML());
242                 return Tuple.tuple(mediaPackage, entity.getOrganization().getId());
243               } catch (Exception e) {
244                 logger.error("Could not parse series entity: {}", e.getMessage());
245                 throw new RuntimeException(e);
246               }
247             });
248     } catch (Exception e) {
249       logger.error("Could not parse series entity: {}", e.getMessage());
250       throw new SearchServiceDatabaseException(e);
251     }
252   }
253 
254   /**
255    * {@inheritDoc}
256    *
257    * @see org.opencastproject.search.impl.persistence.SearchServiceDatabase#getAccessControlList(String)
258    */
259   @Override
260   public AccessControlList getAccessControlList(String mediaPackageId) throws NotFoundException,
261           SearchServiceDatabaseException {
262     try {
263       Optional<SearchEntity> entity = db.exec(getSearchEntityQuery(mediaPackageId));
264       if (entity.isEmpty()) {
265         throw new NotFoundException("Could not found media package with ID " + mediaPackageId);
266       }
267       if (entity.get().getAccessControl() == null) {
268         return null;
269       } else {
270         return AccessControlParser.parseAcl(entity.get().getAccessControl());
271       }
272     } catch (NotFoundException e) {
273       throw e;
274     } catch (Exception e) {
275       logger.error("Could not retrieve ACL {}", mediaPackageId, e);
276       throw new SearchServiceDatabaseException(e);
277     }
278   }
279 
280   /**
281    * {@inheritDoc}
282    *
283    * @see org.opencastproject.search.impl.persistence.SearchServiceDatabase#getAccessControlLists(String, String...)
284    */
285   @Override
286   public Collection<Pair<String, AccessControlList>> getAccessControlLists(final String seriesId, String ... excludeIds)
287           throws SearchServiceDatabaseException {
288     List<String> excludes = Arrays.asList(excludeIds);
289     List<Pair<String,AccessControlList>> accessControlLists = new ArrayList<>();
290     try {
291       List<SearchEntity> result = db.exec(namedQuery.findAll(
292           "Search.findBySeriesId",
293           SearchEntity.class,
294           Pair.of("seriesId", seriesId)
295       ));
296       for (SearchEntity entity: result) {
297         if (entity.getAccessControl() != null && !excludes.contains(entity.getMediaPackageId())) {
298           accessControlLists.add(Pair.of(
299               entity.getMediaPackageId(),
300               AccessControlParser.parseAcl(entity.getAccessControl()))
301           );
302         }
303       }
304     } catch (IOException | AccessControlParsingException e) {
305       throw new SearchServiceDatabaseException(e);
306     }
307     return accessControlLists;
308   }
309 
310   /**
311    * {@inheritDoc}
312    *
313    * @see org.opencastproject.search.impl.persistence.SearchServiceDatabase#getSeries(String)
314    */
315   public Collection<Pair<Organization, MediaPackage>> getSeries(final String seriesId)
316           throws SearchServiceDatabaseException {
317     List<Pair<Organization, MediaPackage>> episodes = new ArrayList<>();
318     EntityManager em = emf.createEntityManager();
319     TypedQuery<SearchEntity> q = em.createNamedQuery("Search.findBySeriesId", SearchEntity.class)
320         .setParameter("seriesId", seriesId);
321     try {
322       for (SearchEntity entity: q.getResultList()) {
323         if (entity.getMediaPackageXML() != null) {
324           episodes.add(Pair.of(
325               entity.getOrganization(),
326               MediaPackageParser.getFromXml(entity.getMediaPackageXML())));
327         }
328       }
329     } catch (MediaPackageException e) {
330       throw new SearchServiceDatabaseException(e);
331     } finally {
332       em.close();
333     }
334     return episodes;
335   }
336 
337   /**
338    * {@inheritDoc}
339    *
340    * @see org.opencastproject.search.impl.persistence.SearchServiceDatabase#storeMediaPackage(MediaPackage,
341    *      AccessControlList, Date)
342    */
343   @Override
344   public void storeMediaPackage(MediaPackage mediaPackage, AccessControlList acl, Date now)
345           throws SearchServiceDatabaseException, UnauthorizedException {
346     String mediaPackageXML = MediaPackageParser.getAsXml(mediaPackage);
347     String mediaPackageId = mediaPackage.getIdentifier().toString();
348     try {
349       db.execTxChecked(em -> {
350         Optional<SearchEntity> entity = getSearchEntityQuery(mediaPackageId).apply(em);
351         if (entity.isEmpty()) {
352           // Create new search entity
353           SearchEntity searchEntity = new SearchEntity();
354           searchEntity.setOrganization(securityService.getOrganization());
355           searchEntity.setMediaPackageId(mediaPackageId);
356           searchEntity.setMediaPackageXML(mediaPackageXML);
357           searchEntity.setAccessControl(AccessControlParser.toXml(acl));
358           searchEntity.setModificationDate(now);
359           searchEntity.setSeriesId(mediaPackage.getSeries());
360           em.persist(searchEntity);
361         } else {
362           // Ensure this user is allowed to update this media package
363           // If user has ROLE_EPISODE_<ID>_WRITE, no further permission checks are necessary
364           String accessControlXml = entity.get().getAccessControl();
365           if (accessControlXml != null && entity.get().getDeletionDate() == null) {
366             AccessControlList accessList = AccessControlParser.parseAcl(accessControlXml);
367             User currentUser = securityService.getUser();
368             Organization currentOrg = securityService.getOrganization();
369             if (!AccessControlUtil.isAuthorized(accessList, currentUser, currentOrg, WRITE.toString(),
370                 mediaPackageId)) {
371               throw new UnauthorizedException(currentUser + " is not authorized to update media package "
372                   + mediaPackageId);
373             }
374           }
375           entity.get().setOrganization(securityService.getOrganization());
376           entity.get().setMediaPackageId(mediaPackageId);
377           entity.get().setMediaPackageXML(mediaPackageXML);
378           entity.get().setAccessControl(AccessControlParser.toXml(acl));
379           entity.get().setModificationDate(now);
380           entity.get().setDeletionDate(null);
381           entity.get().setSeriesId(mediaPackage.getSeries());
382           em.merge(entity.get());
383         }
384       });
385     } catch (UnauthorizedException e) {
386       throw e;
387     } catch (Exception e) {
388       logger.error("Could not update media package: {}", e.getMessage());
389       throw new SearchServiceDatabaseException(e);
390     }
391   }
392 
393   /**
394    * {@inheritDoc}
395    *
396    * @see org.opencastproject.search.impl.persistence.SearchServiceDatabase#getMediaPackage(String)
397    */
398   @Override
399   public MediaPackage getMediaPackage(String mediaPackageId)
400           throws NotFoundException, SearchServiceDatabaseException, UnauthorizedException {
401     try {
402       return db.execTxChecked(em -> {
403         Optional<SearchEntity> episodeEntity = getSearchEntityQuery(mediaPackageId).apply(em);
404         if (episodeEntity.isEmpty() || episodeEntity.get().getDeletionDate() != null) {
405           throw new NotFoundException("No episode with id=" + mediaPackageId + " exists");
406         }
407 
408         String accessControlXml = episodeEntity.get().getAccessControl();
409         if (accessControlXml != null) {
410           AccessControlList acl = AccessControlParser.parseAcl(accessControlXml);
411           User currentUser = securityService.getUser();
412           Organization currentOrg = securityService.getOrganization();
413           // There are several reasons a user may need to load a episode: to read content, to edit it, or add content
414           if (!AccessControlUtil.isAuthorized(acl, currentUser, currentOrg, READ.toString(), mediaPackageId)
415                   && !AccessControlUtil.isAuthorized(acl, currentUser, currentOrg, CONTRIBUTE.toString(),
416               mediaPackageId)
417                   && !AccessControlUtil.isAuthorized(acl, currentUser, currentOrg, WRITE.toString(), mediaPackageId)) {
418             throw new UnauthorizedException(currentUser + " is not authorized to see episode " + mediaPackageId);
419           }
420         }
421         return MediaPackageParser.getFromXml(episodeEntity.get().getMediaPackageXML());
422       });
423     } catch (NotFoundException | UnauthorizedException e) {
424       throw e;
425     } catch (Exception e) {
426       logger.error("Could not get episode {} from database: {} ", mediaPackageId, e.getMessage());
427       throw new SearchServiceDatabaseException(e);
428     }
429   }
430 
431   /**
432    * {@inheritDoc}
433    *
434    * @see org.opencastproject.search.impl.persistence.SearchServiceDatabase#getModificationDate(String)
435    */
436   @Override
437   public Date getModificationDate(String mediaPackageId) throws NotFoundException, SearchServiceDatabaseException {
438     try {
439       return db.execTxChecked(em -> {
440         Optional<SearchEntity> searchEntity = getSearchEntityQuery(mediaPackageId).apply(em);
441         if (searchEntity.isEmpty()) {
442           throw new NotFoundException("No media package with id=" + mediaPackageId + " exists");
443         }
444         // Ensure this user is allowed to read this media package
445         String accessControlXml = searchEntity.get().getAccessControl();
446         if (accessControlXml != null) {
447           AccessControlList acl = AccessControlParser.parseAcl(accessControlXml);
448           User currentUser = securityService.getUser();
449           Organization currentOrg = securityService.getOrganization();
450           if (!AccessControlUtil.isAuthorized(acl, currentUser, currentOrg, READ.toString(), mediaPackageId)) {
451             throw new UnauthorizedException(
452                 currentUser + " is not authorized to read media package " + mediaPackageId);
453           }
454         }
455         return searchEntity.get().getModificationDate();
456       });
457     } catch (NotFoundException e) {
458       throw e;
459     } catch (Exception e) {
460       logger.error("Could not get modification date {}: {}", mediaPackageId, e.getMessage());
461       throw new SearchServiceDatabaseException(e);
462     }
463   }
464 
465   /**
466    * {@inheritDoc}
467    *
468    * @see org.opencastproject.search.impl.persistence.SearchServiceDatabase#getDeletionDate(String)
469    */
470   @Override
471   public Date getDeletionDate(String mediaPackageId) throws NotFoundException, SearchServiceDatabaseException {
472     try {
473       return db.execTxChecked(em -> {
474         Optional<SearchEntity> searchEntity = getSearchEntityQuery(mediaPackageId).apply(em);
475         if (searchEntity.isEmpty()) {
476           throw new NotFoundException("No media package with id=" + mediaPackageId + " exists");
477         }
478         // Ensure this user is allowed to read this media package
479         String accessControlXml = searchEntity.get().getAccessControl();
480         if (accessControlXml != null) {
481           AccessControlList acl = AccessControlParser.parseAcl(accessControlXml);
482           User currentUser = securityService.getUser();
483           Organization currentOrg = securityService.getOrganization();
484           if (!AccessControlUtil.isAuthorized(acl, currentUser, currentOrg, READ.toString(), mediaPackageId)) {
485             throw new UnauthorizedException(
486                 currentUser + " is not authorized to read media package " + mediaPackageId);
487           }
488         }
489         return searchEntity.get().getDeletionDate();
490       });
491     } catch (NotFoundException e) {
492       throw e;
493     } catch (Exception e) {
494       logger.error("Could not get deletion date {}: {}", mediaPackageId, e.getMessage());
495       throw new SearchServiceDatabaseException(e);
496     }
497   }
498 
499   /**
500    * {@inheritDoc}
501    *
502    * @see org.opencastproject.search.impl.persistence.SearchServiceDatabase#isAvailable(String)
503    */
504   public boolean isAvailable(String mediaPackageId) throws SearchServiceDatabaseException {
505     try {
506       return db.execTxChecked(em -> {
507         Optional<SearchEntity> searchEntity = getSearchEntityQuery(mediaPackageId).apply(em);
508         return searchEntity.stream().anyMatch(entity -> entity.getDeletionDate() == null);
509       });
510     } catch (Exception e) {
511       logger.error("Error while checking if mediapackage {} exists in database: {}", mediaPackageId, e.getMessage());
512       throw new SearchServiceDatabaseException(e);
513     }
514   }
515 
516   /**
517    * {@inheritDoc}
518    *
519    * @see org.opencastproject.search.impl.persistence.SearchServiceDatabase#getOrganizationId(String)
520    */
521   @Override
522   public String getOrganizationId(String mediaPackageId) throws NotFoundException, SearchServiceDatabaseException {
523     try {
524       return db.execTxChecked(em -> {
525         Optional<SearchEntity> searchEntity = getSearchEntityQuery(mediaPackageId).apply(em);
526         if (searchEntity.isEmpty()) {
527           throw new NotFoundException("No media package with id=" + mediaPackageId + " exists");
528         }
529         // Ensure this user is allowed to read this media package
530         String accessControlXml = searchEntity.get().getAccessControl();
531         if (accessControlXml != null) {
532           AccessControlList acl = AccessControlParser.parseAcl(accessControlXml);
533           User currentUser = securityService.getUser();
534           Organization currentOrg = securityService.getOrganization();
535           if (!AccessControlUtil.isAuthorized(acl, currentUser, currentOrg, READ.toString(), mediaPackageId)) {
536             throw new UnauthorizedException(
537                 currentUser + " is not authorized to read media package " + mediaPackageId);
538           }
539         }
540         return searchEntity.get().getOrganization().getId();
541       });
542     } catch (NotFoundException e) {
543       throw e;
544     } catch (Exception e) {
545       logger.error("Could not get deletion date {}: {}", mediaPackageId, e.getMessage());
546       throw new SearchServiceDatabaseException(e);
547     }
548   }
549 
550   /**
551    * Gets a search entity by it's id, using the current organizational context.
552    *
553    * @param id
554    *          the media package identifier
555    * @return the search entity, or null if not found
556    */
557   private Function<EntityManager, Optional<SearchEntity>> getSearchEntityQuery(String id) {
558     return namedQuery.findOpt(
559         "Search.findById",
560         SearchEntity.class,
561         Pair.of("mediaPackageId", id)
562     );
563   }
564 }