1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 package org.opencastproject.playlists;
22
23 import static org.opencastproject.security.api.SecurityConstants.GLOBAL_ADMIN_ROLE;
24
25 import org.opencastproject.elasticsearch.api.SearchIndexException;
26 import org.opencastproject.elasticsearch.api.SearchResult;
27 import org.opencastproject.elasticsearch.index.ElasticsearchIndex;
28 import org.opencastproject.elasticsearch.index.objects.event.Event;
29 import org.opencastproject.elasticsearch.index.objects.event.EventSearchQuery;
30 import org.opencastproject.playlists.persistence.PlaylistDatabaseException;
31 import org.opencastproject.playlists.persistence.PlaylistDatabaseService;
32 import org.opencastproject.playlists.serialization.JaxbPlaylist;
33 import org.opencastproject.playlists.serialization.JaxbPlaylistEntry;
34 import org.opencastproject.security.api.AccessControlEntry;
35 import org.opencastproject.security.api.AccessControlList;
36 import org.opencastproject.security.api.AuthorizationService;
37 import org.opencastproject.security.api.Organization;
38 import org.opencastproject.security.api.Permissions;
39 import org.opencastproject.security.api.SecurityService;
40 import org.opencastproject.security.api.UnauthorizedException;
41 import org.opencastproject.security.api.User;
42 import org.opencastproject.util.NotFoundException;
43 import org.opencastproject.util.requests.SortCriterion;
44
45 import com.fasterxml.jackson.core.JsonProcessingException;
46 import com.fasterxml.jackson.databind.DeserializationFeature;
47 import com.fasterxml.jackson.databind.ObjectMapper;
48 import com.fasterxml.jackson.databind.ObjectReader;
49 import com.fasterxml.jackson.module.jaxb.JaxbAnnotationModule;
50
51 import org.osgi.service.component.ComponentContext;
52 import org.osgi.service.component.annotations.Activate;
53 import org.osgi.service.component.annotations.Component;
54 import org.osgi.service.component.annotations.Reference;
55 import org.slf4j.Logger;
56 import org.slf4j.LoggerFactory;
57
58 import java.util.ArrayList;
59 import java.util.Date;
60 import java.util.List;
61
62 @Component(
63 property = {
64 "service.description=Playlist Service",
65 "service.pid=org.opencastproject.playlists.PlaylistService"
66 },
67 immediate = true,
68 service = { PlaylistService.class }
69 )
70 public class PlaylistService {
71
72 private static final Logger logger = LoggerFactory.getLogger(PlaylistService.class);
73
74
75 protected PlaylistDatabaseService persistence;
76
77
78 protected SecurityService securityService;
79
80
81 protected AuthorizationService authorizationService = null;
82
83 private ElasticsearchIndex elasticsearchIndex;
84
85
86
87
88
89
90
91 @Reference(name = "playlist-persistence")
92 public void setPersistence(PlaylistDatabaseService persistence) {
93 this.persistence = persistence;
94 }
95
96
97
98
99
100
101
102 @Reference(name = "security-service")
103 public void setSecurityService(SecurityService securityService) {
104 this.securityService = securityService;
105 }
106
107
108
109
110
111
112
113 @Reference
114 public void setAuthorizationService(AuthorizationService authorizationService) {
115 this.authorizationService = authorizationService;
116 }
117
118 @Reference
119 void setElasticsearchIndex(ElasticsearchIndex elasticsearchIndex) {
120 this.elasticsearchIndex = elasticsearchIndex;
121 }
122
123 @Activate
124 public void activate(ComponentContext cc) throws Exception {
125 logger.info("Activating Playlist Service");
126 }
127
128
129
130
131
132
133
134
135
136 public Playlist getPlaylistById(String id) throws NotFoundException, IllegalStateException, UnauthorizedException {
137 try {
138 Playlist playlist = persistence.getPlaylist(id);
139 if (!checkPermission(playlist, Permissions.Action.READ)) {
140 throw new UnauthorizedException("User does not have read permissions");
141 }
142 return playlist;
143 } catch (PlaylistDatabaseException e) {
144 throw new IllegalStateException("Could not get playlist from database with id ");
145 }
146 }
147
148
149
150
151
152
153
154
155 public List<Playlist> getPlaylists(int limit, int offset) throws IllegalStateException {
156 return getPlaylists(limit, offset, new SortCriterion("", SortCriterion.Order.None));
157 }
158
159 public List<Playlist> getPlaylists(int limit, int offset, SortCriterion sortCriterion)
160 throws IllegalStateException {
161 try {
162 List<Playlist> playlists = persistence.getPlaylists(limit, offset, sortCriterion);
163 playlists.removeIf(playlist -> !checkPermission(playlist, Permissions.Action.READ));
164 return playlists;
165 } catch (PlaylistDatabaseException e) {
166 throw new IllegalStateException("Could not get playlist from database with id ");
167 }
168 }
169
170 public List<Playlist> getAllForAdministrativeRead(Date from, Date to, int limit)
171 throws IllegalStateException, UnauthorizedException {
172 final var user = securityService.getUser();
173 final var orgAdminRole = securityService.getOrganization().getAdminRole();
174 if (!user.hasRole(GLOBAL_ADMIN_ROLE) && !user.hasRole(orgAdminRole)) {
175 throw new UnauthorizedException("Only (org-)admins can call this method");
176 }
177
178 try {
179 return persistence.getAllForAdministrativeRead(from, to, limit);
180 } catch (PlaylistDatabaseException e) {
181 throw new IllegalStateException("Could not get playlist from database", e);
182 }
183 }
184
185
186
187
188
189
190
191
192 public Playlist update(Playlist playlist)
193 throws IllegalStateException, UnauthorizedException, IllegalArgumentException {
194 try {
195 Playlist existingPlaylist = persistence.getPlaylist(playlist.getId());
196 if (!checkPermission(existingPlaylist, Permissions.Action.WRITE)) {
197 throw new UnauthorizedException("User does not have write permissions");
198 }
199
200
201 for (PlaylistEntry entry : playlist.getEntries()) {
202 if (existingPlaylist.getEntries().stream().noneMatch(e -> entry.getId() == e.getId())) {
203 if (entry.getId() != 0) {
204 throw new IllegalArgumentException("When updating a playlist, entries should either have the id of an "
205 + "existing entry, or no id at all.");
206 }
207 }
208 }
209 for (PlaylistAccessControlEntry entry : playlist.getAccessControlEntries()) {
210 if (existingPlaylist.getAccessControlEntries().stream().noneMatch(e -> entry.getId() == e.getId())) {
211 if (entry.getId() != 0) {
212 throw new IllegalArgumentException("When updating a playlist, ACL entries should either have the id of an "
213 + "existing entry, or no id at all.");
214 }
215 }
216 }
217 } catch (NotFoundException e) {
218
219 for (PlaylistEntry entry : playlist.getEntries()) {
220 if (entry.getId() != 0) {
221 throw new IllegalArgumentException("Entries for new playlists should not have identifiers set");
222 }
223 }
224 for (PlaylistAccessControlEntry entry : playlist.getAccessControlEntries()) {
225 if (entry.getId() != 0) {
226 throw new IllegalArgumentException("ACL Entries for new playlists should not have identifiers set");
227 }
228 }
229 } catch (PlaylistDatabaseException e) {
230 throw new IllegalStateException("Could not get playlist from database with id ");
231 }
232
233 if (playlist.getOrganization() == null) {
234 playlist.setOrganization(securityService.getOrganization().getId());
235 }
236 for (PlaylistEntry entry : playlist.getEntries()) {
237 entry.setPlaylist(playlist);
238 }
239 for (PlaylistAccessControlEntry entry : playlist.getAccessControlEntries()) {
240 entry.setPlaylist(playlist);
241 }
242
243 try {
244 playlist = persistence.updatePlaylist(playlist, securityService.getOrganization().getId());
245 return playlist;
246 } catch (PlaylistDatabaseException e) {
247 throw new IllegalStateException("Could not update playlist from database with id ");
248 }
249 }
250
251
252
253
254
255
256
257
258
259 public Playlist updateWithJson(String id, String json) throws JsonProcessingException, UnauthorizedException {
260 try {
261 Playlist existingPlaylist = persistence.getPlaylist(id);
262 if (!checkPermission(existingPlaylist, Permissions.Action.WRITE)) {
263 throw new UnauthorizedException("User does not have write permissions");
264 }
265
266 JaxbAnnotationModule module = new JaxbAnnotationModule();
267 ObjectMapper objectMapper = new ObjectMapper();
268 objectMapper.registerModule(module);
269 objectMapper.enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY);
270
271 ObjectReader updater = objectMapper.readerForUpdating(new JaxbPlaylist(existingPlaylist));
272 JaxbPlaylist merged = updater.readValue(json);
273
274 return update(merged.toPlaylist());
275 } catch (NotFoundException | PlaylistDatabaseException e) {
276 throw new IllegalStateException("Could not get playlist from database with id " + id);
277 }
278 }
279
280
281
282
283
284
285
286
287
288 public Playlist remove(String playlistId)
289 throws NotFoundException, IllegalStateException, UnauthorizedException {
290 try {
291 Playlist playlist = persistence.getPlaylist(playlistId);
292 if (!checkPermission(playlist, Permissions.Action.WRITE)) {
293 throw new UnauthorizedException("User does not have write permissions");
294 }
295 playlist = persistence.deletePlaylist(playlist, securityService.getOrganization().getId());
296 return playlist;
297 } catch (PlaylistDatabaseException e) {
298 throw new IllegalStateException("Could not delete playlist from database with id " + playlistId);
299 }
300 }
301
302
303
304
305
306
307
308
309
310
311 public Playlist updateEntries(String playlistId, List<PlaylistEntry> playlistEntries)
312 throws NotFoundException, IllegalStateException, UnauthorizedException {
313 Playlist playlist;
314 try {
315 playlist = persistence.getPlaylist(playlistId);
316 } catch (PlaylistDatabaseException e) {
317 throw new IllegalStateException(e);
318 }
319 if (!checkPermission(playlist, Permissions.Action.WRITE)) {
320 throw new UnauthorizedException("User does not have write permissions");
321 }
322 playlist.setEntries(playlistEntries);
323
324 try {
325 playlist = persistence.updatePlaylist(playlist, securityService.getOrganization().getId());
326
327 return playlist;
328 } catch (PlaylistDatabaseException e) {
329 throw new IllegalStateException("Could not delete playlist from database with id " + playlistId);
330 }
331 }
332
333
334
335
336
337
338
339
340
341
342
343 public Playlist addEntry(String playlistId, String contentId, PlaylistEntryType type)
344 throws NotFoundException, IllegalStateException, UnauthorizedException {
345 Playlist playlist;
346 try {
347 playlist = persistence.getPlaylist(playlistId);
348 } catch (PlaylistDatabaseException e) {
349 throw new IllegalStateException(e);
350 }
351 if (!checkPermission(playlist, Permissions.Action.WRITE)) {
352 throw new UnauthorizedException("User does not have write permissions");
353 }
354 PlaylistEntry playlistEntry = new PlaylistEntry();
355 playlistEntry.setContentId(contentId);
356 playlistEntry.setType(type);
357 playlist.addEntry(playlistEntry);
358
359 try {
360 playlist = persistence.updatePlaylist(playlist, securityService.getOrganization().getId());
361
362 return playlist;
363 } catch (PlaylistDatabaseException e) {
364 throw new IllegalStateException("Could not delete playlist from database with id " + playlistId);
365 }
366 }
367
368
369
370
371
372
373
374
375
376
377 public Playlist removeEntry(String playlistId, long entryId)
378 throws NotFoundException, IllegalStateException, UnauthorizedException {
379 Playlist playlist;
380 try {
381 playlist = persistence.getPlaylist(playlistId);
382 if (!checkPermission(playlist, Permissions.Action.WRITE)) {
383 throw new UnauthorizedException("User does not have write permissions");
384 }
385 } catch (PlaylistDatabaseException e) {
386 throw new IllegalStateException(e);
387 }
388
389 playlist.removeEntry(
390 playlist.getEntries()
391 .stream()
392 .filter(e -> e.getId() == entryId)
393 .findFirst()
394 .get()
395 );
396
397 try {
398 playlist = persistence.updatePlaylist(playlist, securityService.getOrganization().getId());
399 return playlist;
400 } catch (PlaylistDatabaseException e) {
401 throw new IllegalStateException("Could not delete playlist from database with id " + playlistId);
402 }
403 }
404
405
406
407
408
409
410
411
412 public JaxbPlaylist enrich(Playlist playlist) {
413 var jaxbPlaylist = new JaxbPlaylist(playlist);
414 var org = securityService.getOrganization().getId();
415 var user = securityService.getUser();
416
417
418 List<JaxbPlaylistEntry> jaxbPlaylistEntries = jaxbPlaylist.getEntries();
419 for (JaxbPlaylistEntry entry : jaxbPlaylistEntries) {
420 String contentId = entry.getContentId();
421
422 if (contentId == null || contentId.isEmpty()) {
423 entry.setType(PlaylistEntryType.INACCESSIBLE);
424 logger.warn("Entry {} has no content, marking as inaccessible", entry.getId());
425 continue;
426 }
427
428 try {
429 if (entry.getType() == PlaylistEntryType.EVENT) {
430
431
432 SearchResult<Event> result = elasticsearchIndex.getByQuery(
433 new EventSearchQuery(org, user).withIdentifier(contentId));
434 if (result.getPageSize() != 0) {
435 Event event = result.getItems()[0].getSource();
436 entry.setPublications(event.getPublications());
437 } else {
438 entry.setType(PlaylistEntryType.INACCESSIBLE);
439 }
440 }
441 } catch (SearchIndexException e) {
442 throw new RuntimeException(e);
443 }
444 }
445 jaxbPlaylist.setEntries(jaxbPlaylistEntries);
446
447 return jaxbPlaylist;
448 }
449
450
451
452
453
454
455
456 private boolean checkPermission(Playlist playlist, Permissions.Action action) {
457 User currentUser = securityService.getUser();
458 Organization currentOrg = securityService.getOrganization();
459 String currentOrgAdminRole = currentOrg.getAdminRole();
460 String currentOrgId = currentOrg.getId();
461
462 return currentUser.hasRole(GLOBAL_ADMIN_ROLE)
463 || (currentUser.hasRole(currentOrgAdminRole) && currentOrgId.equals(playlist.getOrganization()))
464 || authorizationService.hasPermission(getAccessControlList(playlist), action.toString());
465 }
466
467
468
469
470
471
472 private AccessControlList getAccessControlList(Playlist playlist) {
473 List<AccessControlEntry> accessControlEntries = new ArrayList<>();
474 for (PlaylistAccessControlEntry entry : playlist.getAccessControlEntries()) {
475 accessControlEntries.add(entry.toAccessControlEntry());
476 }
477 return new AccessControlList(accessControlEntries);
478 }
479 }