Skip to content

Commit fc257d4

Browse files
ayush3797aaronpowellseantleonardAniruddh25
authored
Add support for adding base-route for Runtime (#1506)
## Why make this change? SWA or any other front end can interact with DAB using a path-based routing mechanism, where they may use a path (`base-route`) to redirect requests to DAB (refer to the bug encountered: #1420). This `base-route` gets stripped off when the request lands at DAB. Consequently, DAB is not even aware of anything like a `base-route` being used by the front end. This poses a problem in generation of the `nextLink`, where the `nextLink` which DAB generates, does not contain the `base-route` and consequently when the front end receives the next request, it does not find the base route in the request URL and hence, has no idea where to redirect the request to. Using the newly added option to configure base route for rest requests via the config (CLI option - `--runtime.base-route`), the front-end like SWA can make DAB aware that they are using a base route to redirect requests to DAB. DAB can then accordingly account for the base route while generating the `nextLink` for paginated responses. ## What is this change? 1. Added feature to specify `base-route` in global runtime settings in the config which can be via CLI as well using `--runtime.base-route `option in the `dab init` command. By default, it will be initialized as null. When the base-route is a non-empty string, it will be included in the generated nextLink. This feature is only supported when `authentication provider == StaticWebApps`. We throw an exception when base-route is configured for non-SWA providers. 2. Extended functionality of `Utils.IsApiPathValid(string? apiPath, ApiType apiType)` method to `Utils.IsUriComponentValid(string? uriComponent)` because similar validations are required for api properties: `rest.path`/ `runtime.base-route`/ `graphql.path`, all of which are part of the request URI. 3. Renamed `RuntimeConfigValidator.ValidateRestPathForRelationalDbs()` to `RuntimeConfigValidator.ValidateRestURI()`. 4. Renamed `RuntimeConfigValidator.ValidateGraphQLPath()` to `RuntimeConfigValidator.ValidateGraphQLURI()` so that we follow same convention between rest and gql. ## Q&A 1. Will this require updating an already created configuration file? No, the existing config file will work perfectly fine. The base-route is totally an optional feature. 2. Will a default value be implicitly used if the new base path config property is not set? The default value for the base-route is null. This essentially means that base-route will not come into the picture. 3. What is the default value and does the value differ between dev/prod swa/non-swa? The default value will be same irrespective of dev/prod/swa/non-swa environment and equal to null. So to say, the **change is not a breaking change and we are totally backwards compatible**. ## How was this tested? - [x] Integration Tests - Was added to `ConfigurationTests.cs` to assert that the generated nextLink contains the base-route. - [x] Unit tests - were added to `ConfigValidationUnitTests.cs` to confirm that we throw proper exceptions when runtime base-route is formatted incorrectly (contains reserved characters or doesn't start with a '/') or when base-route is configured for non-SWA authentication providers. --------- Co-authored-by: Aaron Powell <me@aaron-powell.com> Co-authored-by: Sean Leonard <sean.leonard@microsoft.com> Co-authored-by: Aniruddh Munde <anmunde@microsoft.com>
1 parent bda0d65 commit fc257d4

File tree

15 files changed

+362
-262
lines changed

15 files changed

+362
-262
lines changed

src/Cli.Tests/EndToEndTests.cs

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,8 +104,7 @@ public void TestInitForCosmosDBPostgreSql()
104104
[TestMethod]
105105
public void TestInitializingRestAndGraphQLGlobalSettings()
106106
{
107-
string[] args = { "init", "-c", TEST_RUNTIME_CONFIG_FILE, "--database-type", "mssql", "--rest.path", "/rest-api",
108-
"--rest.disabled", "--graphql.path", "/graphql-api" };
107+
string[] args = { "init", "-c", TEST_RUNTIME_CONFIG_FILE, "--database-type", "mssql", "--rest.path", "/rest-api", "--rest.disabled", "--graphql.path", "/graphql-api" };
109108
Program.Execute(args, _cliLogger!, _fileSystem!, _runtimeConfigLoader!);
110109

111110
Assert.IsTrue(_runtimeConfigLoader!.TryLoadConfig(TEST_RUNTIME_CONFIG_FILE, out RuntimeConfig? runtimeConfig));
@@ -720,4 +719,40 @@ public async Task TestExitOfRuntimeEngineWithInvalidConfig(
720719

721720
process.Kill();
722721
}
722+
723+
/// <summary>
724+
/// Test to verify that when base-route is configured, the runtime config is only successfully generated when the
725+
/// authentication provider is Static Web Apps.
726+
/// </summary>
727+
/// <param name="authProvider">Authentication provider specified for the runtime.</param>
728+
/// <param name="isExceptionExpected">Whether an exception is expected as a result of test run.</param>
729+
[DataTestMethod]
730+
[DataRow("StaticWebApps", false)]
731+
[DataRow("AppService", true)]
732+
[DataRow("AzureAD", true)]
733+
public void TestBaseRouteIsConfigurableForSWA(string authProvider, bool isExceptionExpected)
734+
{
735+
string[] initArgs = { "init", "-c", TEST_RUNTIME_CONFIG_FILE, "--host-mode", "development", "--database-type", "mssql",
736+
"--connection-string", "localhost:5000", "--auth.provider", authProvider, "--runtime.base-route", "base-route" };
737+
738+
if (!Enum.TryParse(authProvider, ignoreCase: true, out EasyAuthType _))
739+
{
740+
string[] audIssuers = { "--auth.audience", "aud-xxx", "--auth.issuer", "issuer-xxx" };
741+
initArgs = initArgs.Concat(audIssuers).ToArray();
742+
}
743+
744+
Program.Execute(initArgs, _cliLogger!, _fileSystem!, _runtimeConfigLoader!);
745+
746+
if (isExceptionExpected)
747+
{
748+
Assert.IsFalse(_runtimeConfigLoader!.TryLoadConfig(TEST_RUNTIME_CONFIG_FILE, out RuntimeConfig? runtimeConfig));
749+
Assert.IsNull(runtimeConfig);
750+
}
751+
else
752+
{
753+
Assert.IsTrue(_runtimeConfigLoader!.TryLoadConfig(TEST_RUNTIME_CONFIG_FILE, out RuntimeConfig? runtimeConfig));
754+
Assert.IsNotNull(runtimeConfig);
755+
Assert.AreEqual("/base-route", runtimeConfig.Runtime.BaseRoute);
756+
}
757+
}
723758
}

src/Cli.Tests/TestHelper.cs

Lines changed: 0 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -849,52 +849,6 @@ public static Process ExecuteDabCommand(string command, string flags)
849849
}
850850
}";
851851

