Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/push.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,9 @@ jobs:
- { name: "Repositories", filter: "tests/Repositories/" }
- { name: "Services", filter: "tests/Unit/Services/" }
- { name: "CacheOptimizations", filter: "--filter '(PresentationSpeakerCacheTest|ResourceServerContextTest)'" }
# Named by path because no job in this matrix runs the tests/ root, only its
# subdirectories - a file added there runs nowhere unless it is listed here.
- { name: "PresentationMediaUploads", filter: "tests/PresentationMediaUploadsTest.php tests/PresentationMediaUploadsVisibilityTest.php tests/PresentationSerializerCacheKeyTest.php" }
env:
OTEL_SERVICE_ENABLED: false
APP_ENV: testing
Expand Down
269 changes: 201 additions & 68 deletions app/ModelSerializers/Summit/Presentation/PresentationSerializer.php

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions app/Security/SummitScopes.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ final class SummitScopes
const ReadSummitData = SCOPE_BASE_REALM.'/summits/read';
const ReadAllSummitData = SCOPE_BASE_REALM.'/summits/read/all';
const ReadOverflowEvents = SCOPE_BASE_REALM.'/summits/events/overflow/read';
const ReadAllPresentationMediaUploads = SCOPE_BASE_REALM.'/summits/presentations/media-uploads/read/all';

// me
const MeRead = SCOPE_BASE_REALM.'/me/read';
Expand Down
61 changes: 61 additions & 0 deletions database/migrations/config/Version20260804120000.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?php namespace Database\Migrations\Config;
/**
* Copyright 2026 OpenStack Foundation
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/

use App\Security\SummitScopes;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;

/**
* Register the ReadAllPresentationMediaUploads scope.
*
* Adds 1 api_scopes row and NO endpoint association, on purpose: endpoint scopes are
* validated with array_intersect (any-of, see OAuth2BearerAccessTokenRequestValidator:193),
* so associating this scope with an existing endpoint would admit a token holding only
* this scope to that endpoint - widening access rather than narrowing it. The scope is a
* privilege modifier over endpoints that already exist, read straight off the token by
* PresentationSerializer::getMediaUploadsSerializerType(), which never consults
* endpoint_api_scopes.
*
* The registry that actually issues the token is openstackid's oauth2_api_scope. This row
* is convention and discoverability on the summit-api side; the scope still has to be
* created there and granted to the content-snapshot client for a token to carry it.
*
* Idempotent via WHERE NOT EXISTS.
*/
final class Version20260804120000 extends AbstractMigration
{
use APIEndpointsMigrationHelper;

private const API_NAME = 'summits';

public function getDescription(): string
{
return 'Register ReadAllPresentationMediaUploads scope (no endpoint association).';
}

public function up(Schema $schema): void
{
$this->addSql($this->insertApiScope(
self::API_NAME,
SummitScopes::ReadAllPresentationMediaUploads,
'Read All Presentation Media Uploads',
'Grants read access to presentation media uploads regardless of display_on_site, for trusted service accounts feeding the content pipeline'
));
}

public function down(Schema $schema): void
{
$this->addSql($this->deleteApiScopes(self::API_NAME, [SummitScopes::ReadAllPresentationMediaUploads]));
}
}
5 changes: 5 additions & 0 deletions database/seeders/ApiScopesSeeder.php
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,11 @@ private function seedSummitScopes()
'short_description' => 'Read Summit Overflow Events Data',
'description' => 'Grants read only access to published summit events currently in OVERFLOW occupancy, including overflow streaming URLs and tokens',
],
[
'name' => SummitScopes::ReadAllPresentationMediaUploads,
'short_description' => 'Read All Presentation Media Uploads',
'description' => 'Grants read access to presentation media uploads regardless of display_on_site, for trusted service accounts feeding the content pipeline',
],
[
'name' => SummitScopes::MeRead,
'short_description' => 'Get own summit member data',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@
use models\summit\Presentation;
use models\summit\SummitMediaUploadType;
/**
* Class PresentationMediaUploadsTests
* Class PresentationMediaUploadsTest
*/
class PresentationMediaUploadsTests
class PresentationMediaUploadsTest
extends ProtectedApiTestCase
{
use InsertSummitTestData;
Expand All @@ -44,17 +44,32 @@ protected function setUp():void
{
parent::setUp();
self::$media_file_type_repository = EntityManager::getRepository(SummitMediaFileType::class);
$types = self::$media_file_type_repository->findAll();
self::insertSummitTestData();

// Built here rather than read from the repository: insertSummitTestData() opens with
// DELETE FROM SummitMediaFileType, so anything fetched before it is a detached row by
// the time we flush. It cannot reuse self::$default_media_file_type either - that one
// carries ".PDF", and SummitMediaUploadType::isValidExtension() compares
// strtoupper($ext) against explode('|', ...), so a leading dot never matches.
$media_file_type = new SummitMediaFileType();
$media_file_type->setName("PNG_".rand(1, 100));
$media_file_type->setDescription("PNG");
$media_file_type->setAllowedExtensions("PNG");
self::$em->persist($media_file_type);

self::$media_upload_type = new SummitMediaUploadType();
self::$media_upload_type->setType($types[0]);
self::$media_upload_type->setType($media_file_type);
self::$media_upload_type->setName('TEST');
self::$media_upload_type->setDescription("TEST");
self::$media_upload_type->setMaxSize(2048);
self::$media_upload_type->setMinUploadsQty(2);
self::$media_upload_type->setMaxUploadsQty(4);
self::$media_upload_type->setPrivateStorageType(\App\Models\Utils\IStorageTypesConstants::DropBox);
self::$media_upload_type->setPublicStorageType(\App\Models\Utils\IStorageTypesConstants::Swift);
self::$media_upload_type->setPrivateStorageType(\App\Models\Utils\IStorageTypesConstants::Local);
// Local, not Swift: serializing public_url builds a download strategy for whatever the
// type declares, and the Swift one needs an authUrl that neither this container nor CI
// provides. The assertion is about public_url being serialized at all, not about which
// backend serves it.
self::$media_upload_type->setPublicStorageType(\App\Models\Utils\IStorageTypesConstants::Local);

self::$presentation = new Presentation();
$event_types = self::$summit->getEventTypes();
Expand Down
Loading
Loading