From 8c387a0ff146d907ab8deb310e77c5e8826818d8 Mon Sep 17 00:00:00 2001 From: Lukas Kalbertodt Date: Mon, 29 Jul 2024 14:46:19 +0200 Subject: [PATCH 1/4] Make Tobira harvest API work with multi-tenancy An org-filter was added to the event-query and the playlist `getForAdministrativeRead`. The latter also relaxed its role requirements to work with role admin roles as well. And the Tobira endpoint itself now checks for (org-)admin role. The search query did not need any adjustments, it already filtered by the org. --- .../playlists/PlaylistService.java | 6 ++++-- .../PlaylistDatabaseServiceImpl.java | 4 +++- .../opencastproject/tobira/impl/Harvest.java | 17 +++++++++++++++-- .../tobira/impl/TobiraEndpoint.java | 9 ++++++++- 4 files changed, 30 insertions(+), 6 deletions(-) diff --git a/modules/playlists/src/main/java/org/opencastproject/playlists/PlaylistService.java b/modules/playlists/src/main/java/org/opencastproject/playlists/PlaylistService.java index e927e12a177..5723c2b7268 100644 --- a/modules/playlists/src/main/java/org/opencastproject/playlists/PlaylistService.java +++ b/modules/playlists/src/main/java/org/opencastproject/playlists/PlaylistService.java @@ -169,8 +169,10 @@ public List getPlaylists(int limit, int offset, SortCriterion sortCrit public List getAllForAdministrativeRead(Date from, Date to, int limit) throws IllegalStateException, UnauthorizedException { - if (!this.securityService.getUser().hasRole(GLOBAL_ADMIN_ROLE)) { - throw new UnauthorizedException("Only admins can call this method"); + final var user = securityService.getUser(); + final var orgAdminRole = securityService.getOrganization().getAdminRole(); + if (!user.hasRole(GLOBAL_ADMIN_ROLE) && !user.hasRole(orgAdminRole)) { + throw new UnauthorizedException("Only (org-)admins can call this method"); } try { diff --git a/modules/playlists/src/main/java/org/opencastproject/playlists/persistence/PlaylistDatabaseServiceImpl.java b/modules/playlists/src/main/java/org/opencastproject/playlists/persistence/PlaylistDatabaseServiceImpl.java index 4d5922d937f..e6ff124a344 100644 --- a/modules/playlists/src/main/java/org/opencastproject/playlists/persistence/PlaylistDatabaseServiceImpl.java +++ b/modules/playlists/src/main/java/org/opencastproject/playlists/persistence/PlaylistDatabaseServiceImpl.java @@ -170,10 +170,12 @@ public List getAllForAdministrativeRead(Date startDate, Date endDate, final var criteriaBuilder = em.getCriteriaBuilder(); final var criteriaQuery = criteriaBuilder.createQuery(Playlist.class); final var from = criteriaQuery.from(Playlist.class); + final var org = securityService.getOrganization().getId(); final var select = criteriaQuery.select(from) .where( criteriaBuilder.greaterThanOrEqualTo(from.get("updated"), startDate), - criteriaBuilder.lessThan(from.get("updated"), endDate) + criteriaBuilder.lessThan(from.get("updated"), endDate), + criteriaBuilder.equal(from.get("organization"), org) ) .orderBy(criteriaBuilder.asc(from.get("updated"))); diff --git a/modules/tobira/src/main/java/org/opencastproject/tobira/impl/Harvest.java b/modules/tobira/src/main/java/org/opencastproject/tobira/impl/Harvest.java index ccb831c4394..68e5f7d1a1e 100644 --- a/modules/tobira/src/main/java/org/opencastproject/tobira/impl/Harvest.java +++ b/modules/tobira/src/main/java/org/opencastproject/tobira/impl/Harvest.java @@ -26,6 +26,8 @@ import org.opencastproject.search.api.SearchResultList; import org.opencastproject.search.api.SearchService; import org.opencastproject.security.api.AuthorizationService; +import org.opencastproject.security.api.SecurityConstants; +import org.opencastproject.security.api.SecurityService; import org.opencastproject.security.api.UnauthorizedException; import org.opencastproject.series.api.SeriesException; import org.opencastproject.series.api.SeriesService; @@ -78,9 +80,19 @@ static Jsons.Obj harvest( SearchService searchService, SeriesService seriesService, AuthorizationService authorizationService, + SecurityService securityService, PlaylistService playlistService, Workspace workspace ) throws UnauthorizedException, SeriesException { + final var org = securityService.getOrganization().getId(); + + var user = securityService.getUser(); + var orgAdminRole = securityService.getOrganization().getAdminRole(); + var isAdmin = user.hasRole(SecurityConstants.GLOBAL_ADMIN_ROLE) || user.hasRole(orgAdminRole); + if (!isAdmin) { + throw new UnauthorizedException(user, "only (org-) admins can access the Tobira harvest API"); + } + // ===== Retrieve information about events, series, and playlists ============================= // // In this step, we always request `preferredAmount + 1` in order to figure out the values for @@ -89,8 +101,9 @@ static Jsons.Obj harvest( // Retrieve episodes from index. final SearchSourceBuilder q = new SearchSourceBuilder().query( QueryBuilders.boolQuery() - .must(QueryBuilders.rangeQuery(SearchResult.MODIFIED_DATE).gte(since.getTime())) - .must(QueryBuilders.termQuery(SearchResult.TYPE, SearchService.IndexEntryType.Episode))) + .must(QueryBuilders.termQuery(SearchResult.ORG, org)) + .must(QueryBuilders.rangeQuery(SearchResult.MODIFIED_DATE).gte(since.getTime())) + .must(QueryBuilders.termQuery(SearchResult.TYPE, SearchService.IndexEntryType.Episode))) .sort(SearchResult.MODIFIED_DATE, SortOrder.ASC) .size(preferredAmount + 1); final SearchResultList results = searchService.search(q); diff --git a/modules/tobira/src/main/java/org/opencastproject/tobira/impl/TobiraEndpoint.java b/modules/tobira/src/main/java/org/opencastproject/tobira/impl/TobiraEndpoint.java index d18bdd2e807..3c1d5d4446e 100644 --- a/modules/tobira/src/main/java/org/opencastproject/tobira/impl/TobiraEndpoint.java +++ b/modules/tobira/src/main/java/org/opencastproject/tobira/impl/TobiraEndpoint.java @@ -28,6 +28,7 @@ import org.opencastproject.playlists.PlaylistService; import org.opencastproject.search.api.SearchService; import org.opencastproject.security.api.AuthorizationService; +import org.opencastproject.security.api.SecurityService; import org.opencastproject.series.api.SeriesService; import org.opencastproject.util.Jsons; import org.opencastproject.util.doc.rest.RestParameter; @@ -102,6 +103,7 @@ public class TobiraEndpoint { private SearchService searchService; private SeriesService seriesService; private AuthorizationService authorizationService; + private SecurityService securityService; private PlaylistService playlistService; private Workspace workspace; @@ -125,6 +127,11 @@ public void setAuthorizationService(AuthorizationService service) { this.authorizationService = service; } + @Reference + public void setSecurityService(SecurityService service) { + this.securityService = service; + } + @Reference public void setPlaylistService(PlaylistService service) { this.playlistService = service; @@ -206,7 +213,7 @@ public Response harvest( var json = Harvest.harvest( preferredAmount, new Date(since), - searchService, seriesService, authorizationService, playlistService, workspace); + searchService, seriesService, authorizationService, securityService, playlistService, workspace); // TODO: encoding return Response.ok(json.toJson()).build(); From dde4e6b355a278d442876c7fe804a61d45b83c90 Mon Sep 17 00:00:00 2001 From: Lukas Kalbertodt Date: Mon, 2 Sep 2024 15:18:31 +0200 Subject: [PATCH 2/4] Apply suggestions from code review Co-authored-by: Greg Logan --- .../java/org/opencastproject/tobira/impl/Harvest.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/tobira/src/main/java/org/opencastproject/tobira/impl/Harvest.java b/modules/tobira/src/main/java/org/opencastproject/tobira/impl/Harvest.java index 68e5f7d1a1e..2eaac022153 100644 --- a/modules/tobira/src/main/java/org/opencastproject/tobira/impl/Harvest.java +++ b/modules/tobira/src/main/java/org/opencastproject/tobira/impl/Harvest.java @@ -90,7 +90,7 @@ static Jsons.Obj harvest( var orgAdminRole = securityService.getOrganization().getAdminRole(); var isAdmin = user.hasRole(SecurityConstants.GLOBAL_ADMIN_ROLE) || user.hasRole(orgAdminRole); if (!isAdmin) { - throw new UnauthorizedException(user, "only (org-) admins can access the Tobira harvest API"); + throw new UnauthorizedException(user, "Only (org-) admins can access the Tobira harvest API"); } // ===== Retrieve information about events, series, and playlists ============================= @@ -101,9 +101,9 @@ static Jsons.Obj harvest( // Retrieve episodes from index. final SearchSourceBuilder q = new SearchSourceBuilder().query( QueryBuilders.boolQuery() - .must(QueryBuilders.termQuery(SearchResult.ORG, org)) - .must(QueryBuilders.rangeQuery(SearchResult.MODIFIED_DATE).gte(since.getTime())) - .must(QueryBuilders.termQuery(SearchResult.TYPE, SearchService.IndexEntryType.Episode))) + .must(QueryBuilders.termQuery(SearchResult.ORG, org)) + .must(QueryBuilders.rangeQuery(SearchResult.MODIFIED_DATE).gte(since.getTime())) + .must(QueryBuilders.termQuery(SearchResult.TYPE, SearchService.IndexEntryType.Episode))) .sort(SearchResult.MODIFIED_DATE, SortOrder.ASC) .size(preferredAmount + 1); final SearchResultList results = searchService.search(q); From abfecd9b4cc8efab496ca961e7f195b6fb78f747 Mon Sep 17 00:00:00 2001 From: Lars Kiesow Date: Sat, 7 Sep 2024 22:39:31 +0200 Subject: [PATCH 3/4] Allow Amberscript transcriptions to be attached as tracks Configuring the `amberscript-attach-transcription` workflow operation handler to add subtitles as tracks to the media package has no effect and they will still be added as attachments since the SubRib converter always returns attachments. This patch fixes the problem by simplifying the configuration parsing, converting the attachment to a track if necessary and also adding the recommended generator tags to the final media package element. --- ...ptAttachTranscriptionOperationHandler.java | 56 +++++++++++++------ 1 file changed, 38 insertions(+), 18 deletions(-) diff --git a/modules/transcription-service-workflowoperation/src/main/java/org/opencastproject/transcription/workflowoperation/AmberscriptAttachTranscriptionOperationHandler.java b/modules/transcription-service-workflowoperation/src/main/java/org/opencastproject/transcription/workflowoperation/AmberscriptAttachTranscriptionOperationHandler.java index b1a2be4bfb8..501b98a70b4 100644 --- a/modules/transcription-service-workflowoperation/src/main/java/org/opencastproject/transcription/workflowoperation/AmberscriptAttachTranscriptionOperationHandler.java +++ b/modules/transcription-service-workflowoperation/src/main/java/org/opencastproject/transcription/workflowoperation/AmberscriptAttachTranscriptionOperationHandler.java @@ -23,12 +23,12 @@ import org.opencastproject.caption.api.CaptionService; import org.opencastproject.job.api.Job; import org.opencastproject.job.api.JobContext; -import org.opencastproject.mediapackage.Attachment; import org.opencastproject.mediapackage.MediaPackage; import org.opencastproject.mediapackage.MediaPackageElement; import org.opencastproject.mediapackage.MediaPackageElementFlavor; import org.opencastproject.mediapackage.MediaPackageElementParser; import org.opencastproject.mediapackage.Track; +import org.opencastproject.mediapackage.track.TrackImpl; import org.opencastproject.serviceregistry.api.ServiceRegistry; import org.opencastproject.transcription.api.TranscriptionService; import org.opencastproject.transcription.api.TranscriptionServiceException; @@ -98,22 +98,7 @@ public WorkflowOperationResult start(final WorkflowInstance workflowInstance, Jo MediaPackageElementFlavor targetFlavor = tagsAndFlavors.getSingleTargetFlavor(); List targetTagOption = tagsAndFlavors.getTargetTags(); String captionFormatOption = StringUtils.trimToNull(operation.getConfiguration(TARGET_CAPTION_FORMAT)); - String typeUnparsed = StringUtils.trimToEmpty(operation.getConfiguration(TARGET_TYPE)); - MediaPackageElement.Type type = null; - if (!typeUnparsed.isEmpty()) { - // Case insensitive matching between user input (workflow config key) and enum value - for (MediaPackageElement.Type t : MediaPackageElement.Type.values()) { - if (t.name().equalsIgnoreCase(typeUnparsed)) { - type = t; - } - } - if (type == null || (type != Track.TYPE && type != Attachment.TYPE)) { - throw new IllegalArgumentException(String.format("The given type '%s' for mediapackage %s was illegal. Please" - + "check the operations' configuration keys.", type, mediaPackage.getIdentifier())); - } - } else { - type = Track.TYPE; - } + MediaPackageElement.Type type = getTargetType(operation.getConfiguration(TARGET_TYPE)); // If the target format is not specified, convert to vtt (default output format is srt) String format = (captionFormatOption != null) ? captionFormatOption : "vtt"; @@ -129,12 +114,28 @@ public WorkflowOperationResult start(final WorkflowInstance workflowInstance, Jo MediaPackageElement convertedTranscription = MediaPackageElementParser.getFromXml(job.getPayload()); workspace.delete(transcription.getURI()); + // The SubRip converter always returns Attachments. We may need to turn it into a Track + if (type == MediaPackageElement.Type.Track) { + var elem = new TrackImpl(); + elem.setURI(convertedTranscription.getURI()); + elem.setIdentifier(convertedTranscription.getIdentifier()); + elem.setMimeType(convertedTranscription.getMimeType()); + for (var tag: convertedTranscription.getTags()) { + elem.addTag(tag); + } + convertedTranscription = elem; + } + + convertedTranscription.addTag("generator-type:auto"); + convertedTranscription.addTag("generator:amberscript"); convertedTranscription.setFlavor(targetFlavor); for (String tag : targetTagOption) { convertedTranscription.addTag(tag); } mediaPackage.add(convertedTranscription); - logger.info("Added transcription to the media package {}: {}", mediaPackage, convertedTranscription.getURI()); + logger.info("Added transcription to the media package {} as {}: {}", mediaPackage, + convertedTranscription.getElementType(), + convertedTranscription.getURI()); } catch (TranscriptionServiceException e) { if (e.isCancel()) { @@ -149,6 +150,25 @@ public WorkflowOperationResult start(final WorkflowInstance workflowInstance, Jo return createResult(mediaPackage, Action.CONTINUE); } + /** + * Parse the target-type configuration and return the requested MediaPackageElement.Type + * @param typeUnparsed Configuration value + * @return Requested type + */ + private static MediaPackageElement.Type getTargetType(String typeUnparsed) { + if (StringUtils.isBlank(typeUnparsed)) { + return Track.TYPE; + } + if (MediaPackageElement.Type.Attachment.name().equalsIgnoreCase(typeUnparsed)) { + return MediaPackageElement.Type.Attachment; + } + if (MediaPackageElement.Type.Track.name().equalsIgnoreCase(typeUnparsed)) { + return MediaPackageElement.Type.Track; + } + throw new IllegalArgumentException(String.format("The requested type '%s' is illegal. Please" + + "check the operation's configuration keys.", typeUnparsed)); + } + @Reference(target = "(provider=amberscript)") public void setTranscriptionService(TranscriptionService service) { this.service = service; From e4653e927e2d275401aa94cad2e67e4e51199c94 Mon Sep 17 00:00:00 2001 From: Lars Kiesow Date: Sat, 7 Sep 2024 23:01:04 +0200 Subject: [PATCH 4/4] Fix Amberscript example workflows This patch fixes the amberscript example workflows in the documentation which were missing some mandatory configuration options (like the target flavor in the attach operation). --- .../amberscripttranscripts.md | 215 +++++++----------- 1 file changed, 78 insertions(+), 137 deletions(-) diff --git a/docs/guides/admin/docs/configuration/transcription.configuration/amberscripttranscripts.md b/docs/guides/admin/docs/configuration/transcription.configuration/amberscripttranscripts.md index 562b2b42edc..e460bb1e249 100644 --- a/docs/guides/admin/docs/configuration/transcription.configuration/amberscripttranscripts.md +++ b/docs/guides/admin/docs/configuration/transcription.configuration/amberscripttranscripts.md @@ -38,155 +38,96 @@ dropdowns below.
-amberscript-attach-transcription.xml - -```xml - - - amberscript-attach-transcripts - Attach caption/transcripts generated by AmberScript - - Attach transcription generated by the AmberScript service. - This is an internal workflow, started by the Transcription Service. - - - - - - - - - - - ${transcriptionJobId} - - vtt - engage-download - - - - - - - dublincore/*,security/* - engage-download - merge - false - - - - - - dublincore/*,security/* - engage-download - default - - - - - - - - */* - - - - - - - - true - - security/* - - - - - - +amberscript-attach-transcription.yaml + +```yaml +--- +id: amberscript-attach-transcription +title: Attach caption/transcripts generated by AmberScript +description: Attach transcription generated by the AmberScript service. + This is an internal workflow, started by the Transcription Service. + +operations: + + - id: amberscript-attach-transcription + fail-on-error: true + exception-handler-workflow: partial-error + description: Attach captions/transcription + configurations: + - transcription-job-id: ${transcriptionJobId} + - target-caption-format: vtt + - target-flavor: captions/delivery + - target-tags: engage-download + + - id: publish-engage + fail-on-error: true + exception-handler-workflow: partial-error + description: Distribute and publish to engage server + configurations: + - download-source-flavors: "dublincore/*,security/*" + - download-source-tags: engage-download + - strategy: merge + - check-availability: false + + - id: snapshot + fail-on-error: true + exception-handler-workflow: partial-error + description: Archive media package + configurations: + - source-flavors: "*/*" + + - id: cleanup + fail-on-error: false + description: Remove temporary processing artifacts + configurations: + - delete-external: false + - preserve-flavors: "security/*" ```
-amberscript-start-transcription.xml - -```xml - - - amberscript-start-transcription - Start AmberScript Transcription - - archive - - Start the AmberScript transcription - - - - - - captions/vtt - en - direct - - - - - - */source - audio/mp3 - transcript - audio-mp3 - - - - - - transcript - ${language} - ${jobtype} - ${skipFlavor} - - - - - - +amberscript-start-transcription.yaml + +```yaml +--- +id: amberscript-start-transcription +title: Start AmberScript Transcription +tags: + - archive + +description: Start AmberScript transcription + +operations: + - id: encode + fail-on-error: false + exception-handler-workflow: partial-error + description: Encoding audio for transcription + configurations: + - source-flavor: "*/source" + - target-flavor: audio/mp3 + - target-tags: transcript + - encoding-profile: audio-mp3 + + - id: amberscript-start-transcription + max-attempts: 3 + retry-strategy: hold + fail-on-error: false + exception-handler-workflow: partial-error + description: Start AmberScript transcription job + configurations: + - source-tag: transcript + - language: de + - jobtype: direct + - skip-if-flavor-exists: captions/vtt ```
### Step 4: Include workflow operations into your workflow -Integrate AmberScript workflow operations by including the provided workflow file `amberscript-start-transcription.xml` +Integrate AmberScript workflow operations by including the provided workflow file `amberscript-start-transcription.yaml` into your existing workflow: ```