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
11 changes: 6 additions & 5 deletions mock-server/app/http.php
Original file line number Diff line number Diff line change
Expand Up @@ -316,19 +316,20 @@
->inject('response')
->action(function (Request $request, UtopiaSwooleResponse $response) {

// Location methods render from their own request template, so they are the
// easiest place to lose the project header. The real API pairs an API key
// against this header and rejects the request without it.
if (empty($request->getHeader('x-appwrite-project', ''))) {
// Client SDKs build a URL for the browser, so credentials may arrive in
// the query string instead of headers. The real API accepts both.
$project = $request->getHeader('x-appwrite-project', '') ?: $request->getParam('project', '');
if (empty($project)) {
throw new Exception(Exception::GENERAL_MOCK, 'Missing project ID');
}
$impersonate = $request->getHeader('x-appwrite-impersonate-user-id', '') ?: $request->getParam('impersonateuserid', '');

$response
->setContentType('text/plain')
->addHeader('Content-Disposition', 'attachment; filename="test.txt"')
->addHeader('Expires', \date('D, d M Y H:i:s', \time() + (60 * 60 * 24 * 45)) . ' GMT') // 45 days cache
->addHeader('X-Peak', \memory_get_peak_usage())
->send("GET:/v1/mock/tests/general/download:passed");
->send('GET:/v1/mock/tests/general/download:passed' . ($impersonate === '' ? '' : ':as:' . $impersonate));
});

App::post('/v1/mock/tests/general/upload')
Expand Down
16 changes: 12 additions & 4 deletions src/SDK/SDK.php
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ public function __construct(protected Language $language, protected Specificatio
$this->twig->addFilter(new TwigFilter('methodHeaders', fn(Operation $operation): array => $this->getMethodHeaders($operation)));
$this->twig->addFilter(new TwigFilter('responseDiscriminator', fn(Operation $operation): array => $this->getResponseDiscriminator($operation)));
$this->twig->addFilter(new TwigFilter('securitySchemes', fn(Operation $operation): array => $this->getOperationAuthSchemes($operation)));
$this->twig->addFilter(new TwigFilter('locationSchemes', fn(Operation $operation): array => $this->getOperationAuthSchemes($operation, true)));
$this->twig->addFilter(new TwigFilter('securityHeaders', fn(Operation $operation): array => $this->getOperationSecuritySchemes($operation, ParameterLocation::HEADER, false)));
$this->twig->addFilter(new TwigFilter('securityQueries', fn(Operation $operation): array => $this->getOperationSecuritySchemes($operation, ParameterLocation::QUERY)));
$this->twig->addFilter(new TwigFilter('schemaNullable', fn(Schema|Parameter $value): bool => !isset($this->multipartSchemas[\spl_object_id($this->getSchema($value))]) && $this->getSchema($value)->nullable));
Expand Down Expand Up @@ -1850,7 +1851,14 @@ protected function getMethodHeaders(Operation $operation): array
*
* @return array<string, SecurityScheme>
*/
protected function getOperationAuthSchemes(Operation $operation): array
/**
* The schemes `x-appwrite.auth` configures for an operation. Examples skip
* the optional ones; a URL builder has no request to carry headers, so
* every scheme it lists becomes a query parameter and none may be dropped.
*
* @return array<string, SecurityScheme>
*/
protected function getOperationAuthSchemes(Operation $operation, bool $includeOptional = false): array
{
$auth = $operation->extensions[Extension::APPWRITE->value][Appwrite::AUTH->value] ?? [];
if (!\is_array($auth)) {
Expand All @@ -1859,10 +1867,10 @@ protected function getOperationAuthSchemes(Operation $operation): array
$auth = $auth[$this->getParam('platform')] ?? $auth;
$schemes = [];
$pathSchemes = [];
$optional = \array_diff($operation->acceptedSecuritySchemeNames(), $operation->requiredSecuritySchemeNames());
$optional = $includeOptional
? []
: \array_diff($operation->acceptedSecuritySchemeNames(), $operation->requiredSecuritySchemeNames());
foreach (\array_keys($auth) as $name) {
// Example configuration is independent of API authentication. Only
// omit a candidate when security explicitly makes it optional.
if (\in_array($name, $optional, true)) {
continue;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,8 @@ class {{ service.name | caseUcfirst }}(client: Client) : Service(client) {
"{{ parameter.name }}" to {{ parameter.name | caseCamel }},
{%~ endfor %}
{%~ if (method | methodType) == 'webAuth' %}
{%~ if [(method | securitySchemes)] | length > 0 %}
{%~ for node in [(method | securitySchemes)] %}
{%~ if [(method | locationSchemes)] | length > 0 %}
{%~ for node in [(method | locationSchemes)] %}
{%~ for key,header in node | keys %}
"{{ header | caseLower }}" to client.config["{{ header | caseLower }}"],
{%~ endfor %}
Expand Down
6 changes: 3 additions & 3 deletions templates/dart/base/requests/oauth.twig
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
{% import 'dart/base/utils.twig' as utils %}
{% set queryParams = method | parameters('query') %}
{% set bodyParams = method | parameters('body') %}
final Map<String, dynamic> params = {% if queryParams|length == 0 and bodyParams|length == 0 and ([(method | securitySchemes)]|length == 0) %}{}{% else %}{
{{ utils.map_parameter(queryParams) }}{{ utils.map_parameter(bodyParams) }}{% if [(method | securitySchemes)]|length > 0 %}
{% for node in [(method | securitySchemes)] %}
final Map<String, dynamic> params = {% if queryParams|length == 0 and bodyParams|length == 0 and ([(method | locationSchemes)]|length == 0) %}{}{% else %}{
{{ utils.map_parameter(queryParams) }}{{ utils.map_parameter(bodyParams) }}{% if [(method | locationSchemes)]|length > 0 %}
{% for node in [(method | locationSchemes)] %}
{% for key, header in node|keys %}
'{{header|caseLower}}': client.config['{{header|caseLower}}'],
{% endfor %}
Expand Down
2 changes: 1 addition & 1 deletion templates/dotnet/base/utils.twig
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
{% if parameter.name == 'orderType' %}{{ parameter.name | caseCamel ~ '.ToString()'}}{% elseif (parameter | enumValues) is not empty and (parameter | schemaType) == 'array' %}{{ parameter.name | caseCamel | escapeKeyword }}?.Select(e => e.Value).ToList(){% elseif (parameter | enumValues) is not empty %}{{ parameter.name | caseCamel | escapeKeyword }}?.Value{% else %}{{ parameter.name | caseCamel | escapeKeyword }}{% endif %}
{% endmacro %}
{% macro methodNeedsSecurityParameters(method) %}
{% if ((method | methodType) == "webAuth" or (method | methodType) == "location") and [(method | securitySchemes)]|length > 0 %}{{ true }}{% else %}{{false}}{% endif %}
{% if ((method | methodType) == "webAuth" or (method | methodType) == "location") and [(method | locationSchemes)]|length > 0 %}{{ true }}{% else %}{{false}}{% endif %}
{% endmacro %}
{% macro resultType(namespace, method) %}
{% if (method | methodType) == "webAuth" %}bool{% elseif (method | methodType) == "location" %}byte[]{% elseif (method | responseModels)|length > 1 %}object{% elseif not (method | responseModel) or (method | responseModel) == 'any' %}object{% else %}Models.{{(method | responseModel) | caseUcfirst | overrideIdentifier }}{% endif %}
Expand Down
6 changes: 3 additions & 3 deletions templates/flutter/base/requests/oauth.twig
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
{% import 'flutter/base/utils.twig' as utils %}
{% set queryParams = method | parameters('query') %}
{% set bodyParams = method | parameters('body') %}
final Map<String, dynamic> params = {% if queryParams|length == 0 and bodyParams|length == 0 and ([(method | securitySchemes)]|length == 0) %}{}{% else %}{
{{ utils.map_parameter(queryParams) }}{{ utils.map_parameter(bodyParams) }}{% if [(method | securitySchemes)]|length > 0 %}
{% for node in [(method | securitySchemes)] %}
final Map<String, dynamic> params = {% if queryParams|length == 0 and bodyParams|length == 0 and ([(method | locationSchemes)]|length == 0) %}{}{% else %}{
{{ utils.map_parameter(queryParams) }}{{ utils.map_parameter(bodyParams) }}{% if [(method | locationSchemes)]|length > 0 %}
{% for node in [(method | locationSchemes)] %}
{% for key, header in node|keys %}
'{{header|caseLower}}': client.config['{{header|caseLower}}'],
{% endfor %}
Expand Down
6 changes: 3 additions & 3 deletions templates/react-native/src/services/template.ts.twig
Original file line number Diff line number Diff line change
Expand Up @@ -165,8 +165,8 @@ export class {{ service.name | caseUcfirst }} extends Service {
uri.searchParams.append('{{ security.name }}', this.client.config.{{ security.name | caseLower }});
{%~ endfor %}
{% if (method | methodType) == 'location' or (method | methodType) == 'webAuth' %}
{% if [(method | securitySchemes)]|length > 0 %}
{% for node in [(method | securitySchemes)] %}
{% if [(method | locationSchemes)]|length > 0 %}
{% for node in [(method | locationSchemes)] %}
{% for key,header in node|keys %}
payload['{{header|caseLower}}'] = this.client.config.{{header|caseLower}};

Expand Down Expand Up @@ -484,7 +484,7 @@ export class {{ service.name | caseUcfirst }} extends Service {
{%~ for security in (method | securityQueries) %}
uri.searchParams.append('{{ security.name }}', this.client.config.{{ security.name | caseLower }});
{%~ endfor %}
{% for node in [(method | securitySchemes)] %}
{% for node in [(method | locationSchemes)] %}
{% for key,header in node|keys %}
payload['{{header|caseLower}}'] = this.client.config.{{header|caseLower}};

Expand Down
6 changes: 3 additions & 3 deletions templates/swift/base/params.twig
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,12 @@
{%- else -%} let
{%- endif %} apiParams: [String: Any?] = [
{%~ for parameter in (method | parameters('query')) | merge((method | parameters('body'))) %}
"{{ parameter.name }}": {{ parameter.name | caseCamel | escapeSwiftKeyword }}{% if (parameter | enumValues) is not empty %}{% if (parameter | schemaType) == 'array' %}{% if not parameter.required %}?{% endif %}.map { $0.rawValue }{% else %}{% if not parameter.required %}?{% endif %}.rawValue{% endif %}{% endif %}{% if not loop.last or ((method | parameters('query')) | merge((method | parameters('body')))) | length > 1 or ((method | methodType) == 'webAuth' and [(method | securitySchemes)] | length > 0) %},{% endif %}
"{{ parameter.name }}": {{ parameter.name | caseCamel | escapeSwiftKeyword }}{% if (parameter | enumValues) is not empty %}{% if (parameter | schemaType) == 'array' %}{% if not parameter.required %}?{% endif %}.map { $0.rawValue }{% else %}{% if not parameter.required %}?{% endif %}.rawValue{% endif %}{% endif %}{% if not loop.last or ((method | parameters('query')) | merge((method | parameters('body')))) | length > 1 or ((method | methodType) == 'webAuth' and [(method | locationSchemes)] | length > 0) %},{% endif %}

{%~ endfor %}
{%~ if (method | methodType) == 'webAuth' %}
{%~ if [(method | securitySchemes)] | length > 0 %}
{%~ for node in [(method | securitySchemes)] %}
{%~ if [(method | locationSchemes)] | length > 0 %}
{%~ for node in [(method | locationSchemes)] %}
{%~ for key,header in node | keys %}
"{{ header | caseLower }}": client.config["{{ header | caseLower }}"]{% if not loop.last or ((method | parameters('query')) | merge((method | parameters('body')))) | length > 0 or (node | keys) | length > 1 %},{% endif %}

Expand Down
4 changes: 2 additions & 2 deletions templates/web/src/services/template.ts.twig
Original file line number Diff line number Diff line change
Expand Up @@ -174,8 +174,8 @@ export class {{ service.name | caseUcfirst }} {

{%~ endif %}
{%~ if (method | methodType) == 'location' or (method | methodType) == 'webAuth' %}
{%~ if [(method | securitySchemes)]|length > 0 %}
{%~ for node in [(method | securitySchemes)] %}
{%~ if [(method | locationSchemes)]|length > 0 %}
{%~ for node in [(method | locationSchemes)] %}
{%~ for key,header in node|keys %}
payload['{{header|caseLower}}'] = this.client.config.{{header|caseLower}};
{%~ endfor %}
Expand Down
5 changes: 5 additions & 0 deletions tests/e2e/Base.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ abstract class Base extends TestCase
'GET:/v1/mock/tests/general/redirect/done:passed',
];

protected const LOCATION_RESPONSES = [
'GET:/v1/mock/tests/general/download:passed',
'GET:/v1/mock/tests/general/download:passed:as:impersonated',
];

protected const PATH_PARAM_RESPONSES = [
'GET:/v1/mock/tests/general/path/grant%2Fspecial%26id:passed',
];
Expand Down
1 change: 1 addition & 0 deletions tests/e2e/WebChromiumTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ final class WebChromiumTest extends Base
...Base::BAR_RESPONSES,
...Base::BAR_RESPONSES, // Object params
...Base::GENERAL_RESPONSES,
...Base::LOCATION_RESPONSES,
...Base::PATH_VALIDATION_RESPONSES,
...Base::PATH_PARAM_RESPONSES,
...Base::UPLOAD_RESPONSE,
Expand Down
1 change: 1 addition & 0 deletions tests/e2e/WebNodeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ final class WebNodeTest extends Base
...Base::BAR_RESPONSES,
...Base::BAR_RESPONSES, // Object params
...Base::GENERAL_RESPONSES,
...Base::LOCATION_RESPONSES,
...Base::PATH_VALIDATION_RESPONSES,
...Base::PATH_PARAM_RESPONSES,
...Base::ENUM_RESPONSES,
Expand Down
8 changes: 8 additions & 0 deletions tests/e2e/languages/web/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,14 @@
response = await general.redirect();
console.log(response.result);

// Location URL builder carries the configured credentials in the query string
response = await fetch(general.download());
console.log(await response.text());
client.setImpersonateUserId('impersonated');
response = await fetch(general.download());
console.log(await response.text());
client.setImpersonateUserId('');

for (const [id, plain] of [['', '0'], ['0', '']]) {
try {
await general.validatePath({ id, plain });
Expand Down
8 changes: 8 additions & 0 deletions tests/e2e/languages/web/node.js
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,14 @@ async function start() {
response = await general.redirect();
console.log(response.result);

// Location URL builder carries the configured credentials in the query string
response = await fetch(general.download());
console.log(await response.text());
client.setImpersonateUserId('impersonated');
response = await fetch(general.download());
console.log(await response.text());
client.setImpersonateUserId('');

for (const [id, plain] of [['', '0'], ['0', '']]) {
try {
await general.validatePath({ id, plain });
Expand Down
4 changes: 3 additions & 1 deletion tests/generation/GenerationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,8 @@ public function testFixtureSelection(string $name): void
* A canonical document keys `x-appwrite.auth` by platform. The fixture's
* platform-auth operation lists `Project` for client and `Project, Key` for
* server plus an optional Session, and the server alias variant lists
* `Project, JWT`. Optional security must not become example configuration.
* `Project, JWT`. Optional security must not become example configuration;
* the Web E2E proves it still reaches a location method's query string.
*/
public function testExampleCredentialsFollowPlatformAuth(): void
{
Expand All @@ -306,6 +307,7 @@ public function testExampleCredentialsFollowPlatformAuth(): void
['client', 'docs/examples/general/zzderivedauth.md', ['->setproject('], ['->setkey(', '->setjwt(', '->setsession(']],
['server', 'docs/examples/general/zzplatformalias.md', ['->setproject(', '->setjwt('], ['->setkey(', '->setsession(']],
['client', 'docs/examples/general/zzplatformalias.md', ['->setproject('], ['->setkey(', '->setjwt(', '->setsession(']],
['client', 'docs/examples/general/download.md', ['->setproject('], ['->setimpersonateuserid(']],
Comment thread
ChiragAgg5k marked this conversation as resolved.
];

foreach ($examples as [$platform, $path, $present, $absent]) {
Expand Down
6 changes: 6 additions & 0 deletions tests/resources/spec-openapi3.json
Original file line number Diff line number Diff line change
Expand Up @@ -1288,6 +1288,12 @@
"Project": [],
"Key": [],
"JWT": []
},
{
"Project": [],
"Key": [],
"JWT": [],
"ImpersonateUserId": []
}
],
"responses": {
Expand Down
Loading