Skip to content

Commit 906a7a5

Browse files
authored
fix(scopes): add new specific scopes for sponsor extra questions endp… (#524)
* fix(scopes): add new specific scopes for sponsor extra questions endpoints chore(utils): add new helper trait to perform endpoint migrations * chore: pr review
1 parent 31b61e7 commit 906a7a5

9 files changed

Lines changed: 385 additions & 6 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,4 @@ public/apc.php
3939
.claude/
4040
.nvmrc
4141
.codegraph
42+
docs/

app/Http/Controllers/Apis/Protected/Summit/OAuth2SummitSponsorApiController.php

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3205,11 +3205,11 @@ public function deleteSocialNetwork($summit_id, $sponsor_id, $social_network_id)
32053205
});
32063206
}
32073207

3208-
// Extra Questions
3208+
// Sponsor Extra Questions
32093209

32103210
#[OA\Get(
32113211
path: "/api/v1/summits/{id}/sponsors/{sponsor_id}/extra-questions",
3212-
description: "required-groups " . IGroup::SuperAdmins . ", " . IGroup::Administrators . ", " . IGroup::SummitAdministrators . ", " . IGroup::Sponsors,
3212+
description: "required-groups " . IGroup::SuperAdmins . ", " . IGroup::Administrators . ", " . IGroup::SummitAdministrators . ", " . IGroup::Sponsors . ", " . IGroup::SponsorExternalUsers,
32133213
summary: 'Read Sponsor Extra Questions',
32143214
operationId: 'getSponsorExtraQuestions',
32153215
tags: ['Sponsors'],
@@ -3219,13 +3219,15 @@ public function deleteSocialNetwork($summit_id, $sponsor_id, $social_network_id)
32193219
IGroup::Administrators,
32203220
IGroup::SummitAdministrators,
32213221
IGroup::Sponsors,
3222+
IGroup::SponsorExternalUsers,
32223223
]
32233224
],
32243225
security: [
32253226
[
32263227
'summit_sponsor_oauth2' => [
32273228
SummitScopes::ReadSummitData,
32283229
SummitScopes::ReadAllSummitData,
3230+
SummitScopes::ReadSponsorExtraQuestions,
32293231
]
32303232
]
32313233
],
@@ -3365,6 +3367,7 @@ function ($page, $per_page, $filter, $order, $applyExtraFilters) {
33653367
'summit_sponsor_oauth2' => [
33663368
SummitScopes::ReadSummitData,
33673369
SummitScopes::ReadAllSummitData,
3370+
SummitScopes::ReadSponsorExtraQuestions,
33683371
]
33693372
]
33703373
],
@@ -3421,6 +3424,7 @@ public function getMetadata($summit_id)
34213424
[
34223425
'summit_sponsor_oauth2' => [
34233426
SummitScopes::WriteSummitData,
3427+
SummitScopes::WriteSponsorExtraQuestions,
34243428
]
34253429
]
34263430
],
@@ -3510,6 +3514,7 @@ public function addExtraQuestion($summit_id, $sponsor_id)
35103514
'summit_sponsor_oauth2' => [
35113515
SummitScopes::ReadSummitData,
35123516
SummitScopes::ReadAllSummitData,
3517+
SummitScopes::ReadSponsorExtraQuestions,
35133518
]
35143519
]
35153520
],
@@ -3600,6 +3605,7 @@ public function getExtraQuestion($summit_id, $sponsor_id, $extra_question_id)
36003605
[
36013606
'summit_sponsor_oauth2' => [
36023607
SummitScopes::WriteSummitData,
3608+
SummitScopes::WriteSponsorExtraQuestions,
36033609
]
36043610
]
36053611
],
@@ -3695,6 +3701,7 @@ public function updateExtraQuestion($summit_id, $sponsor_id, $extra_question_id)
36953701
[
36963702
'summit_sponsor_oauth2' => [
36973703
SummitScopes::WriteSummitData,
3704+
SummitScopes::WriteSponsorExtraQuestions,
36983705
]
36993706
]
37003707
],
@@ -3780,6 +3787,7 @@ public function deleteExtraQuestion($summit_id, $sponsor_id, $extra_question_id)
37803787
[
37813788
'summit_sponsor_oauth2' => [
37823789
SummitScopes::WriteSummitData,
3790+
SummitScopes::WriteSponsorExtraQuestions,
37833791
]
37843792
]
37853793
],
@@ -3879,6 +3887,7 @@ function ($payload, $summit, $sponsor_id, $question_id) {
38793887
[
38803888
'summit_sponsor_oauth2' => [
38813889
SummitScopes::WriteSummitData,
3890+
SummitScopes::WriteSponsorExtraQuestions,
38823891
]
38833892
]
38843893
],
@@ -3987,6 +3996,7 @@ function ($value_id, $payload, $summit, $sponsor_id, $extra_question_id) {
39873996
[
39883997
'summit_sponsor_oauth2' => [
39893998
SummitScopes::WriteSummitData,
3999+
SummitScopes::WriteSponsorExtraQuestions,
39904000
]
39914001
]
39924002
],

