Skip to content

Commit 0ee1232

Browse files
committed
feat(PLATECO-111): add allowed app names by regular expressions
1 parent b7cde74 commit 0ee1232

7 files changed

Lines changed: 172 additions & 5 deletions

File tree

.github/CODEOWNERS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
* @MacPaw/platform-backend-engineers

README.md

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,38 @@ Add this config to `config/packages/schema_context.yaml`:
3535

3636
```yaml
3737
schema_context:
38-
app_name: '%env(APP_NAME)%' # Application name
39-
header_name: 'X-Tenant' # Request header to extract schema name
40-
default_schema: 'public' # Default schema to fallback to
41-
allowed_app_names: ['develop', 'staging', 'test'] # App names where schema context is allowed to change
38+
# Name of your application (required). Used to decide whether incoming
39+
# requests are allowed to override the schema from baggage.
40+
app_name: '%env(APP_NAME)%'
41+
42+
# The KEY inside the RFC 8941 "baggage" HTTP header that holds the schema name.
43+
# This is NOT an HTTP header name itself. Default: 'X-Schema'
44+
header_name: 'X-Schema'
45+
46+
# Fallback schema used when baggage doesn't contain the key or value is empty
47+
default_schema: 'public'
48+
49+
# Explicit app names that ARE allowed to take schema from baggage
50+
allowed_app_names: ['develop', 'staging', 'test']
51+
52+
# Additionally allow app names by regex patterns (evaluated with preg_match)
53+
# Example: allow PR preview apps like "pr-123"
54+
allowed_app_names_regex: ['/^pr-\d+$/']
55+
```
56+
57+
Notes:
58+
- The bundle reads the standard "baggage" HTTP header and expects a comma-separated list of key=value pairs.
59+
- It looks up the schema by the configured header_name key inside that baggage.
60+
61+
Example incoming HTTP headers:
62+
```
63+
GET / HTTP/1.1
64+
Host: example.test
65+
baggage: X-Schema=tenant_42,traceId=abc123
4266
```
67+
68+
With the config above, the bundle will resolve schema "tenant_42" and store the entire baggage map.
69+
4370
### 2. Set Environment Parameters
4471
If you're using .env, define the app name:
4572

config/services.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ services:
1919
$defaultSchema: '%schema_context.default_schema%'
2020
$appName: '%schema_context.app_name%'
2121
$allowedAppNames: '%schema_context.allowed_app_names%'
22+
$allowedAppNamesRegex: '%schema_context.allowed_app_names_regex%'
2223
tags:
2324
- { name: kernel.event_subscriber }
2425

src/DependencyInjection/Configuration.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ public function getConfigTreeBuilder(): TreeBuilder
2222
->scalarPrototype()->end()
2323
->defaultValue([])
2424
->end()
25+
->arrayNode('allowed_app_names_regex')
26+
->scalarPrototype()->end()
27+
->defaultValue([])
28+
->end()
2529
->end();
2630

2731
return $treeBuilder;

src/DependencyInjection/SchemaContextExtension.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ public function load(array $configs, ContainerBuilder $container)
2020
$container->setParameter('schema_context.default_schema', $config['default_schema']);
2121
$container->setParameter('schema_context.app_name', $config['app_name']);
2222
$container->setParameter('schema_context.allowed_app_names', $config['allowed_app_names']);
23+
$container->setParameter('schema_context.allowed_app_names_regex', $config['allowed_app_names_regex']);
2324

2425
$loader = new YamlFileLoader($container, new FileLocator(__DIR__ . '/../../config'));
2526

src/EventListener/BaggageRequestListener.php

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ public function __construct(
2020
private string $appName,
2121
/** @var string[] */
2222
private array $allowedAppNames,
23+
/** @var string[] */
24+
private array $allowedAppNamesRegex,
2325
) {
2426
}
2527

@@ -54,6 +56,18 @@ public function onKernelRequest(RequestEvent $event): void
5456

5557
private function isAllowedAppName(): bool
5658
{
57-
return in_array($this->appName, $this->allowedAppNames, true);
59+
$isAppNameAllowed = in_array($this->appName, $this->allowedAppNames, true);
60+
61+
if ($isAppNameAllowed === true || count($this->allowedAppNamesRegex) === 0) {
62+
return $isAppNameAllowed;
63+
}
64+
65+
foreach ($this->allowedAppNamesRegex as $pattern) {
66+
if (preg_match($pattern, $this->appName) === 1) {
67+
return true;
68+
}
69+
}
70+
71+
return false;
5872
}
5973
}

tests/EventListener/BaggageRequestListenerTest.php

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ public function testBaggageFromHeaderIsSet(): void
2525
'default',
2626
'test-app',
2727
['test-app'],
28+
[],
2829
);
2930

3031
$request = new Request([], [], [], [], [], ['HTTP_BAGGAGE' => 'X-Schema=tenant1']);
@@ -50,6 +51,7 @@ public function testBaggageFromHeaderIsSetWithMultiplyParameters(): void
5051
'default',
5152
'test-app',
5253
['test-app'],
54+
[],
5355
);
5456

