From cff665b9568e88c2c9a314bfd5bcdc030b3f6e97 Mon Sep 17 00:00:00 2001 From: smarcet Date: Wed, 8 Apr 2026 12:54:12 -0300 Subject: [PATCH 1/2] fix(scopes): add new specific scopes for sponsor extra questions endpoints chore(utils): add new helper trait to perform endpoint migrations --- .gitignore | 1 + .../OAuth2SummitSponsorApiController.php | 11 +- app/Security/SummitScopes.php | 3 + app/Swagger/Security/SponsorOAuth2Schema.php | 2 + .../config/APIEndpointsMigrationHelper.php | 228 ++++++++++++++++++ .../config/Version20260408143000.php | 108 +++++++++ database/seeders/ApiEndpointsSeeder.php | 16 +- database/seeders/ApiScopesSeeder.php | 11 + routes/api_v1.php | 2 +- 9 files changed, 377 insertions(+), 5 deletions(-) create mode 100644 database/migrations/config/APIEndpointsMigrationHelper.php create mode 100644 database/migrations/config/Version20260408143000.php diff --git a/.gitignore b/.gitignore index 7a9bebdf6..e90b4d509 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,4 @@ public/apc.php .claude/ .nvmrc .codegraph +docs/ diff --git a/app/Http/Controllers/Apis/Protected/Summit/OAuth2SummitSponsorApiController.php b/app/Http/Controllers/Apis/Protected/Summit/OAuth2SummitSponsorApiController.php index e99e12ba5..ccff868ad 100644 --- a/app/Http/Controllers/Apis/Protected/Summit/OAuth2SummitSponsorApiController.php +++ b/app/Http/Controllers/Apis/Protected/Summit/OAuth2SummitSponsorApiController.php @@ -3205,7 +3205,7 @@ public function deleteSocialNetwork($summit_id, $sponsor_id, $social_network_id) }); } - // Extra Questions + // Sponsor Extra Questions #[OA\Get( path: "/api/v1/summits/{id}/sponsors/{sponsor_id}/extra-questions", @@ -3219,6 +3219,7 @@ public function deleteSocialNetwork($summit_id, $sponsor_id, $social_network_id) IGroup::Administrators, IGroup::SummitAdministrators, IGroup::Sponsors, + IGroup::SponsorExternalUsers, ] ], security: [ @@ -3226,6 +3227,7 @@ public function deleteSocialNetwork($summit_id, $sponsor_id, $social_network_id) 'summit_sponsor_oauth2' => [ SummitScopes::ReadSummitData, SummitScopes::ReadAllSummitData, + SummitScopes::ReadSponsorExtraQuestions, ] ] ], @@ -3365,6 +3367,7 @@ function ($page, $per_page, $filter, $order, $applyExtraFilters) { 'summit_sponsor_oauth2' => [ SummitScopes::ReadSummitData, SummitScopes::ReadAllSummitData, + SummitScopes::ReadSponsorExtraQuestions, ] ] ], @@ -3510,6 +3513,7 @@ public function addExtraQuestion($summit_id, $sponsor_id) 'summit_sponsor_oauth2' => [ SummitScopes::ReadSummitData, SummitScopes::ReadAllSummitData, + SummitScopes::ReadSponsorExtraQuestions, ] ] ], @@ -3600,6 +3604,7 @@ public function getExtraQuestion($summit_id, $sponsor_id, $extra_question_id) [ 'summit_sponsor_oauth2' => [ SummitScopes::WriteSummitData, + SummitScopes::WriteSponsorExtraQuestions, ] ] ], @@ -3695,6 +3700,7 @@ public function updateExtraQuestion($summit_id, $sponsor_id, $extra_question_id) [ 'summit_sponsor_oauth2' => [ SummitScopes::WriteSummitData, + SummitScopes::WriteSponsorExtraQuestions, ] ] ], @@ -3780,6 +3786,7 @@ public function deleteExtraQuestion($summit_id, $sponsor_id, $extra_question_id) [ 'summit_sponsor_oauth2' => [ SummitScopes::WriteSummitData, + SummitScopes::WriteSponsorExtraQuestions, ] ] ], @@ -3879,6 +3886,7 @@ function ($payload, $summit, $sponsor_id, $question_id) { [ 'summit_sponsor_oauth2' => [ SummitScopes::WriteSummitData, + SummitScopes::WriteSponsorExtraQuestions, ] ] ], @@ -3987,6 +3995,7 @@ function ($value_id, $payload, $summit, $sponsor_id, $extra_question_id) { [ 'summit_sponsor_oauth2' => [ SummitScopes::WriteSummitData, + SummitScopes::WriteSponsorExtraQuestions, ] ] ], diff --git a/app/Security/SummitScopes.php b/app/Security/SummitScopes.php index 75a71ab7d..04b4d21be 100644 --- a/app/Security/SummitScopes.php +++ b/app/Security/SummitScopes.php @@ -124,5 +124,8 @@ final class SummitScopes const WriteSummitsConfirmExternalOrders = SCOPE_BASE_REALM.'/summits/confirm-external-orders'; const ReadSummitsConfirmExternalOrders = SCOPE_BASE_REALM.'/summits/read-external-orders'; + + const WriteSponsorExtraQuestions = SCOPE_BASE_REALM.'/summits/sponsors/extra-questions/write'; + const ReadSponsorExtraQuestions = SCOPE_BASE_REALM.'/summits/sponsors/extra-questions/read'; } diff --git a/app/Swagger/Security/SponsorOAuth2Schema.php b/app/Swagger/Security/SponsorOAuth2Schema.php index 81f880857..5dbf82407 100644 --- a/app/Swagger/Security/SponsorOAuth2Schema.php +++ b/app/Swagger/Security/SponsorOAuth2Schema.php @@ -17,6 +17,8 @@ SummitScopes::ReadSummitData => 'Read Summit Sponsor Data', SummitScopes::ReadAllSummitData => 'Read All Summit Sponsor Data', SummitScopes::WriteSummitData => 'Write Summit Sponsor Data', + SummitScopes::ReadSponsorExtraQuestions => 'Read Summit Sponsor Extra Questions Data', + SummitScopes::WriteSponsorExtraQuestions => 'Write Summit Sponsor Extra Questions Data', ], ), ], diff --git a/database/migrations/config/APIEndpointsMigrationHelper.php b/database/migrations/config/APIEndpointsMigrationHelper.php new file mode 100644 index 000000000..44ef76e7b --- /dev/null +++ b/database/migrations/config/APIEndpointsMigrationHelper.php @@ -0,0 +1,228 @@ +addSql($this->insertApiScope('summits', $scopeName, $desc, $desc)); + * $this->addSql($this->insertEndpointScope('summits', $endpointName, $scopeName)); + * $this->addSql($this->insertEndpointAuthzGroup('summits', $endpointName, $groupSlug)); + * } + * } + */ +trait APIEndpointsMigrationHelper +{ + /** + * Generate idempotent INSERT for api_endpoints table. + * + * @param string $apiName API identifier (e.g., 'summits') + * @param string $endpointName Endpoint identifier (e.g., 'get-sponsor-extra-questions') + * @param string $route Route pattern (e.g., '/api/v1/summits/{id}/sponsors/{sponsor_id}/extra-questions') + * @param string $httpMethod HTTP method as serialized PHP array (e.g., 'a:1:{i:0;s:3:"GET";}') + * @param bool $active Whether the endpoint is active (default: true) + * @param bool $allowCors Whether to allow CORS (default: false) + * @param bool $allowCredentials Whether to allow credentials (default: false) + * @return string SQL INSERT statement + */ + protected function insertEndpoint( + string $apiName, + string $endpointName, + string $route, + string $httpMethod, + bool $active = true, + bool $allowCors = false, + bool $allowCredentials = false + ): string { + $activeInt = $active ? 1 : 0; + $corsInt = $allowCors ? 1 : 0; + $credentialsInt = $allowCredentials ? 1 : 0; + + return <<addSql($this->insertApiScope( + self::API_NAME, + $readScope, + 'Read Summit Sponsor Extra Questions Data', + 'Read Summit Sponsor Extra Questions Data' + )); + $this->addSql($this->insertApiScope( + self::API_NAME, + $writeScope, + 'Write Summit Sponsor Extra Questions Data', + 'Write Summit Sponsor Extra Questions Data' + )); + + // 2. Insert endpoint_api_scopes associations + $associations = [ + ['get-sponsor-extra-questions', $readScope], + ['add-sponsor-extra-question', $writeScope], + ['get-sponsor-extra-question', $readScope], + ['update-sponsor-extra-question', $writeScope], + ['delete-sponsor-extra-question', $writeScope], + ['get-sponsor-extra-questions-metadata', $readScope], + ['add-sponsor-extra-question-value', $writeScope], + ['update-sponsor-extra-question-value', $writeScope], + ['delete-sponsor-extra-question-value', $writeScope], + ]; + + foreach ($associations as [$endpointName, $scopeName]) { + $this->addSql($this->insertEndpointScope(self::API_NAME, $endpointName, $scopeName)); + } + + // 3. Insert endpoint_api_authz_groups + $this->addSql($this->insertEndpointAuthzGroup(self::API_NAME, 'get-sponsor-extra-questions', $externalGroupSlug)); + } + + /** + * @param Schema $schema + */ + public function down(Schema $schema): void + { + $readScope = SummitScopes::ReadSponsorExtraQuestions; + $writeScope = SummitScopes::WriteSponsorExtraQuestions; + $externalGroupSlug = IGroup::SponsorExternalUsers; + + // Reverse order: authz groups → endpoint scopes → api scopes + $this->addSql($this->deleteEndpointAuthzGroup(self::API_NAME, 'get-sponsor-extra-questions', $externalGroupSlug)); + $this->addSql($this->deleteScopesEndpoints([$readScope, $writeScope])); + $this->addSql($this->deleteApiScopes(self::API_NAME, [$readScope, $writeScope])); + } +} diff --git a/database/seeders/ApiEndpointsSeeder.php b/database/seeders/ApiEndpointsSeeder.php index feb38babe..78e1f35f9 100644 --- a/database/seeders/ApiEndpointsSeeder.php +++ b/database/seeders/ApiEndpointsSeeder.php @@ -2506,13 +2506,15 @@ private function seedSummitEndpoints() 'http_method' => 'GET', 'scopes' => [ SummitScopes::ReadSummitData, - SummitScopes::ReadAllSummitData + SummitScopes::ReadAllSummitData, + SummitScopes::ReadSponsorExtraQuestions, ], 'authz_groups' => [ IGroup::SuperAdmins, IGroup::Administrators, IGroup::SummitAdministrators, IGroup::Sponsors, + IGroup::SponsorExternalUsers, ] ], [ @@ -2521,6 +2523,7 @@ private function seedSummitEndpoints() 'http_method' => 'POST', 'scopes' => [ SummitScopes::WriteSummitData, + SummitScopes::WriteSponsorExtraQuestions, ], 'authz_groups' => [ IGroup::SuperAdmins, @@ -2536,7 +2539,8 @@ private function seedSummitEndpoints() 'http_method' => 'GET', 'scopes' => [ SummitScopes::ReadSummitData, - SummitScopes::ReadAllSummitData + SummitScopes::ReadAllSummitData, + SummitScopes::ReadSponsorExtraQuestions, ], 'authz_groups' => [ IGroup::SuperAdmins, @@ -2552,6 +2556,7 @@ private function seedSummitEndpoints() 'http_method' => 'PUT', 'scopes' => [ SummitScopes::WriteSummitData, + SummitScopes::WriteSponsorExtraQuestions, ], 'authz_groups' => [ IGroup::SuperAdmins, @@ -2567,6 +2572,7 @@ private function seedSummitEndpoints() 'http_method' => 'DELETE', 'scopes' => [ SummitScopes::WriteSummitData, + SummitScopes::WriteSponsorExtraQuestions, ], 'authz_groups' => [ IGroup::SuperAdmins, @@ -2582,7 +2588,8 @@ private function seedSummitEndpoints() 'http_method' => 'GET', 'scopes' => [ SummitScopes::ReadSummitData, - SummitScopes::ReadAllSummitData + SummitScopes::ReadAllSummitData, + SummitScopes::ReadSponsorExtraQuestions, ], 'authz_groups' => [ IGroup::SuperAdmins, @@ -2598,6 +2605,7 @@ private function seedSummitEndpoints() 'http_method' => 'POST', 'scopes' => [ SummitScopes::WriteSummitData, + SummitScopes::WriteSponsorExtraQuestions, ], 'authz_groups' => [ IGroup::SuperAdmins, @@ -2613,6 +2621,7 @@ private function seedSummitEndpoints() 'http_method' => 'PUT', 'scopes' => [ SummitScopes::WriteSummitData, + SummitScopes::WriteSponsorExtraQuestions, ], 'authz_groups' => [ IGroup::SuperAdmins, @@ -2628,6 +2637,7 @@ private function seedSummitEndpoints() 'http_method' => 'DELETE', 'scopes' => [ SummitScopes::WriteSummitData, + SummitScopes::WriteSponsorExtraQuestions, ], 'authz_groups' => [ IGroup::SuperAdmins, diff --git a/database/seeders/ApiScopesSeeder.php b/database/seeders/ApiScopesSeeder.php index d6638f6b7..6941e8956 100644 --- a/database/seeders/ApiScopesSeeder.php +++ b/database/seeders/ApiScopesSeeder.php @@ -393,7 +393,18 @@ private function seedSummitScopes() 'name' => SummitScopes::WriteAttendeeNotesData, 'short_description' => 'Write Attendee Notes Data', 'description' => 'Grants write access for Attendee Notes Data', + ], + [ + 'name' => SummitScopes::ReadSponsorExtraQuestions, + 'short_description' => 'Read Summit Sponsor Extra Questions Data', + 'description' => 'Read Summit Sponsor Extra Questions Data', + ], + [ + 'name' => SummitScopes::WriteSponsorExtraQuestions, + 'short_description' => 'Write Summit Sponsor Extra Questions Data', + 'description' => 'Write Summit Sponsor Extra Questions Data', ] + ]; foreach ($scopes as $scope_info) { diff --git a/routes/api_v1.php b/routes/api_v1.php index 6c9d66b20..fa9ba05e7 100644 --- a/routes/api_v1.php +++ b/routes/api_v1.php @@ -1297,7 +1297,7 @@ }); }); - // extra questions + // sponsor extra questions Route::group(['prefix' => 'extra-questions'], function () { Route::get('', ['middleware' => 'auth.user', 'uses' => 'OAuth2SummitSponsorApiController@getExtraQuestions']); Route::post('', ['middleware' => 'auth.user', 'uses' => 'OAuth2SummitSponsorApiController@addExtraQuestion']); From 5424bd5b5420d5d8f43732f345d3fc7fedc1cd84 Mon Sep 17 00:00:00 2001 From: smarcet Date: Wed, 8 Apr 2026 13:50:37 -0300 Subject: [PATCH 2/2] chore: pr review --- .../OAuth2SummitSponsorApiController.php | 3 ++- .../config/APIEndpointsMigrationHelper.php | 22 ++++++++++++------- .../config/Version20260408143000.php | 2 +- 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/app/Http/Controllers/Apis/Protected/Summit/OAuth2SummitSponsorApiController.php b/app/Http/Controllers/Apis/Protected/Summit/OAuth2SummitSponsorApiController.php index ccff868ad..5ae4be982 100644 --- a/app/Http/Controllers/Apis/Protected/Summit/OAuth2SummitSponsorApiController.php +++ b/app/Http/Controllers/Apis/Protected/Summit/OAuth2SummitSponsorApiController.php @@ -3209,7 +3209,7 @@ public function deleteSocialNetwork($summit_id, $sponsor_id, $social_network_id) #[OA\Get( path: "/api/v1/summits/{id}/sponsors/{sponsor_id}/extra-questions", - description: "required-groups " . IGroup::SuperAdmins . ", " . IGroup::Administrators . ", " . IGroup::SummitAdministrators . ", " . IGroup::Sponsors, + description: "required-groups " . IGroup::SuperAdmins . ", " . IGroup::Administrators . ", " . IGroup::SummitAdministrators . ", " . IGroup::Sponsors . ", " . IGroup::SponsorExternalUsers, summary: 'Read Sponsor Extra Questions', operationId: 'getSponsorExtraQuestions', tags: ['Sponsors'], @@ -3424,6 +3424,7 @@ public function getMetadata($summit_id) [ 'summit_sponsor_oauth2' => [ SummitScopes::WriteSummitData, + SummitScopes::WriteSponsorExtraQuestions, ] ] ], diff --git a/database/migrations/config/APIEndpointsMigrationHelper.php b/database/migrations/config/APIEndpointsMigrationHelper.php index 44ef76e7b..3ded89412 100644 --- a/database/migrations/config/APIEndpointsMigrationHelper.php +++ b/database/migrations/config/APIEndpointsMigrationHelper.php @@ -39,10 +39,10 @@ trait APIEndpointsMigrationHelper * @param string $apiName API identifier (e.g., 'summits') * @param string $endpointName Endpoint identifier (e.g., 'get-sponsor-extra-questions') * @param string $route Route pattern (e.g., '/api/v1/summits/{id}/sponsors/{sponsor_id}/extra-questions') - * @param string $httpMethod HTTP method as serialized PHP array (e.g., 'a:1:{i:0;s:3:"GET";}') + * @param string $httpMethod Plain HTTP method string (e.g., 'GET', 'POST', 'PUT', 'DELETE') * @param bool $active Whether the endpoint is active (default: true) - * @param bool $allowCors Whether to allow CORS (default: false) - * @param bool $allowCredentials Whether to allow credentials (default: false) + * @param bool $allowCors Whether to allow CORS (default: true, matches seedApiEndpoints behavior) + * @param bool $allowCredentials Whether to allow credentials (default: true, matches seedApiEndpoints behavior) * @return string SQL INSERT statement */ protected function insertEndpoint( @@ -51,8 +51,8 @@ protected function insertEndpoint( string $route, string $httpMethod, bool $active = true, - bool $allowCors = false, - bool $allowCredentials = false + bool $allowCors = true, + bool $allowCredentials = true ): string { $activeInt = $active ? 1 : 0; $corsInt = $allowCors ? 1 : 0; @@ -195,16 +195,22 @@ protected function deleteEndpointAuthzGroup(string $apiName, string $endpointNam /** * Generate DELETE for endpoint_api_scopes table (all associations for given scopes). * - * @param array $scopes List of scope URIs to remove associations for + * Constrained by API to prevent removing associations for other APIs that may + * reuse the same scope URI (api_scopes.name has no global uniqueness constraint). + * + * @param string $apiName API identifier (e.g., 'summits') + * @param array $scopes List of scope URIs to remove associations for * @return string SQL DELETE statement */ - protected function deleteScopesEndpoints(array $scopes): string + protected function deleteScopesEndpoints(string $apiName, array $scopes): string { $scopeList = "'" . implode("', '", $scopes) . "'"; return <<addSql($this->deleteEndpointAuthzGroup(self::API_NAME, 'get-sponsor-extra-questions', $externalGroupSlug)); - $this->addSql($this->deleteScopesEndpoints([$readScope, $writeScope])); + $this->addSql($this->deleteScopesEndpoints(self::API_NAME, [$readScope, $writeScope])); $this->addSql($this->deleteApiScopes(self::API_NAME, [$readScope, $writeScope])); } }