1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 package org.opencastproject.search.impl;
23
24 import static org.opencastproject.security.api.SecurityConstants.GLOBAL_ADMIN_ROLE;
25 import static org.opencastproject.util.data.functions.Misc.chuck;
26
27 import org.opencastproject.job.api.AbstractJobProducer;
28 import org.opencastproject.job.api.Job;
29 import org.opencastproject.mediapackage.MediaPackage;
30 import org.opencastproject.mediapackage.MediaPackageParser;
31 import org.opencastproject.search.api.SearchException;
32 import org.opencastproject.search.api.SearchResultList;
33 import org.opencastproject.search.api.SearchService;
34 import org.opencastproject.search.impl.persistence.SearchServiceDatabase;
35 import org.opencastproject.search.impl.persistence.SearchServiceDatabaseException;
36 import org.opencastproject.security.api.Organization;
37 import org.opencastproject.security.api.OrganizationDirectoryService;
38 import org.opencastproject.security.api.SecurityService;
39 import org.opencastproject.security.api.StaticFileAuthorization;
40 import org.opencastproject.security.api.UnauthorizedException;
41 import org.opencastproject.security.api.User;
42 import org.opencastproject.security.api.UserDirectoryService;
43 import org.opencastproject.security.util.SecurityUtil;
44 import org.opencastproject.serviceregistry.api.ServiceRegistry;
45 import org.opencastproject.serviceregistry.api.ServiceRegistryException;
46 import org.opencastproject.util.LoadUtil;
47 import org.opencastproject.util.NotFoundException;
48 import org.opencastproject.util.data.Tuple;
49
50 import com.google.common.cache.CacheBuilder;
51 import com.google.common.cache.CacheLoader;
52 import com.google.common.cache.LoadingCache;
53
54 import org.apache.commons.lang3.tuple.Pair;
55 import org.elasticsearch.action.search.SearchResponse;
56 import org.elasticsearch.search.builder.SearchSourceBuilder;
57 import org.osgi.service.component.ComponentContext;
58 import org.osgi.service.component.annotations.Activate;
59 import org.osgi.service.component.annotations.Component;
60 import org.osgi.service.component.annotations.Modified;
61 import org.osgi.service.component.annotations.Reference;
62 import org.slf4j.Logger;
63 import org.slf4j.LoggerFactory;
64
65 import java.util.Collection;
66 import java.util.Collections;
67 import java.util.List;
68 import java.util.Map;
69 import java.util.concurrent.TimeUnit;
70 import java.util.regex.Matcher;
71 import java.util.regex.Pattern;
72
73
74
75
76 @Component(
77 immediate = true,
78 service = { SearchService.class, SearchServiceImpl.class, StaticFileAuthorization.class },
79 property = {
80 "service.description=Search Service",
81 "service.pid=org.opencastproject.search.impl.SearchServiceImpl"
82 }
83 )
84 public final class SearchServiceImpl extends AbstractJobProducer implements SearchService, StaticFileAuthorization {
85
86
87 private static final Logger logger = LoggerFactory.getLogger(SearchServiceImpl.class);
88
89
90 public static final String JOB_TYPE = "org.opencastproject.search";
91
92
93 public static final float DEFAULT_ADD_JOB_LOAD = 0.1f;
94
95
96 public static final float DEFAULT_DELETE_JOB_LOAD = 0.1f;
97
98
99 public static final String ADD_JOB_LOAD_KEY = "job.load.add";
100
101
102 public static final String DELETE_JOB_LOAD_KEY = "job.load.delete";
103
104
105 private float addJobLoad = DEFAULT_ADD_JOB_LOAD;
106
107
108 private float deleteJobLoad = DEFAULT_DELETE_JOB_LOAD;
109
110
111 private enum Operation {
112 Add, Delete, DeleteSeries
113 }
114
115 private SearchServiceIndex index;
116
117
118 private SecurityService securityService;
119
120
121 private ServiceRegistry serviceRegistry;
122
123
124 private SearchServiceDatabase persistence;
125
126
127 protected UserDirectoryService userDirectoryService = null;
128
129
130 protected OrganizationDirectoryService organizationDirectory = null;
131
132 private final LoadingCache<Tuple<User, String>, Boolean> cache;
133
134 private static final Pattern staticFilePattern =
135 Pattern.compile("^/([^/]+)/(?:engage-player|engage-live)/([^/]+)/.*$");
136
137
138
139
140 public SearchServiceImpl() {
141 super(JOB_TYPE);
142
143 cache = CacheBuilder.newBuilder()
144 .maximumSize(2048)
145 .expireAfterWrite(1, TimeUnit.MINUTES)
146 .build(new CacheLoader<>() {
147 @Override
148 public Boolean load(Tuple<User, String> key) {
149 return loadUrlAccess(key.getB());
150 }
151 });
152 }
153
154
155
156
157
158
159
160 @Override
161 @Activate
162 public void activate(final ComponentContext cc) throws IllegalStateException {
163 super.activate(cc);
164 }
165
166
167
168
169
170
171 public Job add(MediaPackage mediaPackage) throws SearchException, IllegalArgumentException {
172 try {
173 return serviceRegistry.createJob(JOB_TYPE, Operation.Add.toString(),
174 Collections.singletonList(MediaPackageParser.getAsXml(mediaPackage)), addJobLoad);
175 } catch (ServiceRegistryException e) {
176 throw new SearchException(e);
177 }
178 }
179
180 @Override
181 public void addSynchronously(MediaPackage mediaPackage)
182 throws SearchException, IllegalArgumentException, UnauthorizedException {
183 try {
184 index.addSynchronously(mediaPackage);
185 } catch (SearchServiceDatabaseException e) {
186 throw new SearchException(e);
187 }
188 }
189
190 @Override
191 public Collection<Pair<Organization, MediaPackage>> getSeries(String seriesId) {
192 try {
193 return persistence.getSeries(seriesId);
194 } catch (SearchServiceDatabaseException e) {
195 throw new SearchException(e);
196 }
197 }
198
199
200
201
202
203
204 public Job delete(String mediaPackageId) throws SearchException {
205 try {
206 return serviceRegistry.createJob(
207 JOB_TYPE, Operation.Delete.toString(), Collections.singletonList(mediaPackageId), deleteJobLoad);
208 } catch (ServiceRegistryException e) {
209 throw new SearchException(e);
210 }
211 }
212
213 public boolean deleteSynchronously(String mediaPackageId) throws SearchException {
214 return index.deleteSynchronously(mediaPackageId);
215 }
216
217
218
219
220
221
222 public Job deleteSeries(String seriesId) throws SearchException {
223 try {
224 return serviceRegistry.createJob(
225 JOB_TYPE, Operation.DeleteSeries.toString(), Collections.singletonList(seriesId), deleteJobLoad);
226 } catch (ServiceRegistryException e) {
227 throw new SearchException(e);
228 }
229 }
230
231 @Override
232 public MediaPackage get(String mediaPackageId) throws NotFoundException, UnauthorizedException {
233 try {
234 return persistence.getMediaPackage(mediaPackageId);
235 } catch (SearchServiceDatabaseException e) {
236 throw new SearchException(e);
237 }
238 }
239
240 public SearchResultList search(SearchSourceBuilder searchSource) throws SearchException {
241 SearchResponse idxResp = this.index.search(searchSource);
242 return new SearchResultList(idxResp.getHits());
243 }
244
245
246
247
248
249 @Override
250 protected String process(Job job) throws Exception {
251 Operation op = null;
252 String operation = job.getOperation();
253 List<String> arguments = job.getArguments();
254 try {
255 op = Operation.valueOf(operation);
256 Organization org = organizationDirectory.getOrganization(job.getOrganization());
257 User user = userDirectoryService.loadUser(job.getCreator());
258 boolean[] deleted = new boolean[1];
259 switch (op) {
260 case Add:
261 MediaPackage mediaPackage = MediaPackageParser.getFromXml(arguments.get(0));
262 SecurityUtil.runAs(securityService, org, user, () -> {
263 try {
264 index.addSynchronously(mediaPackage);
265 } catch (UnauthorizedException | SearchServiceDatabaseException e) {
266 chuck(e);
267 }
268 });
269 return null;
270 case Delete:
271 String mediapackageId = arguments.get(0);
272 SecurityUtil.runAs(securityService, org, user, () -> {
273 deleted[0] = index.deleteSynchronously(mediapackageId);
274 });
275 return Boolean.toString(deleted[0]);
276 case DeleteSeries:
277 String seriesId = arguments.get(0);
278 SecurityUtil.runAs(securityService, org, user, () -> {
279 deleted[0] = index.deleteSeriesSynchronously(seriesId);
280 });
281 return Boolean.toString(deleted[0]);
282 default:
283 throw new IllegalStateException("Don't know how to handle operation '" + operation + "'");
284 }
285 } catch (IllegalArgumentException e) {
286 throw new ServiceRegistryException("This service can't handle operations of type '" + op + "'", e);
287 } catch (IndexOutOfBoundsException e) {
288 throw new ServiceRegistryException("This argument list for operation '" + op + "' does not meet expectations", e);
289 } catch (Exception e) {
290 throw new ServiceRegistryException("Error handling operation '" + op + "'", e);
291 }
292 }
293
294 @Reference
295 public void setSearchIndex(SearchServiceIndex ssi) {
296 this.index = ssi;
297 }
298
299 @Reference
300 public void setPersistence(SearchServiceDatabase persistence) {
301 this.persistence = persistence;
302 }
303
304
305 @Reference
306 public void setServiceRegistry(ServiceRegistry serviceRegistry) {
307 this.serviceRegistry = serviceRegistry;
308 }
309
310
311
312
313
314
315
316 @Reference
317 public void setSecurityService(SecurityService securityService) {
318 this.securityService = securityService;
319 }
320
321
322
323
324
325
326
327 @Reference
328 public void setUserDirectoryService(UserDirectoryService userDirectoryService) {
329 this.userDirectoryService = userDirectoryService;
330 }
331
332
333
334
335
336
337
338 @Reference
339 public void setOrganizationDirectoryService(OrganizationDirectoryService organizationDirectory) {
340 this.organizationDirectory = organizationDirectory;
341 }
342
343
344
345
346 @Override
347 protected OrganizationDirectoryService getOrganizationDirectoryService() {
348 return organizationDirectory;
349 }
350
351
352
353
354 @Override
355 protected SecurityService getSecurityService() {
356 return securityService;
357 }
358
359
360
361
362 @Override
363 protected ServiceRegistry getServiceRegistry() {
364 return serviceRegistry;
365 }
366
367
368
369
370 @Override
371 protected UserDirectoryService getUserDirectoryService() {
372 return userDirectoryService;
373 }
374
375 @Modified
376 public void modified(Map<String, Object> properties) {
377 if (properties == null) {
378 logger.info("No configuration available, using defaults");
379 properties = Map.of();
380 }
381
382 logger.info("Configuring SearchServiceImpl");
383 addJobLoad = LoadUtil.getConfiguredLoadValue(properties, ADD_JOB_LOAD_KEY, DEFAULT_ADD_JOB_LOAD, serviceRegistry);
384 deleteJobLoad = LoadUtil.getConfiguredLoadValue(
385 properties, DELETE_JOB_LOAD_KEY, DEFAULT_DELETE_JOB_LOAD, serviceRegistry);
386 }
387
388 @Override
389 public List<Pattern> getProtectedUrlPattern() {
390 return Collections.singletonList(staticFilePattern);
391 }
392
393 private boolean loadUrlAccess(final String mediaPackageId) {
394 logger.debug("Check if user `{}` has access to media package `{}`", securityService.getUser(), mediaPackageId);
395 try {
396 persistence.getMediaPackage(mediaPackageId);
397 } catch (NotFoundException | SearchServiceDatabaseException | UnauthorizedException e) {
398 return false;
399 }
400 return true;
401 }
402
403 @Override
404 public boolean verifyUrlAccess(final String path) {
405
406 final User user = securityService.getUser();
407 if (user.hasRole(GLOBAL_ADMIN_ROLE)) {
408 logger.debug("Allow access for admin `{}`", user);
409 return true;
410 }
411
412
413 final Matcher m = staticFilePattern.matcher(path);
414 if (!m.matches()) {
415 logger.debug("Path does not match pattern. Preventing access.");
416 return false;
417 }
418
419
420 final String organizationId = m.group(1);
421 if (!securityService.getOrganization().getId().equals(organizationId)) {
422 logger.debug("The user's organization does not match. Preventing access.");
423 return false;
424 }
425
426
427 final String mediaPackageId = m.group(2);
428 final boolean access = cache.getUnchecked(Tuple.tuple(user, mediaPackageId));
429 logger.debug("Check if user `{}` has access to media package `{}` using cache: {}", user, mediaPackageId, access);
430 return access;
431 }
432 }