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