5557
$request = new Request([], [], [], [], [], ['HTTP_BAGGAGE' => 'X-Schema= tenant1 ,test , foo=bar']);
@@ -77,6 +79,7 @@ public function testDefaultSchemaIsUsedIfHeaderMissing(): void
7779
'fallback',
7880
'test-app',
7981
['test-app'],
82+
[]
8083
);
8184

8285
$request = new Request();
@@ -88,4 +91,120 @@ public function testDefaultSchemaIsUsedIfHeaderMissing(): void
8891
self::assertSame('fallback', $resolver->getSchema());
8992
self::assertNull($resolver->getBaggage());
9093
}
94+
95+
public function testAppNameIsAllowed(): void
96+
{
97+
$resolver = new BaggageSchemaResolver();
98+
$baggageCodec = new BaggageCodec();
99+
$listener = new BaggageRequestListener(
100+
$resolver,
101+
$baggageCodec,
102+
'X-Schema',
103+
'fallback',
104+
'test-app',
105+
['test-app'],
106+
[],
107+
);
108+
109+
$request = new Request();
110+
$kernel = $this->createMock(HttpKernelInterface::class);
111+
$event = new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST);
112+
113+
$listener->onKernelRequest($event);
114+
115+
$reflection = new \ReflectionClass(BaggageRequestListener::class);
116+
$reflectionMethod = $reflection->getMethod('isAllowedAppName');
117+
$reflectionMethod->setAccessible(true);
118+
119+
self::assertTrue($reflectionMethod->invoke($listener));
120+
self::assertSame('fallback', $resolver->getSchema());
121+
self::assertNull($resolver->getBaggage());
122+
}
123+
124+
public function testAppNameIsNotAllowed(): void
125+
{
126+
$resolver = new BaggageSchemaResolver();
127+
$baggageCodec = new BaggageCodec();
128+
$listener = new BaggageRequestListener(
129+
$resolver,
130+
$baggageCodec,
131+
'X-Schema',
132+
'fallback',
133+
'staging',
134+
['test-app'],
135+
[],
136+
);
137+
138+
$request = new Request();
139+
$kernel = $this->createMock(HttpKernelInterface::class);
140+
$event = new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST);
141+
142+
$listener->onKernelRequest($event);
143+
144+
$reflection = new \ReflectionClass(BaggageRequestListener::class);
145+
$reflectionMethod = $reflection->getMethod('isAllowedAppName');
146+
$reflectionMethod->setAccessible(true);
147+
148+
self::assertFalse($reflectionMethod->invoke($listener));
149+
self::assertNull($resolver->getSchema());
150+
self::assertNull($resolver->getBaggage());
151+
}
152+
153+
public function testAppNameIsNotAllowedByRegex(): void
154+
{
155+
$resolver = new BaggageSchemaResolver();
156+
$baggageCodec = new BaggageCodec();
157+
$listener = new BaggageRequestListener(
158+
$resolver,
159+
$baggageCodec,
160+
'X-Schema',
161+
'fallback',
162+
'staging',
163+
['test-app'],
164+
['/^prod$/'],
165+
);
166+
167+
$request = new Request();
168+
$kernel = $this->createMock(HttpKernelInterface::class);
169+
$event = new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST);
170+
171+
$listener->onKernelRequest($event);
172+
173+
$reflection = new \ReflectionClass(BaggageRequestListener::class);
174+
$reflectionMethod = $reflection->getMethod('isAllowedAppName');
175+
$reflectionMethod->setAccessible(true);
176+
177+
self::assertFalse($reflectionMethod->invoke($listener));
178+
self::assertNull($resolver->getSchema());
179+
self::assertNull($resolver->getBaggage());
180+
}
181+
182+
public function testAppNameIsAllowedByRegex(): void
183+
{
184+
$resolver = new BaggageSchemaResolver();
185+
$baggageCodec = new BaggageCodec();
186+
$listener = new BaggageRequestListener(
187+
$resolver,
188+
$baggageCodec,
189+
'X-Schema',
190+
'fallback',
191+
'pr-100',
192+
['test-app'],
193+
['/^pr-\d+$/'],
194+
);
195+
196+
$request = new Request();
197+
$kernel = $this->createMock(HttpKernelInterface::class);
198+
$event = new RequestEvent($kernel, $request, HttpKernelInterface::MAIN_REQUEST);
199+
200+
$listener->onKernelRequest($event);
201+
202+
$reflection = new \ReflectionClass(BaggageRequestListener::class);
203+
$reflectionMethod = $reflection->getMethod('isAllowedAppName');
204+
$reflectionMethod->setAccessible(true);
205+
206+
self::assertTrue($reflectionMethod->invoke($listener));
207+
self::assertEquals('fallback', $resolver->getSchema());
208+
self::assertNull($resolver->getBaggage());
209+
}
91210
}

0 commit comments

Comments
 (0)