852-
public const string CONFIG_WITH_SINGLE_ENTITY =
853-
@"{" +
854-
@"""$schema"": """ + DAB_DRAFT_SCHEMA_TEST_PATH + @"""" + "," +
855-
@"""data-source"": {
856-
""database-type"": ""mssql"",
857-
""connection-string"": ""localhost:5000"",
858-
""options"":{
859-
""set-session-context"": true
860-
}
861-
},
862-
""runtime"": {
863-
""rest"": {
864-
""path"": ""/api"",
865-
""enabled"": true
866-
},
867-
""graphql"": {
868-
""path"": ""/graphql"",
869-
""enabled"": true,
870-
""allow-introspection"": true
871-
},
872-
""host"": {
873-
""mode"": ""production"",
874-
""cors"": {
875-
""origins"": [],
876-
""allow-credentials"": false
877-
},
878-
""authentication"": {
879-
""provider"": ""StaticWebApps""
880-
}
881-
}
882-
},
883-
""entities"": {
884-
""book"": {
885-
""source"": ""s001.book"",
886-
""permissions"": [
887-
{
888-
""role"": ""anonymous"",
889-
""actions"": [
890-
""*""
891-
]
892-
}
893-
]
894-
}
895-
}
896-
}";
897-
898852
public const string BASE_CONFIG =
899853
@"{" +
900854
@"""$schema"": """ + DAB_DRAFT_SCHEMA_TEST_PATH + @"""" + "," +

src/Cli.Tests/UtilsTests.cs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -174,14 +174,14 @@ public void TestStoredProcedurePermissions(
174174
/// Test to verify that CLI is able to figure out if the api path prefix for rest/graphql contains invalid characters.
175175
/// </summary>
176176
[DataTestMethod]
177-
[DataRow("/", ApiType.REST, true, DisplayName = "Only forward slash as api path")]
178-
[DataRow("/$%^", ApiType.REST, false, DisplayName = "Api path containing only reserved characters.")]
179-
[DataRow("/rest-api", ApiType.REST, true, DisplayName = "Valid api path")]
180-
[DataRow("/graphql@api", ApiType.GraphQL, false, DisplayName = "Api path containing some reserved characters.")]
181-
[DataRow("/api path", ApiType.REST, true, DisplayName = "Api path containing space.")]
182-
public void TestApiPathIsWellFormed(string apiPath, ApiType apiType, bool expectSuccess)
177+
[DataRow("/", true, DisplayName = "Only forward slash as api path")]
178+
[DataRow("/$%^", false, DisplayName = "Api path containing only reserved characters.")]
179+
[DataRow("/rest-api", true, DisplayName = "Valid api path")]
180+
[DataRow("/graphql@api", false, DisplayName = "Api path containing some reserved characters.")]
181+
[DataRow("/api path", true, DisplayName = "Api path containing space.")]
182+
public void TestApiPathIsWellFormed(string apiPath, bool expectSuccess)
183183
{
184-
Assert.AreEqual(expectSuccess, IsApiPathValid(apiPath, apiType));
184+
Assert.AreEqual(expectSuccess, IsURIComponentValid(apiPath));
185185
}
186186

187187
/// <summary>

src/Cli/Commands/InitOptions.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ public InitOptions(
3030
string? audience = null,
3131
string? issuer = null,
3232
string restPath = RestRuntimeOptions.DEFAULT_PATH,
33+
string? runtimeBaseRoute = null,
3334
bool restDisabled = false,
3435
string graphQLPath = GraphQLRuntimeOptions.DEFAULT_PATH,
3536
bool graphqlDisabled = false,
@@ -48,6 +49,7 @@ public InitOptions(
4849
Audience = audience;
4950
Issuer = issuer;
5051
RestPath = restPath;
52+
RuntimeBaseRoute = runtimeBaseRoute;
5153
RestDisabled = restDisabled;
5254
GraphQLPath = graphQLPath;
5355
GraphQLDisabled = graphqlDisabled;
@@ -89,6 +91,9 @@ public InitOptions(
8991
[Option("rest.path", Default = RestRuntimeOptions.DEFAULT_PATH, Required = false, HelpText = "Specify the REST endpoint's default prefix.")]
9092
public string RestPath { get; }
9193

94+
[Option("runtime.base-route", Default = null, Required = false, HelpText = "Specifies the base route for API requests.")]
95+
public string? RuntimeBaseRoute { get; }
96+
9297
[Option("rest.disabled", Default = false, Required = false, HelpText = "Disables REST endpoint for all entities.")]
9398
public bool RestDisabled { get; }
9499

src/Cli/ConfigGenerator.cs

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
using Azure.DataApiBuilder.Config.Converters;
1010
using Azure.DataApiBuilder.Config.NamingPolicies;
1111
using Azure.DataApiBuilder.Config.ObjectModel;
12+
using Azure.DataApiBuilder.Core.Configurations;
1213
using Azure.DataApiBuilder.Service;
1314
using Cli.Commands;
1415
using Microsoft.Extensions.Logging;
@@ -87,6 +88,7 @@ public static bool TryCreateRuntimeConfig(InitOptions options, RuntimeConfigLoad
8788
DatabaseType dbType = options.DatabaseType;
8889
string? restPath = options.RestPath;
8990
string graphQLPath = options.GraphQLPath;
91+
string? runtimeBaseRoute = options.RuntimeBaseRoute;
9092
Dictionary<string, JsonElement> dbOptions = new();
9193

9294
HyphenatedNamingPolicy namingPolicy = new();
@@ -147,11 +149,33 @@ public static bool TryCreateRuntimeConfig(InitOptions options, RuntimeConfigLoad
147149
return false;
148150
}
149151

150-
if (!IsApiPathValid(restPath, ApiType.REST) || !IsApiPathValid(options.GraphQLPath, ApiType.GraphQL))
152+
if (!IsURIComponentValid(restPath))
151153
{
154+
_logger.LogError($"{ApiType.REST} path {RuntimeConfigValidator.URI_COMPONENT_WITH_RESERVED_CHARS_ERR_MSG}");
152155
return false;
153156
}
154157

158+
if (!IsURIComponentValid(options.GraphQLPath))
159+
{
160+
_logger.LogError($"{ApiType.GraphQL} path {RuntimeConfigValidator.URI_COMPONENT_WITH_RESERVED_CHARS_ERR_MSG}");
161+
return false;
162+
}
163+
164+
if (!IsURIComponentValid(runtimeBaseRoute))
165+
{
166+
_logger.LogError($"Runtime base-route {RuntimeConfigValidator.URI_COMPONENT_WITH_RESERVED_CHARS_ERR_MSG}");
167+
return false;
168+
}
169+
170+
if (runtimeBaseRoute is not null)
171+
{
172+
if (!Enum.TryParse(options.AuthenticationProvider, ignoreCase: true, out EasyAuthType easyAuthMode) || easyAuthMode is not EasyAuthType.StaticWebApps)
173+
{
174+
_logger.LogError($"Runtime base-route can only be specified when the authentication provider is Static Web Apps.");
175+
return false;
176+
}
177+
}
178+
155179
if (options.RestDisabled && options.GraphQLDisabled)
156180
{
157181
_logger.LogError($"Both Rest and GraphQL cannot be disabled together.");
@@ -166,6 +190,12 @@ public static bool TryCreateRuntimeConfig(InitOptions options, RuntimeConfigLoad
166190
restPath = "/" + restPath;
167191
}
168192

193+
// Prefix base-route with '/', if not already present.
194+
if (runtimeBaseRoute is not null && !runtimeBaseRoute.StartsWith('/'))
195+
{
196+
runtimeBaseRoute = "/" + runtimeBaseRoute;
197+
}
198+
169199
// Prefix GraphQL path with '/', if not already present.
170200
if (!graphQLPath.StartsWith('/'))
171201
{
@@ -183,7 +213,8 @@ public static bool TryCreateRuntimeConfig(InitOptions options, RuntimeConfigLoad
183213
Authentication: new(
184214
Provider: options.AuthenticationProvider,
185215
Jwt: (options.Audience is null && options.Issuer is null) ? null : new(options.Audience, options.Issuer)),
186-
Mode: options.HostMode)
216+
Mode: options.HostMode),
217+
BaseRoute: runtimeBaseRoute
187218
),
188219
Entities: new RuntimeEntities(new Dictionary<string, Entity>()));
189220

src/Cli/Utils.cs

Lines changed: 10 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
using Azure.DataApiBuilder.Config;
1010
using Azure.DataApiBuilder.Config.Converters;
1111
using Azure.DataApiBuilder.Config.ObjectModel;
12-
using Azure.DataApiBuilder.Service.Exceptions;
1312
using Cli.Commands;
1413
using Microsoft.Extensions.Logging;
1514
using static Azure.DataApiBuilder.Core.Configurations.RuntimeConfigValidator;
@@ -184,35 +183,25 @@ public static bool TryParseMappingDictionary(IEnumerable<string> mappingList, ou
184183
}
185184

186185
/// <summary>
187-
/// Returns true if the api path contains any reserved characters like "[\.:\?#/\[\]@!$&'()\*\+,;=]+"
186+
/// Checks whether the URI component conforms with the expected behavior and does not contain any reserved characters.
188187
/// </summary>
189-
/// <param name="apiPath">path prefix for rest/graphql apis</param>
190-
/// <param name="apiType">Either REST or GraphQL</param>
191-
public static bool IsApiPathValid(string? apiPath, ApiType apiType)
188+
/// <param name="uriComponent">Path prefix/base route for REST/GraphQL APIs.</param>
189+
public static bool IsURIComponentValid(string? uriComponent)
192190
{
193-
// apiPath is null only in case of cosmosDB and apiType=REST. For this case, validation is not required.
194-
// Since, cosmosDB do not support REST calls.
195-
if (apiPath is null)
191+
// uriComponent is null only in case of cosmosDB and apiType=REST or when the runtime base-route is specified as null.
192+
// For these cases, validation is not required.
193+
if (uriComponent is null)
196194
{
197195
return true;
198196
}
199197

200-
// removing leading '/' before checking for forbidden characters.
201-
if (apiPath.StartsWith('/'))
198+
// removing leading '/' before checking for reserved characters.
199+
if (uriComponent.StartsWith('/'))
202200
{
203-
apiPath = apiPath.Substring(1);
201+
uriComponent = uriComponent.Substring(1);
204202
}
205203

206-
try
207-
{
208-
DoApiPathInvalidCharCheck(apiPath, apiType);
209-
return true;
210-
}
211-
catch (DataApiBuilderException ex)
212-
{
213-
_logger.LogError(ex.Message);
214-
return false;
215-
}
204+
return !DoesUriComponentContainReservedChars(uriComponent);
216205
}
217206

218207
/// <summary>

src/Config/ObjectModel/ApiType.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,7 @@ namespace Azure.DataApiBuilder.Config.ObjectModel;
99
public enum ApiType
1010
{
1111
REST,
12-
GraphQL
12+
GraphQL,
13+
// This is required to indicate features common between all APIs.
14+
All
1315
}

src/Config/ObjectModel/AuthenticationOptions.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,10 @@ public record AuthenticationOptions(string Provider, JwtOptions? Jwt)
3636
/// </summary>
3737
/// <returns>True if the provider is enabled for JWT, otherwise false.</returns>
3838
public bool IsJwtConfiguredIdentityProvider() => !IsEasyAuthAuthenticationProvider() && !IsAuthenticationSimulatorEnabled();
39+
40+
/// <summary>
41+
/// A shorthand method to determine whether Static Web Apps is configured for the current authentication provider.
42+
/// </summary>
43+
/// <returns>True if the provider is enabled for Static Web Apps, otherwise false.</returns>
44+
public bool IsStaticWebAppsIdentityProvider() => EasyAuthType.StaticWebApps.ToString().Equals(Provider, StringComparison.OrdinalIgnoreCase);
3945
};

src/Config/ObjectModel/RuntimeOptions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@
33

44
namespace Azure.DataApiBuilder.Config.ObjectModel;
55

6-
public record RuntimeOptions(RestRuntimeOptions Rest, GraphQLRuntimeOptions GraphQL, HostOptions Host);
6+
public record RuntimeOptions(RestRuntimeOptions Rest, GraphQLRuntimeOptions GraphQL, HostOptions Host, string? BaseRoute = null);

0 commit comments

Comments
 (0)