app/Security/SummitScopes.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,5 +124,8 @@ final class SummitScopes
124124

125125
const WriteSummitsConfirmExternalOrders = SCOPE_BASE_REALM.'/summits/confirm-external-orders';
126126
const ReadSummitsConfirmExternalOrders = SCOPE_BASE_REALM.'/summits/read-external-orders';
127+
128+
const WriteSponsorExtraQuestions = SCOPE_BASE_REALM.'/summits/sponsors/extra-questions/write';
129+
const ReadSponsorExtraQuestions = SCOPE_BASE_REALM.'/summits/sponsors/extra-questions/read';
127130
}
128131

app/Swagger/Security/SponsorOAuth2Schema.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
SummitScopes::ReadSummitData => 'Read Summit Sponsor Data',
1818
SummitScopes::ReadAllSummitData => 'Read All Summit Sponsor Data',
1919
SummitScopes::WriteSummitData => 'Write Summit Sponsor Data',
20+
SummitScopes::ReadSponsorExtraQuestions => 'Read Summit Sponsor Extra Questions Data',
21+
SummitScopes::WriteSponsorExtraQuestions => 'Write Summit Sponsor Extra Questions Data',
2022
],
2123
),
2224
],
Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
<?php namespace Database\Migrations\Config;
2+
/**
3+
* Copyright 2026 OpenStack Foundation
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
* Unless required by applicable law or agreed to in writing, software
9+
* distributed under the License is distributed on an "AS IS" BASIS,
10+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
* See the License for the specific language governing permissions and
12+
* limitations under the License.
13+
**/
14+
15+
/**
16+
* Reusable SQL template helpers for config entity manager migrations.
17+
*
18+
* Provides idempotent INSERT and DELETE templates for api_endpoints, api_scopes,
19+
* endpoint_api_scopes, and endpoint_api_authz_groups tables.
20+
*
21+
* Usage:
22+
* final class VersionXXX extends AbstractMigration
23+
* {
24+
* use APIEndpointsMigrationHelper;
25+
*
26+
* public function up(Schema $schema): void
27+
* {
28+
* $this->addSql($this->insertApiScope('summits', $scopeName, $desc, $desc));
29+
* $this->addSql($this->insertEndpointScope('summits', $endpointName, $scopeName));
30+
* $this->addSql($this->insertEndpointAuthzGroup('summits', $endpointName, $groupSlug));
31+
* }
32+
* }
33+
*/
34+
trait APIEndpointsMigrationHelper
35+
{
36+
/**
37+
* Generate idempotent INSERT for api_endpoints table.
38+
*
39+
* @param string $apiName API identifier (e.g., 'summits')
40+
* @param string $endpointName Endpoint identifier (e.g., 'get-sponsor-extra-questions')
41+
* @param string $route Route pattern (e.g., '/api/v1/summits/{id}/sponsors/{sponsor_id}/extra-questions')
42+
* @param string $httpMethod Plain HTTP method string (e.g., 'GET', 'POST', 'PUT', 'DELETE')
43+
* @param bool $active Whether the endpoint is active (default: true)
44+
* @param bool $allowCors Whether to allow CORS (default: true, matches seedApiEndpoints behavior)
45+
* @param bool $allowCredentials Whether to allow credentials (default: true, matches seedApiEndpoints behavior)
46+
* @return string SQL INSERT statement
47+
*/
48+
protected function insertEndpoint(
49+
string $apiName,
50+
string $endpointName,
51+
string $route,
52+
string $httpMethod,
53+
bool $active = true,
54+
bool $allowCors = true,
55+
bool $allowCredentials = true
56+
): string {
57+
$activeInt = $active ? 1 : 0;
58+
$corsInt = $allowCors ? 1 : 0;
59+
$credentialsInt = $allowCredentials ? 1 : 0;
60+
61+
return <<<SQL
62+
INSERT INTO api_endpoints (api_id, name, route, http_method, active, allow_cors, allow_credentials, created_at, updated_at)
63+
SELECT a.id, '{$endpointName}', '{$route}', '{$httpMethod}', {$activeInt}, {$corsInt}, {$credentialsInt}, NOW(), NOW()
64+
FROM apis a
65+
WHERE a.name = '{$apiName}'
66+
AND NOT EXISTS (SELECT 1 FROM api_endpoints e WHERE e.api_id = a.id AND e.name = '{$endpointName}');
67+
SQL;
68+
}
69+
70+
/**
71+
* Generate DELETE for api_endpoints table.
72+
*
73+
* @param string $apiName API identifier
74+
* @param string $endpointName Endpoint identifier to delete
75+
* @return string SQL DELETE statement
76+
*/
77+
protected function deleteEndpoint(string $apiName, string $endpointName): string
78+
{
79+
return <<<SQL
80+
DELETE e FROM api_endpoints e
81+
INNER JOIN apis a ON a.id = e.api_id
82+
WHERE a.name = '{$apiName}'
83+
AND e.name = '{$endpointName}';
84+
SQL;
85+
}
86+
87+
/**
88+
* Generate idempotent INSERT for api_scopes table.
89+
*
90+
* @param string $apiName API identifier (e.g., 'summits', 'resource-server')
91+
* @param string $scopeName Full scope URI (e.g., 'https://example.com/summits/read')
92+
* @param string $shortDesc Short description for the scope
93+
* @param string $desc Full description for the scope
94+
* @param bool $active Whether the scope is active (default: true)
95+
* @param bool $default Whether the scope is default (default: false)
96+
* @param bool $system Whether the scope is a system scope (default: false)
97+
* @return string SQL INSERT statement
98+
*/
99+
protected function insertApiScope(
100+
string $apiName,
101+
string $scopeName,
102+
string $shortDesc,
103+
string $desc,
104+
bool $active = true,
105+
bool $default = false,
106+
bool $system = false
107+
): string {
108+
$activeInt = $active ? 1 : 0;
109+
$defaultInt = $default ? 1 : 0;
110+
$systemInt = $system ? 1 : 0;
111+
112+
return <<<SQL
113+
INSERT INTO api_scopes (api_id, name, short_description, description, active, `default`, `system`, created_at, updated_at)
114+
SELECT a.id, '{$scopeName}', '{$shortDesc}', '{$desc}', {$activeInt}, {$defaultInt}, {$systemInt}, NOW(), NOW()
115+
FROM apis a
116+
WHERE a.name = '{$apiName}'
117+
AND NOT EXISTS (SELECT 1 FROM api_scopes s WHERE s.api_id = a.id AND s.name = '{$scopeName}');
118+
SQL;
119+
}
120+
121+
/**
122+
* Generate idempotent INSERT for endpoint_api_scopes table.
123+
*
124+
* Links an endpoint to a scope by their names.
125+
*
126+
* @param string $apiName API identifier (e.g., 'summits')
127+
* @param string $endpointName Endpoint identifier (e.g., 'get-sponsor-extra-questions')
128+
* @param string $scopeName Full scope URI
129+
* @return string SQL INSERT statement
130+
*/
131+
protected function insertEndpointScope(string $apiName, string $endpointName, string $scopeName): string
132+
{
133+
return <<<SQL
134+
INSERT INTO endpoint_api_scopes (api_endpoint_id, scope_id, created_at, updated_at)
135+
SELECT e.id, s.id, NOW(), NOW()
136+
FROM api_endpoints e
137+
INNER JOIN apis a ON a.id = e.api_id
138+
INNER JOIN api_scopes s ON s.api_id = a.id
139+
WHERE a.name = '{$apiName}'
140+
AND e.name = '{$endpointName}'
141+
AND s.name = '{$scopeName}'
142+
AND NOT EXISTS (
143+
SELECT 1 FROM endpoint_api_scopes eas
144+
WHERE eas.api_endpoint_id = e.id AND eas.scope_id = s.id
145+
);
146+
SQL;
147+
}
148+
149+
/**
150+
* Generate idempotent INSERT for endpoint_api_authz_groups table.
151+
*
152+
* Links an endpoint to an authorization group by slug.
153+
*
154+
* @param string $apiName API identifier (e.g., 'summits')
155+
* @param string $endpointName Endpoint identifier
156+
* @param string $groupSlug Group slug (e.g., 'sponsors-external-users')
157+
* @return string SQL INSERT statement
158+
*/
159+
protected function insertEndpointAuthzGroup(string $apiName, string $endpointName, string $groupSlug): string
160+
{
161+
return <<<SQL
162+
INSERT INTO endpoint_api_authz_groups (api_endpoint_id, group_slug, created_at, updated_at)
163+
SELECT e.id, '{$groupSlug}', NOW(), NOW()
164+
FROM api_endpoints e
165+
INNER JOIN apis a ON a.id = e.api_id
166+
WHERE a.name = '{$apiName}'
167+
AND e.name = '{$endpointName}'
168+
AND NOT EXISTS (
169+
SELECT 1 FROM endpoint_api_authz_groups eag
170+
WHERE eag.api_endpoint_id = e.id AND eag.group_slug = '{$groupSlug}'
171+
);
172+
SQL;
173+
}
174+
175+
/**
176+
* Generate DELETE for endpoint_api_authz_groups table.
177+
*
178+
* @param string $apiName API identifier
179+
* @param string $endpointName Endpoint identifier
180+
* @param string $groupSlug Group slug to remove
181+
* @return string SQL DELETE statement
182+
*/
183+
protected function deleteEndpointAuthzGroup(string $apiName, string $endpointName, string $groupSlug): string
184+
{
185+
return <<<SQL
186+
DELETE eag FROM endpoint_api_authz_groups eag
187+
INNER JOIN api_endpoints e ON e.id = eag.api_endpoint_id
188+
INNER JOIN apis a ON a.id = e.api_id
189+
WHERE a.name = '{$apiName}'
190+
AND e.name = '{$endpointName}'
191+
AND eag.group_slug = '{$groupSlug}';
192+
SQL;
193+
}
194+
195+
/**
196+
* Generate DELETE for endpoint_api_scopes table (all associations for given scopes).
197+
*
198+
* Constrained by API to prevent removing associations for other APIs that may
199+
* reuse the same scope URI (api_scopes.name has no global uniqueness constraint).
200+
*
201+
* @param string $apiName API identifier (e.g., 'summits')
202+
* @param array $scopes List of scope URIs to remove associations for
203+
* @return string SQL DELETE statement
204+
*/
205+
protected function deleteScopesEndpoints(string $apiName, array $scopes): string
206+
{
207+
$scopeList = "'" . implode("', '", $scopes) . "'";
208+
return <<<SQL
209+
DELETE eas FROM endpoint_api_scopes eas
210+
INNER JOIN api_scopes s ON s.id = eas.scope_id
211+
INNER JOIN apis a ON a.id = s.api_id
212+
WHERE a.name = '{$apiName}'
213+
AND s.name IN ({$scopeList});
214+
SQL;
215+
}
216+
217+
/**
218+
* Generate DELETE for api_scopes table.
219+
*
220+
* @param string $apiName API identifier
221+
* @param array $scopes List of scope URIs to delete
222+
* @return string SQL DELETE statement
223+
*/
224+
protected function deleteApiScopes(string $apiName, array $scopes): string
225+
{
226+
$scopeList = "'" . implode("', '", $scopes) . "'";
227+
return <<<SQL
228+
DELETE s FROM api_scopes s
229+
INNER JOIN apis a ON a.id = s.api_id
230+
WHERE a.name = '{$apiName}'
231+
AND s.name IN ({$scopeList});
232+
SQL;
233+
}
234+
}

0 commit comments

Comments
 (0)