diff --git a/README.md b/README.md index 1746a62482..10e187cf25 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,42 @@ dab init DAB supports tables, views, and stored procedures. It works with SQL Server, Azure Cosmos DB, PostgreSQL, MySQL, and SQL Data Warehouse. Security is engine-level, but permissions are per entity. +The file `dab-config.json` is automatically created through this process. These are the resulting contents: + +```json +{ + "$schema": "https://github.com/Azure/data-api-builder/releases/download/v1.5.56/dab.draft.schema.json", + "data-source": { + "database-type": "mssql", + "connection-string": "@env('my-connection-string')", + "options": { + "set-session-context": false + } + }, + "runtime": { + "rest": { + "enabled": true, + "path": "/api", + "request-body-strict": true + }, + "graphql": { + "enabled": true, + "path": "/graphql", + "allow-introspection": true + }, + "host": { + "cors": { + "origins": [], + "allow-credentials": false + }, + "authentication": { + "provider": "AppService" + }, + "mode": "development" + } + }, + "entities": { } +} ``` dab add Actor --source "dbo.Actor" diff --git a/docs/media/dab-aifoundry-architecture.png b/docs/media/dab-aifoundry-architecture.png new file mode 100644 index 0000000000..6823ef6418 Binary files /dev/null and b/docs/media/dab-aifoundry-architecture.png differ diff --git a/docs/testing-guide/ai-foundry-integration.md b/docs/testing-guide/ai-foundry-integration.md new file mode 100644 index 0000000000..0165b725f6 --- /dev/null +++ b/docs/testing-guide/ai-foundry-integration.md @@ -0,0 +1,275 @@ +# Deploying SQL MCP Server implemented in Data API builder and Integrating with Azure AI Foundry + +This document provides an end‑to‑end guide to stand up a **SQL MCP Server** with **Model Context Protocol (MCP)** tools implemented in **Data API builder (DAB)** container that also exposes **REST** and **GraphQL** endpoints, and to integrate those MCP tools with an **Azure AI Foundry Agent**. + +Architecture diagram showing SQL MCP Server integration with Azure AI Foundry + +## 1. Architecture Overview + +**Components** +- **Azure SQL Database** hosting domain tables and stored procedures. +- **DAB container** (Azure Container Instances in this guide) that: + - reads `dab-config.json` from an **Azure Files** share at startup, + - exposes **REST**, **GraphQL**, and **MCP** endpoints. +- **Azure Storage (Files)** to store and version `dab-config.json`. +- **Azure AI Foundry Agent** configured with an **MCP tool** pointing to the SQL MCP Server endpoint. + +**Flow** +1. DAB starts in ACI → reads `dab-config.json` from the mounted Azure Files share. +2. DAB exposes `/api` (REST), `/graphql` (GraphQL), and `/mcp` (MCP). +3. Azure AI Foundry Agent invokes MCP tools to read/update data via DAB’s surface (tables, views and stored procedures). + + +## 2. Prerequisites +- Azure Subscription with permissions for Resource Groups, Storage, ACI, and Azure SQL. +- Azure SQL Database provisioned and reachable from ACI. +- Azure CLI (`az`) and .NET SDK installed locally. +- DAB CLI version **1.7.81 or later**. +- Outbound network access from ACI to your Azure SQL server. + + +## 3. Prepare the Database + +You need to create the necessary tables and stored procedures in your Azure SQL Database. Below is an example of how to create a simple `Products` table and a stored procedure to retrieve products by category. + +**Example:** + +1. Connect to your Azure SQL Database using Azure Data Studio, SQL Server Management Studio, or the Azure Portal's Query Editor. + +2. Run the following SQL script to create a sample table and stored procedure: + +```sql +-- Create Products table +CREATE TABLE Products ( + ProductID INT IDENTITY(1,1) PRIMARY KEY, + Name NVARCHAR(100) NOT NULL, + Category NVARCHAR(50) NOT NULL, + Price DECIMAL(10,2) NOT NULL +); + +-- Create stored procedure to get products by category +CREATE PROCEDURE GetProductsByCategory + @Category NVARCHAR(50) +AS +BEGIN + SET NOCOUNT ON; + SELECT ProductID, Name, Category, Price + FROM Products + WHERE Category = @Category; +END; +``` + +## 4. Install DAB CLI and Bootstrap Configuration + +``` +dotnet tool install --global Microsoft.DataApiBuilder --version 1.7.81 +export DATABASE_CONNECTION_STRING="Server=.database.windows.net;Database=;User ID=;Password=;Encrypt=True;" + +dab init \ + --database-type "mssql" \ + --connection-string "@env('DATABASE_CONNECTION_STRING')" \ + --host-mode "Development" \ + --rest.enabled true \ + --graphql.enabled true \ + --mcp.enabled true \ + --mcp.path "/mcp" + +``` + +## 5. Add all required entities (tables and stored procedures) to `dab-config.json` and enable MCP tools in the config + +Here is how to add a table entity and a stored procedure to your `dab-config.json`, and ensure MCP tools are enabled: + +1. **Open your `dab-config.json` file.** + +2. **Add an entity (table) definition** under the `"entities"` section. For example, to expose a `Customers` table: + ``` + "entities": { + "Customers": { + "source": "Customers", + "rest": true, + "graphql": true, + "mcp": true, + "permissions": [ + { + "role": "anonymous", + "actions": [ "read", "create", "update", "delete" ] + } + ] + } + } + ``` + +3. **Add a stored procedure** under the "entities" section. For example, to expose a stored procedure called GetCustomerOrders: + + ``` + "GetCustomerOrders": { + "source": { + "object": "GetCustomerOrders", + "type": "stored-procedure" + }, + "rest": true, + "graphql": true, + "mcp": true, + "permissions": [ + { + "role": "anonymous", + "actions": [ "execute" ] + } + ] + } + ``` + +Note: Make sure the "entities" section is a valid JSON object. If you have multiple entities, separate them with commas. + +4. **Ensure MCP is enabled in the "runtime" section:** + +``` +"runtime": { + "rest": { "enabled": true }, + "graphql": { "enabled": true }, + "mcp": { + "enabled": true, + "path": "/mcp" + } +} +``` + +5. **Example dab-config.json structure:** + +``` +{ + "data-source": { + "database-type": "mssql", + "connection-string": "@env('DATABASE_CONNECTION_STRING')" + }, + "runtime": { + "rest": { "enabled": true }, + "graphql": { "enabled": true }, + "mcp": { + "enabled": true, + "path": "/mcp" + } + }, + "entities": { + "Customers": { + "source": "Customers", + "rest": true, + "graphql": true, + "mcp": true, + "permissions": [ + { + "role": "anonymous", + "actions": [ "read", "create", "update", "delete" ] + } + ] + }, + "GetCustomerOrders": { + "source": { + "object": "GetCustomerOrders", + "type": "stored-procedure" + }, + "rest": true, + "graphql": true, + "mcp": true, + "permissions": [ + { + "role": "anonymous", + "actions": [ "execute" ] + } + ] + } + } +} +``` + +6. **Save the file.** + +## 6. Store dab-config.json in Azure Files + +1. **Create a Storage Account** (if you don't have one): +az storage account create +--name +--resource-group +--location +--sku Standard_LRS + + +2. **Create a File Share**: +az storage share create +--name +--account-name + + +3. **Upload `dab-config.json` to the File Share**: +az storage file upload +--account-name +--share-name +--source ./dab-config.json +--path dab-config.json + + +4. **Retrieve the Storage Account key** (needed for mounting in ACI): +az storage account keys list +--account-name +--resource-group + +Use the value of `key1` or `key2` as `` in the next step. + + +## 7. Deploy DAB to Azure Container Instances + +``` +az container create \ + --resource-group \ + --name dab-mcp-demo \ + --image mcr.microsoft.com/azure-databases/data-api-builder:1.7.81-rc \ + --dns-name-label \ + --ports 5000 \ + --location \ + --environment-variables DAB_CONFIG_PATH="/aci/dab-config.json" \ + --azure-file-volume-share-name \ + --azure-file-volume-account-name \ + --azure-file-volume-account-key \ + --azure-file-volume-mount-path "/aci" \ + --os-type Linux \ + --cpu 1 \ + --memory 1.5 \ + --command-line "dotnet Azure.DataApiBuilder.Service.dll --ConfigFileName $DAB_CONFIG_PATH --LogLevel Debug" +``` + +## 8. Integrate with Azure AI Foundry + +Follow these steps to connect your SQL MCP endpoint deployed in DAB to Azure AI Foundry and test the integration: + +1. **Create or Open a Project** + - Navigate to the [Azure AI Foundry portal](https://ai.azure.com/foundry) and sign in. + - On the dashboard, click **Projects** in the left navigation pane. + - To create a new project, click **New Project**, enter a name (e.g., `DAB-MCP-Demo`), and click **Create**. + - To use an existing project, select it from the list. + +2. **Add an Agent** + - Within your project, go to the **Agents** tab. + - Click **Add Agent**. + - Enter an agent name (e.g., `DAB-MCP-Agent`). + - (Optional) Add a description. + - Click **Create**. + +3. **Configure the MCP Tool** + - In the agent's configuration page, go to the **Tools** section. + - Click **Add Tool** and select **MCP** from the tool type dropdown. + - In the **MCP Endpoint URL** field, enter your SQL MCP endpoint in DAB, e.g., `http:///mcp`. + - (Optional) Configure authentication if your endpoint requires it. + - Click **Save** to add the tool. + +4. **Test in Playground** + - Go to the **Playground** tab in your project. + - Select the agent you created from the agent dropdown. + - In the input box, enter a prompt that will trigger the MCP tool, such as: + ``` + Get all records from the Customers entity. + ``` + - Click **Run**. + - The agent should invoke the MCP tool, which will call your DAB MCP endpoint and return the results. + - **Expected Result:** You should see the data returned from your DAB instance displayed in the Playground output panel. + - If there are errors, check the DAB container logs and ensure the MCP endpoint is reachable from Azure AI Foundry. diff --git a/docs/Testing/mcp-inspector-testing.md b/docs/testing-guide/mcp-inspector-testing.md similarity index 100% rename from docs/Testing/mcp-inspector-testing.md rename to docs/testing-guide/mcp-inspector-testing.md diff --git a/schemas/dab.draft.schema.json b/schemas/dab.draft.schema.json index 80cfd953ad..dd0bebd3cb 100644 --- a/schemas/dab.draft.schema.json +++ b/schemas/dab.draft.schema.json @@ -370,7 +370,7 @@ "description": "Custom authentication provider defined by the user. Use the JWT property to configure the custom provider." } ], - "default": "StaticWebApps" + "default": "AppService" }, "jwt": { "type": "object", diff --git a/src/Azure.DataApiBuilder.Mcp/BuiltInTools/DescribeEntitiesTool.cs b/src/Azure.DataApiBuilder.Mcp/BuiltInTools/DescribeEntitiesTool.cs index cd2a7cc28b..c5b283214f 100644 --- a/src/Azure.DataApiBuilder.Mcp/BuiltInTools/DescribeEntitiesTool.cs +++ b/src/Azure.DataApiBuilder.Mcp/BuiltInTools/DescribeEntitiesTool.cs @@ -111,6 +111,30 @@ public Task ExecuteAsync( } } + // Get current user's role for permission filtering + // For discovery tools like describe_entities, we use the first valid role from the header + // This differs from operation-specific tools that check permissions per entity per operation + if (httpContext != null && authResolver.IsValidRoleContext(httpContext)) + { + string roleHeader = httpContext.Request.Headers[AuthorizationResolver.CLIENT_ROLE_HEADER].ToString(); + if (!string.IsNullOrWhiteSpace(roleHeader)) + { + string[] roles = roleHeader + .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + + if (roles.Length > 1) + { + logger?.LogWarning("Multiple roles detected in request header: [{Roles}]. Using first role '{FirstRole}' for entity discovery. " + + "Consider using a single role for consistent permission reporting.", + string.Join(", ", roles), roles[0]); + } + + // For discovery operations, take the first role from comma-separated list + // This provides a consistent view of available entities for the primary role + currentUserRole = roles.FirstOrDefault(); + } + } + (bool nameOnly, HashSet? entityFilter) = ParseArguments(arguments, logger); if (currentUserRole == null) diff --git a/src/Azure.DataApiBuilder.Mcp/Core/McpProtocolDefaults.cs b/src/Azure.DataApiBuilder.Mcp/Core/McpProtocolDefaults.cs new file mode 100644 index 0000000000..48b235f480 --- /dev/null +++ b/src/Azure.DataApiBuilder.Mcp/Core/McpProtocolDefaults.cs @@ -0,0 +1,39 @@ +using Azure.DataApiBuilder.Product; +using Microsoft.Extensions.Configuration; + +namespace Azure.DataApiBuilder.Mcp.Core +{ + /// + /// Centralized defaults and configuration keys for MCP protocol settings. + /// + public static class McpProtocolDefaults + { + /// + /// Default MCP server name advertised during initialization. + /// + public const string MCP_SERVER_NAME = "SQL MCP Server"; + /// + /// Default MCP server version advertised during initialization. + /// + public static readonly string MCP_SERVER_VERSION = ProductInfo.GetProductVersion(); + /// + /// Default MCP protocol version advertised when no configuration override is provided. + /// + public const string DEFAULT_PROTOCOL_VERSION = "2025-06-18"; + + /// + /// Configuration key used to override the MCP protocol version. + /// + public const string PROTOCOL_VERSION_CONFIG_KEY = "MCP:ProtocolVersion"; + + /// + /// Helper to resolve the effective protocol version from configuration. + /// Falls back to when the key is not set. + /// + public static string ResolveProtocolVersion(IConfiguration? configuration) + { + return configuration?.GetValue(PROTOCOL_VERSION_CONFIG_KEY) ?? DEFAULT_PROTOCOL_VERSION; + } + } +} + diff --git a/src/Azure.DataApiBuilder.Mcp/Core/McpServerConfiguration.cs b/src/Azure.DataApiBuilder.Mcp/Core/McpServerConfiguration.cs index 86cccd2aaf..d76af816bd 100644 --- a/src/Azure.DataApiBuilder.Mcp/Core/McpServerConfiguration.cs +++ b/src/Azure.DataApiBuilder.Mcp/Core/McpServerConfiguration.cs @@ -21,7 +21,7 @@ internal static IServiceCollection ConfigureMcpServer(this IServiceCollection se { services.AddMcpServer(options => { - options.ServerInfo = new() { Name = "Data API builder MCP Server", Version = "1.0.0" }; + options.ServerInfo = new() { Name = McpProtocolDefaults.MCP_SERVER_NAME, Version = McpProtocolDefaults.MCP_SERVER_VERSION }; options.Capabilities = new() { Tools = new() diff --git a/src/Azure.DataApiBuilder.Mcp/Core/McpStdioServer.cs b/src/Azure.DataApiBuilder.Mcp/Core/McpStdioServer.cs new file mode 100644 index 0000000000..6584e819af --- /dev/null +++ b/src/Azure.DataApiBuilder.Mcp/Core/McpStdioServer.cs @@ -0,0 +1,476 @@ +using System.Collections; +using System.Reflection; +using System.Security.Claims; +using System.Text; +using System.Text.Json; +using Azure.DataApiBuilder.Core.AuthenticationHelpers.AuthenticationSimulator; +using Azure.DataApiBuilder.Mcp.Model; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using ModelContextProtocol.Protocol; + +namespace Azure.DataApiBuilder.Mcp.Core +{ + /// + /// MCP stdio server: + /// - Reads JSON-RPC requests (initialize, listTools, callTool) from STDIN + /// - Writes ONLY MCP JSON responses to STDOUT + /// - Writes diagnostics to STDERR (so STDOUT remains “pure MCP”) + /// + public class McpStdioServer : IMcpStdioServer + { + private readonly McpToolRegistry _toolRegistry; + private readonly IServiceProvider _serviceProvider; + private readonly string _protocolVersion; + + private const int MAX_LINE_LENGTH = 1024 * 1024; // 1 MB limit for incoming JSON-RPC requests + + public McpStdioServer(McpToolRegistry toolRegistry, IServiceProvider serviceProvider) + { + _toolRegistry = toolRegistry ?? throw new ArgumentNullException(nameof(toolRegistry)); + _serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); + + // Allow protocol version to be configured via IConfiguration, using centralized defaults. + IConfiguration? configuration = _serviceProvider.GetService(); + _protocolVersion = McpProtocolDefaults.ResolveProtocolVersion(configuration); + } + + /// + /// Runs the MCP stdio server loop, reading JSON-RPC requests from STDIN and writing MCP JSON responses to STDOUT. + /// + /// Token to signal cancellation of the server loop. + /// A task representing the asynchronous operation. + public async Task RunAsync(CancellationToken cancellationToken) + { + Console.Error.WriteLine("[MCP DEBUG] MCP stdio server started."); + + // Use UTF-8 WITHOUT BOM + UTF8Encoding utf8NoBom = new(encoderShouldEmitUTF8Identifier: false); + + using Stream stdin = Console.OpenStandardInput(); + using Stream stdout = Console.OpenStandardOutput(); + using StreamReader reader = new(stdin, utf8NoBom); + using StreamWriter writer = new(stdout, utf8NoBom) { AutoFlush = true }; + + // Redirect Console.Out to use our writer + Console.SetOut(writer); + while (!cancellationToken.IsCancellationRequested) + { + string? line = await reader.ReadLineAsync(cancellationToken); + if (string.IsNullOrWhiteSpace(line)) + { + continue; + } + + if (line.Length > MAX_LINE_LENGTH) + { + WriteError(id: null, code: McpStdioJsonRpcErrorCodes.INVALID_REQUEST, message: "Request too large"); + continue; + } + + JsonDocument doc; + try + { + doc = JsonDocument.Parse(line); + } + catch (JsonException jsonEx) + { + Console.Error.WriteLine($"[MCP DEBUG] JSON parse error: {jsonEx.Message}"); + WriteError(id: null, code: McpStdioJsonRpcErrorCodes.PARSE_ERROR, message: "Parse error"); + continue; + } + catch (Exception ex) + { + Console.Error.WriteLine($"[MCP DEBUG] Unexpected error parsing request: {ex.Message}"); + WriteError(id: null, code: McpStdioJsonRpcErrorCodes.INTERNAL_ERROR, message: "Internal error"); + continue; + } + + using (doc) + { + JsonElement root = doc.RootElement; + + JsonElement? id = null; + if (root.TryGetProperty("id", out JsonElement idEl)) + { + id = idEl; // preserve original type (string or number) + } + + if (!root.TryGetProperty("method", out JsonElement methodEl)) + { + WriteError(id, McpStdioJsonRpcErrorCodes.INVALID_REQUEST, "Invalid Request"); + continue; + } + + string method = methodEl.GetString() ?? string.Empty; + + try + { + switch (method) + { + case "initialize": + HandleInitialize(id); + break; + + case "notifications/initialized": + break; + + case "tools/list": + HandleListTools(id); + break; + + case "tools/call": + await HandleCallToolAsync(id, root, cancellationToken); + break; + + case "ping": + WriteResult(id, new { ok = true }); + break; + + case "shutdown": + WriteResult(id, new { ok = true }); + return; + + default: + WriteError(id, McpStdioJsonRpcErrorCodes.METHOD_NOT_FOUND, $"Method not found: {method}"); + break; + } + } + catch (Exception) + { + WriteError(id, McpStdioJsonRpcErrorCodes.INTERNAL_ERROR, "Internal error"); + } + } + } + } + + /// + /// Handles the "initialize" JSON-RPC method by sending the MCP protocol version, server capabilities, and server info to the client. + /// + /// + /// The request identifier extracted from the incoming JSON-RPC request. Used to correlate the response with the request. + /// + /// + /// This method constructs and writes the MCP "initialize" response to STDOUT. It uses the protocol version defined by PROTOCOL_VERSION + /// and includes supported capabilities and server information. No notifications are sent here; the server waits for the client to send + /// "notifications/initialized" before sending any notifications. + /// + private void HandleInitialize(JsonElement? id) + { + var result = new + { + protocolVersion = _protocolVersion, + capabilities = new + { + tools = new { listChanged = true }, + logging = new { } + }, + serverInfo = new + { + name = McpProtocolDefaults.MCP_SERVER_NAME, + version = McpProtocolDefaults.MCP_SERVER_VERSION + } + }; + + WriteResult(id, result); + } + + /// + /// Handles the "tools/list" JSON-RPC method by sending the list of available tools to the client. + /// + /// + /// The request identifier extracted from the incoming JSON-RPC request. Used to correlate the response with the request. + /// + private void HandleListTools(JsonElement? id) + { + List toolsWire = new(); + int count = 0; + + // Tools are expected to be registered during application startup only. + // If this ever changes and tools can be added/removed at runtime while + // requests are being handled, we may need to introduce locking here or + // have the registry return a thread-safe snapshot. + foreach (Tool tool in _toolRegistry.GetAllTools()) + { + count++; + toolsWire.Add(new + { + name = tool.Name, + description = tool.Description, + inputSchema = tool.InputSchema + }); + } + + WriteResult(id, new { tools = toolsWire }); + } + + /// + /// Handles the "tools/call" JSON-RPC method by executing the specified tool with the provided arguments. + /// + /// The request identifier extracted from the incoming JSON-RPC request. Used to correlate the response with the request. + /// The root JSON element of the incoming JSON-RPC request. + /// Cancellation token to signal operation cancellation. + private async Task HandleCallToolAsync(JsonElement? id, JsonElement root, CancellationToken ct) + { + if (!root.TryGetProperty("params", out JsonElement @params) || @params.ValueKind != JsonValueKind.Object) + { + WriteError(id, McpStdioJsonRpcErrorCodes.INVALID_PARAMS, "Missing params"); + return; + } + + // If neither params.name (the MCP-standard field for the tool identifier) + // nor the legacy params.tool field is present or non-empty, we cannot tell + // which tool to execute. In that case we log a debug message to STDERR for + // diagnostics and return a JSON-RPC error (-32602 "Missing tool name") to + // the MCP client so it can fix the request payload. + string? toolName = null; + if (@params.TryGetProperty("name", out JsonElement nameEl) && nameEl.ValueKind == JsonValueKind.String) + { + toolName = nameEl.GetString(); + } + else if (@params.TryGetProperty("tool", out JsonElement toolEl) && toolEl.ValueKind == JsonValueKind.String) + { + toolName = toolEl.GetString(); + } + + if (string.IsNullOrWhiteSpace(toolName)) + { + Console.Error.WriteLine("[MCP DEBUG] callTool → missing tool name."); + WriteError(id, McpStdioJsonRpcErrorCodes.INVALID_PARAMS, "Missing tool name"); + return; + } + + if (!_toolRegistry.TryGetTool(toolName!, out IMcpTool? tool) || tool is null) + { + Console.Error.WriteLine($"[MCP DEBUG] callTool → tool not found: {toolName}"); + WriteError(id, McpStdioJsonRpcErrorCodes.INVALID_PARAMS, $"Tool not found: {toolName}"); + return; + } + + JsonDocument? argsDoc = null; + try + { + if (@params.TryGetProperty("arguments", out JsonElement argsEl) && argsEl.ValueKind == JsonValueKind.Object) + { + string rawArgs = argsEl.GetRawText(); + Console.Error.WriteLine($"[MCP DEBUG] callTool → tool: {toolName}, args: {rawArgs}"); + argsDoc = JsonDocument.Parse(rawArgs); + } + else + { + Console.Error.WriteLine($"[MCP DEBUG] callTool → tool: {toolName}, args: "); + } + + // Execute the tool. + // If a MCP stdio role override is set in the environment, create + // a request HttpContext with the X-MS-API-ROLE header so tools and authorization + // helpers that read IHttpContextAccessor will see the role. We also ensure the + // Simulator authentication handler can authenticate the user by flowing the + // Authorization header commonly used in tests/simulator scenarios. + CallToolResult callResult; + IConfiguration? configuration = _serviceProvider.GetService(); + string? stdioRole = configuration?.GetValue("MCP:Role"); + if (!string.IsNullOrWhiteSpace(stdioRole)) + { + IServiceScopeFactory scopeFactory = _serviceProvider.GetRequiredService(); + using IServiceScope scope = scopeFactory.CreateScope(); + IServiceProvider scopedProvider = scope.ServiceProvider; + + // Create a default HttpContext and set the client role header + DefaultHttpContext httpContext = new(); + httpContext.Request.Headers["X-MS-API-ROLE"] = stdioRole; + + // Build a simulator-style identity with the given role + ClaimsIdentity identity = new( + authenticationType: SimulatorAuthenticationDefaults.AUTHENTICATIONSCHEME); + identity.AddClaim(new Claim(ClaimTypes.Role, stdioRole)); + httpContext.User = new ClaimsPrincipal(identity); + + // If IHttpContextAccessor is registered, populate it for downstream code. + IHttpContextAccessor? httpContextAccessor = scopedProvider.GetService(); + if (httpContextAccessor is not null) + { + httpContextAccessor.HttpContext = httpContext; + } + + try + { + // Execute the tool with the scoped service provider so any scoped services resolve correctly. + callResult = await tool.ExecuteAsync(argsDoc, scopedProvider, ct); + } + finally + { + // Clear the accessor's HttpContext to avoid leaking across calls + if (httpContextAccessor is not null) + { + httpContextAccessor.HttpContext = null; + } + } + } + else + { + callResult = await tool.ExecuteAsync(argsDoc, _serviceProvider, ct); + } + + // Normalize to MCP content blocks (array). We try to pass through if a 'Content' property exists, + // otherwise we wrap into a single text block. + object[] content = CoerceToMcpContentBlocks(callResult); + + WriteResult(id, new { content }); + } + finally + { + argsDoc?.Dispose(); + } + } + + /// + /// Coerces the call result into an array of MCP content blocks. + /// Tools can either return a custom object with a public "Content" property + /// or a raw value; this helper normalizes both patterns into the MCP wire format. + /// + /// The result object returned from a tool execution. + /// An array of content blocks suitable for MCP output. + private static object[] CoerceToMcpContentBlocks(object? callResult) + { + if (callResult is null) + { + return Array.Empty(); + } + + // Prefer a public instance "Content" property if present. + PropertyInfo? prop = callResult.GetType().GetProperty("Content", BindingFlags.Instance | BindingFlags.Public); + + if (prop is not null) + { + object? value = prop.GetValue(callResult); + + if (value is IEnumerable enumerable && value is not string) + { + List list = new(); + foreach (object item in enumerable) + { + if (item is string s) + { + list.Add(new { type = "text", text = s }); + } + else if (item is JsonElement jsonEl) + { + list.Add(new { type = "application/json", data = jsonEl }); + } + else + { + list.Add(item); + } + } + + return list.ToArray(); + } + + if (value is string sContent) + { + return new object[] { new { type = "text", text = sContent } }; + } + + if (value is JsonElement jsonContent) + { + return new object[] { new { type = "application/json", data = jsonContent } }; + } + } + + // If callResult itself is a JsonElement, treat it as application/json. + if (callResult is JsonElement jsonResult) + { + return new object[] { new { type = "application/json", data = jsonResult } }; + } + + // Fallback: serialize to text. + string text = SafeToString(callResult); + return new object[] { new { type = "text", text } }; + } + + /// + /// Safely converts an object to its string representation, preferring JSON serialization for readability. + /// + /// The object to convert to a string. + /// A string representation of the object. + private static string SafeToString(object obj) + { + try + { + // Try JSON first for readability + string json = JsonSerializer.Serialize(obj); + + // If JSON is extremely large, truncate to avoid flooding MCP output. + // 32 KB is large enough to show useful JSON detail for diagnostics + // without flooding MCP output or impacting performance. + const int MAX_JSON_PREVIEW_CHARS = 32 * 1024; // 32 KB + + if (json.Length > MAX_JSON_PREVIEW_CHARS) + { + return string.Concat(json.AsSpan(0, MAX_JSON_PREVIEW_CHARS), $"... [truncated, total length={json.Length} chars]"); + } + + return json; + } + catch + { + return obj.ToString() ?? string.Empty; + } + } + + /// + /// Writes a JSON-RPC result response to the standard output. + /// + /// The request identifier extracted from the incoming JSON-RPC request. Used to correlate the response with the request. + /// The result object to include in the response. + private static void WriteResult(JsonElement? id, object resultObject) + { + var response = new + { + jsonrpc = "2.0", + id = id.HasValue ? GetIdValue(id.Value) : null, + result = resultObject + }; + + string json = JsonSerializer.Serialize(response); + Console.Out.WriteLine(json); + } + + /// + /// Writes a JSON-RPC error response to the standard output. + /// + /// The request identifier extracted from the incoming JSON-RPC request. Used to correlate the response with the request. + /// The error code. + /// The error message. + private static void WriteError(JsonElement? id, int code, string message) + { + var errorObj = new + { + jsonrpc = "2.0", + id = id.HasValue ? GetIdValue(id.Value) : null, + error = new { code, message } + }; + + string json = JsonSerializer.Serialize(errorObj); + Console.Out.WriteLine(json); + } + + /// + /// Extracts the value of a JSON-RPC request identifier. + /// + /// The JSON element representing the request identifier. + /// The extracted identifier value as an object, or null if the identifier is not a primitive type. + private static object? GetIdValue(JsonElement id) + { + return id.ValueKind switch + { + JsonValueKind.String => id.GetString(), + JsonValueKind.Number => id.TryGetInt64(out long l) ? l : + id.TryGetDouble(out double d) ? d : null, + _ => null + }; + } + } +} diff --git a/src/Azure.DataApiBuilder.Mcp/IMcpStdioServer.cs b/src/Azure.DataApiBuilder.Mcp/IMcpStdioServer.cs new file mode 100644 index 0000000000..033e0e3eaa --- /dev/null +++ b/src/Azure.DataApiBuilder.Mcp/IMcpStdioServer.cs @@ -0,0 +1,7 @@ +namespace Azure.DataApiBuilder.Mcp.Core +{ + public interface IMcpStdioServer + { + Task RunAsync(CancellationToken cancellationToken); + } +} diff --git a/src/Azure.DataApiBuilder.Mcp/Model/McpStdioJsonRpcErrorCodes.cs b/src/Azure.DataApiBuilder.Mcp/Model/McpStdioJsonRpcErrorCodes.cs new file mode 100644 index 0000000000..3bac194068 --- /dev/null +++ b/src/Azure.DataApiBuilder.Mcp/Model/McpStdioJsonRpcErrorCodes.cs @@ -0,0 +1,36 @@ +namespace Azure.DataApiBuilder.Mcp.Model +{ + /// + /// JSON-RPC 2.0 standard error codes used by the MCP stdio server. + /// These values come from the JSON-RPC 2.0 specification and are shared + /// so they are not hard-coded throughout the codebase. + /// + internal static class McpStdioJsonRpcErrorCodes + { + /// + /// Invalid JSON was received by the server. + /// An error occurred on the server while parsing the JSON text. + /// + public const int PARSE_ERROR = -32700; + + /// + /// The JSON sent is not a valid Request object. + /// + public const int INVALID_REQUEST = -32600; + + /// + /// The method does not exist / is not available. + /// + public const int METHOD_NOT_FOUND = -32601; + + /// + /// Invalid method parameter(s). + /// + public const int INVALID_PARAMS = -32602; + + /// + /// Internal JSON-RPC error. + /// + public const int INTERNAL_ERROR = -32603; + } +} diff --git a/src/Cli.Tests/AddOpenTelemetryTests.cs b/src/Cli.Tests/AddOpenTelemetryTests.cs index d84473980c..b1a702d158 100644 --- a/src/Cli.Tests/AddOpenTelemetryTests.cs +++ b/src/Cli.Tests/AddOpenTelemetryTests.cs @@ -138,7 +138,7 @@ private static string GenerateRuntimeSection(string telemetrySection) ""allow-credentials"": false }}, ""authentication"": {{ - ""provider"": ""StaticWebApps"" + ""provider"": ""AppService"" }} }}, {telemetrySection} diff --git a/src/Cli.Tests/AddTelemetryTests.cs b/src/Cli.Tests/AddTelemetryTests.cs index ac4fe99a8f..37cd77f17d 100644 --- a/src/Cli.Tests/AddTelemetryTests.cs +++ b/src/Cli.Tests/AddTelemetryTests.cs @@ -140,7 +140,7 @@ private static string GenerateRuntimeSection(string telemetrySection) ""allow-credentials"": false }}, ""authentication"": {{ - ""provider"": ""StaticWebApps"" + ""provider"": ""AppService"" }} }}, {telemetrySection} diff --git a/src/Cli.Tests/ConfigGeneratorTests.cs b/src/Cli.Tests/ConfigGeneratorTests.cs index 604860eb69..59a7f7b8dd 100644 --- a/src/Cli.Tests/ConfigGeneratorTests.cs +++ b/src/Cli.Tests/ConfigGeneratorTests.cs @@ -139,7 +139,7 @@ public void TestSpecialCharactersInConnectionString() setSessionContext: false, hostMode: HostMode.Production, corsOrigin: null, - authenticationProvider: EasyAuthType.StaticWebApps.ToString(), + authenticationProvider: EasyAuthType.AppService.ToString(), config: TEST_RUNTIME_CONFIG_FILE); StringBuilder expectedRuntimeConfigJson = new( @@ -173,7 +173,7 @@ public void TestSpecialCharactersInConnectionString() ""allow-credentials"": false }, ""authentication"": { - ""provider"": ""StaticWebApps"" + ""provider"": ""AppService"" }, ""mode"": ""production"" } diff --git a/src/Cli.Tests/EndToEndTests.cs b/src/Cli.Tests/EndToEndTests.cs index 5dbf97ca5e..99a9b77b6e 100644 --- a/src/Cli.Tests/EndToEndTests.cs +++ b/src/Cli.Tests/EndToEndTests.cs @@ -226,7 +226,7 @@ public void TestEnablingMultipleCreateOperation(CliBool isMultipleCreateEnabled, public void TestAddEntity() { string[] initArgs = { "init", "-c", TEST_RUNTIME_CONFIG_FILE, "--host-mode", "development", "--database-type", - "mssql", "--connection-string", TEST_ENV_CONN_STRING, "--auth.provider", "StaticWebApps" }; + "mssql", "--connection-string", TEST_ENV_CONN_STRING, "--auth.provider", "AppService" }; Program.Execute(initArgs, _cliLogger!, _fileSystem!, _runtimeConfigLoader!); Assert.IsTrue(_runtimeConfigLoader!.TryLoadConfig(TEST_RUNTIME_CONFIG_FILE, out RuntimeConfig? runtimeConfig)); diff --git a/src/Cli.Tests/InitTests.cs b/src/Cli.Tests/InitTests.cs index 80eda44788..051bfdf7a7 100644 --- a/src/Cli.Tests/InitTests.cs +++ b/src/Cli.Tests/InitTests.cs @@ -49,7 +49,7 @@ public Task MsSQLDatabase() setSessionContext: true, hostMode: HostMode.Development, corsOrigin: new List() { "http://localhost:3000", "http://nolocalhost:80" }, - authenticationProvider: EasyAuthType.StaticWebApps.ToString(), + authenticationProvider: EasyAuthType.AppService.ToString(), restPath: "rest-api", config: TEST_RUNTIME_CONFIG_FILE); @@ -71,7 +71,7 @@ public Task CosmosDbPostgreSqlDatabase() setSessionContext: false, hostMode: HostMode.Development, corsOrigin: new List() { "http://localhost:3000", "http://nolocalhost:80" }, - authenticationProvider: EasyAuthType.StaticWebApps.ToString(), + authenticationProvider: EasyAuthType.AppService.ToString(), restPath: "/rest-endpoint", config: TEST_RUNTIME_CONFIG_FILE); @@ -94,7 +94,7 @@ public Task TestInitializingConfigWithoutConnectionString() setSessionContext: false, hostMode: HostMode.Development, corsOrigin: new List() { "http://localhost:3000", "http://nolocalhost:80" }, - authenticationProvider: EasyAuthType.StaticWebApps.ToString(), + authenticationProvider: EasyAuthType.AppService.ToString(), config: TEST_RUNTIME_CONFIG_FILE); return ExecuteVerifyTest(options); @@ -118,7 +118,7 @@ public Task CosmosDbNoSqlDatabase() setSessionContext: false, hostMode: HostMode.Production, corsOrigin: null, - authenticationProvider: EasyAuthType.StaticWebApps.ToString(), + authenticationProvider: EasyAuthType.AppService.ToString(), config: TEST_RUNTIME_CONFIG_FILE); return ExecuteVerifyTest(options); @@ -151,7 +151,7 @@ bool expectSuccess setSessionContext: false, hostMode: HostMode.Production, corsOrigin: null, - authenticationProvider: EasyAuthType.StaticWebApps.ToString(), + authenticationProvider: EasyAuthType.AppService.ToString(), restEnabled: CliBool.True, graphqlEnabled: CliBool.True, config: TEST_RUNTIME_CONFIG_FILE); @@ -189,7 +189,7 @@ public void VerifyRequiredOptionsForCosmosDbNoSqlDatabase( setSessionContext: false, hostMode: HostMode.Production, corsOrigin: null, - authenticationProvider: EasyAuthType.StaticWebApps.ToString(), + authenticationProvider: EasyAuthType.AppService.ToString(), config: TEST_RUNTIME_CONFIG_FILE); Assert.AreEqual(expectedResult, TryCreateRuntimeConfig(options, _runtimeConfigLoader!, _fileSystem!, out RuntimeConfig? _)); @@ -219,7 +219,7 @@ public void EnsureFailureWhenBothRestAndGraphQLAreDisabled( setSessionContext: false, hostMode: HostMode.Production, corsOrigin: null, - authenticationProvider: EasyAuthType.StaticWebApps.ToString(), + authenticationProvider: EasyAuthType.AppService.ToString(), restEnabled: restEnabled, graphqlEnabled: graphQLEnabled, restDisabled: restDisabled, @@ -245,7 +245,7 @@ public Task TestSpecialCharactersInConnectionString() setSessionContext: false, hostMode: HostMode.Production, corsOrigin: null, - authenticationProvider: EasyAuthType.StaticWebApps.ToString(), + authenticationProvider: EasyAuthType.AppService.ToString(), config: TEST_RUNTIME_CONFIG_FILE); return ExecuteVerifyTest(options); @@ -267,7 +267,7 @@ public void EnsureFailureOnReInitializingExistingConfig() setSessionContext: false, hostMode: HostMode.Development, corsOrigin: new List() { }, - authenticationProvider: EasyAuthType.StaticWebApps.ToString(), + authenticationProvider: EasyAuthType.AppService.ToString(), config: TEST_RUNTIME_CONFIG_FILE); // Config generated successfully for the first time. @@ -346,7 +346,7 @@ public void EnsureFailureReInitializingExistingConfigWithDifferentCase() setSessionContext: true, hostMode: HostMode.Development, corsOrigin: new List() { "http://localhost:3000", "http://nolocalhost:80" }, - authenticationProvider: EasyAuthType.StaticWebApps.ToString(), + authenticationProvider: EasyAuthType.AppService.ToString(), restPath: "rest-api", config: TEST_RUNTIME_CONFIG_FILE); Assert.AreEqual(true, TryGenerateConfig(initOptionsWithAllLowerCaseFileName, _runtimeConfigLoader!, _fileSystem!)); @@ -361,7 +361,7 @@ public void EnsureFailureReInitializingExistingConfigWithDifferentCase() setSessionContext: true, hostMode: HostMode.Development, corsOrigin: new List() { "http://localhost:3000", "http://nolocalhost:80" }, - authenticationProvider: EasyAuthType.StaticWebApps.ToString(), + authenticationProvider: EasyAuthType.AppService.ToString(), restPath: "rest-api", config: TEST_RUNTIME_CONFIG_FILE.ToUpper()); // Platform Dependent @@ -384,7 +384,7 @@ public Task RestPathWithoutStartingSlashWillHaveItAdded() setSessionContext: false, hostMode: HostMode.Production, corsOrigin: null, - authenticationProvider: EasyAuthType.StaticWebApps.ToString(), + authenticationProvider: EasyAuthType.AppService.ToString(), restPath: "abc", config: TEST_RUNTIME_CONFIG_FILE); @@ -403,7 +403,7 @@ public Task GraphQLPathWithoutStartingSlashWillHaveItAdded() setSessionContext: false, hostMode: HostMode.Production, corsOrigin: null, - authenticationProvider: EasyAuthType.StaticWebApps.ToString(), + authenticationProvider: EasyAuthType.AppService.ToString(), graphQLPath: "abc", config: TEST_RUNTIME_CONFIG_FILE); @@ -466,7 +466,7 @@ public Task VerifyCorrectConfigGenerationWithMultipleMutationOptions(DatabaseTyp setSessionContext: true, hostMode: HostMode.Development, corsOrigin: new List() { "http://localhost:3000", "http://nolocalhost:80" }, - authenticationProvider: EasyAuthType.StaticWebApps.ToString(), + authenticationProvider: EasyAuthType.AppService.ToString(), restPath: "rest-api", config: TEST_RUNTIME_CONFIG_FILE, multipleCreateOperationEnabled: isMultipleCreateEnabled); @@ -482,7 +482,7 @@ public Task VerifyCorrectConfigGenerationWithMultipleMutationOptions(DatabaseTyp setSessionContext: true, hostMode: HostMode.Development, corsOrigin: new List() { "http://localhost:3000", "http://nolocalhost:80" }, - authenticationProvider: EasyAuthType.StaticWebApps.ToString(), + authenticationProvider: EasyAuthType.AppService.ToString(), restPath: "rest-api", config: TEST_RUNTIME_CONFIG_FILE, multipleCreateOperationEnabled: isMultipleCreateEnabled); diff --git a/src/Cli.Tests/ModuleInitializer.cs b/src/Cli.Tests/ModuleInitializer.cs index e00dc00a89..a75a882968 100644 --- a/src/Cli.Tests/ModuleInitializer.cs +++ b/src/Cli.Tests/ModuleInitializer.cs @@ -77,6 +77,8 @@ public static void Init() VerifierSettings.IgnoreMember(config => config.McpDmlTools); // Ignore the IsStaticWebAppsIdentityProvider as that's unimportant from a test standpoint. VerifierSettings.IgnoreMember(config => config.IsStaticWebAppsIdentityProvider); + // Ignore the IsAppServiceIdentityProvider as that's unimportant from a test standpoint. + VerifierSettings.IgnoreMember(config => config.IsAppServiceIdentityProvider); // Ignore the RestPath as that's unimportant from a test standpoint. VerifierSettings.IgnoreMember(config => config.RestPath); // Ignore the GraphQLPath as that's unimportant from a test standpoint. diff --git a/src/Cli.Tests/Snapshots/AddEntityTests.AddEntityWithAnExistingNameButWithDifferentCase.verified.txt b/src/Cli.Tests/Snapshots/AddEntityTests.AddEntityWithAnExistingNameButWithDifferentCase.verified.txt index 44fbbaa18b..12d7802079 100644 --- a/src/Cli.Tests/Snapshots/AddEntityTests.AddEntityWithAnExistingNameButWithDifferentCase.verified.txt +++ b/src/Cli.Tests/Snapshots/AddEntityTests.AddEntityWithAnExistingNameButWithDifferentCase.verified.txt @@ -18,7 +18,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps + Provider: AppService } } }, diff --git a/src/Cli.Tests/Snapshots/AddEntityTests.AddEntityWithCachingEnabled.verified.txt b/src/Cli.Tests/Snapshots/AddEntityTests.AddEntityWithCachingEnabled.verified.txt index 6b3b5cc018..b4b428038b 100644 --- a/src/Cli.Tests/Snapshots/AddEntityTests.AddEntityWithCachingEnabled.verified.txt +++ b/src/Cli.Tests/Snapshots/AddEntityTests.AddEntityWithCachingEnabled.verified.txt @@ -18,7 +18,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps + Provider: AppService } } }, diff --git a/src/Cli.Tests/Snapshots/AddEntityTests.AddEntityWithPolicyAndFieldProperties_70de36ebf1478d0d.verified.txt b/src/Cli.Tests/Snapshots/AddEntityTests.AddEntityWithPolicyAndFieldProperties_70de36ebf1478d0d.verified.txt index 7c701037fd..10324a7f50 100644 --- a/src/Cli.Tests/Snapshots/AddEntityTests.AddEntityWithPolicyAndFieldProperties_70de36ebf1478d0d.verified.txt +++ b/src/Cli.Tests/Snapshots/AddEntityTests.AddEntityWithPolicyAndFieldProperties_70de36ebf1478d0d.verified.txt @@ -18,7 +18,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps + Provider: AppService } } }, diff --git a/src/Cli.Tests/Snapshots/AddEntityTests.AddEntityWithPolicyAndFieldProperties_9f612e68879149a3.verified.txt b/src/Cli.Tests/Snapshots/AddEntityTests.AddEntityWithPolicyAndFieldProperties_9f612e68879149a3.verified.txt index cec0a3d8ab..72d4c28329 100644 --- a/src/Cli.Tests/Snapshots/AddEntityTests.AddEntityWithPolicyAndFieldProperties_9f612e68879149a3.verified.txt +++ b/src/Cli.Tests/Snapshots/AddEntityTests.AddEntityWithPolicyAndFieldProperties_9f612e68879149a3.verified.txt @@ -18,7 +18,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps + Provider: AppService } } }, diff --git a/src/Cli.Tests/Snapshots/AddEntityTests.AddEntityWithPolicyAndFieldProperties_bea2d26f3e5462d8.verified.txt b/src/Cli.Tests/Snapshots/AddEntityTests.AddEntityWithPolicyAndFieldProperties_bea2d26f3e5462d8.verified.txt index c318861497..d21d3b195d 100644 --- a/src/Cli.Tests/Snapshots/AddEntityTests.AddEntityWithPolicyAndFieldProperties_bea2d26f3e5462d8.verified.txt +++ b/src/Cli.Tests/Snapshots/AddEntityTests.AddEntityWithPolicyAndFieldProperties_bea2d26f3e5462d8.verified.txt @@ -18,7 +18,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps + Provider: AppService } } }, diff --git a/src/Cli.Tests/Snapshots/AddEntityTests.AddNewEntityWhenEntitiesEmpty.verified.txt b/src/Cli.Tests/Snapshots/AddEntityTests.AddNewEntityWhenEntitiesEmpty.verified.txt index 8d8f226805..3d6deefaba 100644 --- a/src/Cli.Tests/Snapshots/AddEntityTests.AddNewEntityWhenEntitiesEmpty.verified.txt +++ b/src/Cli.Tests/Snapshots/AddEntityTests.AddNewEntityWhenEntitiesEmpty.verified.txt @@ -18,7 +18,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps + Provider: AppService } } }, diff --git a/src/Cli.Tests/Snapshots/AddEntityTests.AddNewEntityWhenEntitiesNotEmpty.verified.txt b/src/Cli.Tests/Snapshots/AddEntityTests.AddNewEntityWhenEntitiesNotEmpty.verified.txt index 4e3184736d..d2a1b26553 100644 --- a/src/Cli.Tests/Snapshots/AddEntityTests.AddNewEntityWhenEntitiesNotEmpty.verified.txt +++ b/src/Cli.Tests/Snapshots/AddEntityTests.AddNewEntityWhenEntitiesNotEmpty.verified.txt @@ -18,7 +18,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps + Provider: AppService } } }, diff --git a/src/Cli.Tests/Snapshots/AddEntityTests.AddNewEntityWhenEntitiesWithSourceAsStoredProcedure.verified.txt b/src/Cli.Tests/Snapshots/AddEntityTests.AddNewEntityWhenEntitiesWithSourceAsStoredProcedure.verified.txt index 21759deeed..2418bbaf9e 100644 --- a/src/Cli.Tests/Snapshots/AddEntityTests.AddNewEntityWhenEntitiesWithSourceAsStoredProcedure.verified.txt +++ b/src/Cli.Tests/Snapshots/AddEntityTests.AddNewEntityWhenEntitiesWithSourceAsStoredProcedure.verified.txt @@ -18,7 +18,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps + Provider: AppService } } }, diff --git a/src/Cli.Tests/Snapshots/AddEntityTests.AddStoredProcedureWithBothMcpProperties.verified.txt b/src/Cli.Tests/Snapshots/AddEntityTests.AddStoredProcedureWithBothMcpProperties.verified.txt new file mode 100644 index 0000000000..c5772388d6 --- /dev/null +++ b/src/Cli.Tests/Snapshots/AddEntityTests.AddStoredProcedureWithBothMcpProperties.verified.txt @@ -0,0 +1,61 @@ +{ + DataSource: { + DatabaseType: MSSQL + }, + Runtime: { + Rest: { + Enabled: true, + Path: /api, + RequestBodyStrict: true + }, + GraphQL: { + Enabled: true, + Path: /graphql, + AllowIntrospection: true + }, + Host: { + Cors: { + AllowCredentials: false + }, + Authentication: { + Provider: AppService + } + } + }, + Entities: [ + { + UpdateBook: { + Source: { + Object: dbo.UpdateBook, + Type: stored-procedure + }, + GraphQL: { + Singular: UpdateBook, + Plural: UpdateBooks, + Enabled: true, + Operation: Mutation + }, + Rest: { + Methods: [ + Post + ], + Enabled: true + }, + Permissions: [ + { + Role: anonymous, + Actions: [ + { + Action: Execute + } + ] + } + ], + Mcp: { + CustomToolEnabled: true, + DmlToolEnabled: false + } + } + } + ] +} \ No newline at end of file diff --git a/src/Cli.Tests/Snapshots/AddEntityTests.AddStoredProcedureWithBothMcpPropertiesEnabled.verified.txt b/src/Cli.Tests/Snapshots/AddEntityTests.AddStoredProcedureWithBothMcpPropertiesEnabled.verified.txt new file mode 100644 index 0000000000..f0c74a20b7 --- /dev/null +++ b/src/Cli.Tests/Snapshots/AddEntityTests.AddStoredProcedureWithBothMcpPropertiesEnabled.verified.txt @@ -0,0 +1,61 @@ +{ + DataSource: { + DatabaseType: MSSQL + }, + Runtime: { + Rest: { + Enabled: true, + Path: /api, + RequestBodyStrict: true + }, + GraphQL: { + Enabled: true, + Path: /graphql, + AllowIntrospection: true + }, + Host: { + Cors: { + AllowCredentials: false + }, + Authentication: { + Provider: AppService + } + } + }, + Entities: [ + { + GetAllBooks: { + Source: { + Object: dbo.GetAllBooks, + Type: stored-procedure + }, + GraphQL: { + Singular: GetAllBooks, + Plural: GetAllBooks, + Enabled: true, + Operation: Mutation + }, + Rest: { + Methods: [ + Post + ], + Enabled: true + }, + Permissions: [ + { + Role: anonymous, + Actions: [ + { + Action: Execute + } + ] + } + ], + Mcp: { + CustomToolEnabled: true, + DmlToolEnabled: true + } + } + } + ] +} \ No newline at end of file diff --git a/src/Cli.Tests/Snapshots/AddEntityTests.AddStoredProcedureWithMcpCustomToolEnabled.verified.txt b/src/Cli.Tests/Snapshots/AddEntityTests.AddStoredProcedureWithMcpCustomToolEnabled.verified.txt new file mode 100644 index 0000000000..50b1b5c571 --- /dev/null +++ b/src/Cli.Tests/Snapshots/AddEntityTests.AddStoredProcedureWithMcpCustomToolEnabled.verified.txt @@ -0,0 +1,61 @@ +{ + DataSource: { + DatabaseType: MSSQL + }, + Runtime: { + Rest: { + Enabled: true, + Path: /api, + RequestBodyStrict: true + }, + GraphQL: { + Enabled: true, + Path: /graphql, + AllowIntrospection: true + }, + Host: { + Cors: { + AllowCredentials: false + }, + Authentication: { + Provider: AppService + } + } + }, + Entities: [ + { + GetBookById: { + Source: { + Object: dbo.GetBookById, + Type: stored-procedure + }, + GraphQL: { + Singular: GetBookById, + Plural: GetBookByIds, + Enabled: true, + Operation: Mutation + }, + Rest: { + Methods: [ + Post + ], + Enabled: true + }, + Permissions: [ + { + Role: anonymous, + Actions: [ + { + Action: Execute + } + ] + } + ], + Mcp: { + CustomToolEnabled: true, + DmlToolEnabled: true + } + } + } + ] +} \ No newline at end of file diff --git a/src/Cli.Tests/Snapshots/AddEntityTests.AddTableEntityWithMcpDmlTools_mcpDmlTools=false_source=authors.verified.txt b/src/Cli.Tests/Snapshots/AddEntityTests.AddTableEntityWithMcpDmlTools_mcpDmlTools=false_source=authors.verified.txt new file mode 100644 index 0000000000..e1ba348224 --- /dev/null +++ b/src/Cli.Tests/Snapshots/AddEntityTests.AddTableEntityWithMcpDmlTools_mcpDmlTools=false_source=authors.verified.txt @@ -0,0 +1,57 @@ +{ + DataSource: { + DatabaseType: MSSQL + }, + Runtime: { + Rest: { + Enabled: true, + Path: /api, + RequestBodyStrict: true + }, + GraphQL: { + Enabled: true, + Path: /graphql, + AllowIntrospection: true + }, + Host: { + Cors: { + AllowCredentials: false + }, + Authentication: { + Provider: AppService + } + } + }, + Entities: [ + { + Author: { + Source: { + Object: authors, + Type: Table + }, + GraphQL: { + Singular: Author, + Plural: Authors, + Enabled: true + }, + Rest: { + Enabled: true + }, + Permissions: [ + { + Role: anonymous, + Actions: [ + { + Action: * + } + ] + } + ], + Mcp: { + CustomToolEnabled: false, + DmlToolEnabled: false + } + } + } + ] +} \ No newline at end of file diff --git a/src/Cli.Tests/Snapshots/AddEntityTests.AddTableEntityWithMcpDmlTools_mcpDmlTools=true_source=books.verified.txt b/src/Cli.Tests/Snapshots/AddEntityTests.AddTableEntityWithMcpDmlTools_mcpDmlTools=true_source=books.verified.txt new file mode 100644 index 0000000000..82287b53aa --- /dev/null +++ b/src/Cli.Tests/Snapshots/AddEntityTests.AddTableEntityWithMcpDmlTools_mcpDmlTools=true_source=books.verified.txt @@ -0,0 +1,57 @@ +{ + DataSource: { + DatabaseType: MSSQL + }, + Runtime: { + Rest: { + Enabled: true, + Path: /api, + RequestBodyStrict: true + }, + GraphQL: { + Enabled: true, + Path: /graphql, + AllowIntrospection: true + }, + Host: { + Cors: { + AllowCredentials: false + }, + Authentication: { + Provider: AppService + } + } + }, + Entities: [ + { + Book: { + Source: { + Object: books, + Type: Table + }, + GraphQL: { + Singular: Book, + Plural: Books, + Enabled: true + }, + Rest: { + Enabled: true + }, + Permissions: [ + { + Role: anonymous, + Actions: [ + { + Action: * + } + ] + } + ], + Mcp: { + CustomToolEnabled: false, + DmlToolEnabled: true + } + } + } + ] +} \ No newline at end of file diff --git a/src/Cli.Tests/Snapshots/AddEntityTests.TestAddNewSpWithDifferentRestAndGraphQLOptions_0c9cbb8942b4a4e5.verified.txt b/src/Cli.Tests/Snapshots/AddEntityTests.TestAddNewSpWithDifferentRestAndGraphQLOptions_0c9cbb8942b4a4e5.verified.txt index c7cb996d03..9729cf4aae 100644 --- a/src/Cli.Tests/Snapshots/AddEntityTests.TestAddNewSpWithDifferentRestAndGraphQLOptions_0c9cbb8942b4a4e5.verified.txt +++ b/src/Cli.Tests/Snapshots/AddEntityTests.TestAddNewSpWithDifferentRestAndGraphQLOptions_0c9cbb8942b4a4e5.verified.txt @@ -18,7 +18,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps + Provider: AppService } } }, diff --git a/src/Cli.Tests/Snapshots/AddEntityTests.TestAddNewSpWithDifferentRestAndGraphQLOptions_286d268a654ece27.verified.txt b/src/Cli.Tests/Snapshots/AddEntityTests.TestAddNewSpWithDifferentRestAndGraphQLOptions_286d268a654ece27.verified.txt index 13beb6bc7c..99d78eaa0e 100644 --- a/src/Cli.Tests/Snapshots/AddEntityTests.TestAddNewSpWithDifferentRestAndGraphQLOptions_286d268a654ece27.verified.txt +++ b/src/Cli.Tests/Snapshots/AddEntityTests.TestAddNewSpWithDifferentRestAndGraphQLOptions_286d268a654ece27.verified.txt @@ -18,7 +18,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps + Provider: AppService } } }, diff --git a/src/Cli.Tests/Snapshots/AddEntityTests.TestAddNewSpWithDifferentRestAndGraphQLOptions_3048323e01b42681.verified.txt b/src/Cli.Tests/Snapshots/AddEntityTests.TestAddNewSpWithDifferentRestAndGraphQLOptions_3048323e01b42681.verified.txt index bdd7c8f7a0..ec4549f38a 100644 --- a/src/Cli.Tests/Snapshots/AddEntityTests.TestAddNewSpWithDifferentRestAndGraphQLOptions_3048323e01b42681.verified.txt +++ b/src/Cli.Tests/Snapshots/AddEntityTests.TestAddNewSpWithDifferentRestAndGraphQLOptions_3048323e01b42681.verified.txt @@ -18,7 +18,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps + Provider: AppService } } }, diff --git a/src/Cli.Tests/Snapshots/AddEntityTests.TestAddNewSpWithDifferentRestAndGraphQLOptions_3440d150a2282b9c.verified.txt b/src/Cli.Tests/Snapshots/AddEntityTests.TestAddNewSpWithDifferentRestAndGraphQLOptions_3440d150a2282b9c.verified.txt index 20d7bcd624..6ba80ba348 100644 --- a/src/Cli.Tests/Snapshots/AddEntityTests.TestAddNewSpWithDifferentRestAndGraphQLOptions_3440d150a2282b9c.verified.txt +++ b/src/Cli.Tests/Snapshots/AddEntityTests.TestAddNewSpWithDifferentRestAndGraphQLOptions_3440d150a2282b9c.verified.txt @@ -18,7 +18,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps + Provider: AppService } } }, diff --git a/src/Cli.Tests/Snapshots/AddEntityTests.TestAddNewSpWithDifferentRestAndGraphQLOptions_381c28d25063be0c.verified.txt b/src/Cli.Tests/Snapshots/AddEntityTests.TestAddNewSpWithDifferentRestAndGraphQLOptions_381c28d25063be0c.verified.txt index 13beb6bc7c..99d78eaa0e 100644 --- a/src/Cli.Tests/Snapshots/AddEntityTests.TestAddNewSpWithDifferentRestAndGraphQLOptions_381c28d25063be0c.verified.txt +++ b/src/Cli.Tests/Snapshots/AddEntityTests.TestAddNewSpWithDifferentRestAndGraphQLOptions_381c28d25063be0c.verified.txt @@ -18,7 +18,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps + Provider: AppService } } }, diff --git a/src/Cli.Tests/Snapshots/AddEntityTests.TestAddNewSpWithDifferentRestAndGraphQLOptions_458373311f6ed4ed.verified.txt b/src/Cli.Tests/Snapshots/AddEntityTests.TestAddNewSpWithDifferentRestAndGraphQLOptions_458373311f6ed4ed.verified.txt index b87e804456..11d8adb815 100644 --- a/src/Cli.Tests/Snapshots/AddEntityTests.TestAddNewSpWithDifferentRestAndGraphQLOptions_458373311f6ed4ed.verified.txt +++ b/src/Cli.Tests/Snapshots/AddEntityTests.TestAddNewSpWithDifferentRestAndGraphQLOptions_458373311f6ed4ed.verified.txt @@ -18,7 +18,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps + Provider: AppService } } }, diff --git a/src/Cli.Tests/Snapshots/AddEntityTests.TestAddNewSpWithDifferentRestAndGraphQLOptions_66799c963a6306ae.verified.txt b/src/Cli.Tests/Snapshots/AddEntityTests.TestAddNewSpWithDifferentRestAndGraphQLOptions_66799c963a6306ae.verified.txt index 18c5f966c5..5c6a0b7d6b 100644 --- a/src/Cli.Tests/Snapshots/AddEntityTests.TestAddNewSpWithDifferentRestAndGraphQLOptions_66799c963a6306ae.verified.txt +++ b/src/Cli.Tests/Snapshots/AddEntityTests.TestAddNewSpWithDifferentRestAndGraphQLOptions_66799c963a6306ae.verified.txt @@ -18,7 +18,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps + Provider: AppService } } }, diff --git a/src/Cli.Tests/Snapshots/AddEntityTests.TestAddNewSpWithDifferentRestAndGraphQLOptions_66f598295b8682fd.verified.txt b/src/Cli.Tests/Snapshots/AddEntityTests.TestAddNewSpWithDifferentRestAndGraphQLOptions_66f598295b8682fd.verified.txt index 1cd138d10f..e3a6363fb1 100644 --- a/src/Cli.Tests/Snapshots/AddEntityTests.TestAddNewSpWithDifferentRestAndGraphQLOptions_66f598295b8682fd.verified.txt +++ b/src/Cli.Tests/Snapshots/AddEntityTests.TestAddNewSpWithDifferentRestAndGraphQLOptions_66f598295b8682fd.verified.txt @@ -18,7 +18,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps + Provider: AppService } } }, diff --git a/src/Cli.Tests/Snapshots/AddEntityTests.TestAddNewSpWithDifferentRestAndGraphQLOptions_73f95f7e2cd3ed71.verified.txt b/src/Cli.Tests/Snapshots/AddEntityTests.TestAddNewSpWithDifferentRestAndGraphQLOptions_73f95f7e2cd3ed71.verified.txt index c7cb996d03..9729cf4aae 100644 --- a/src/Cli.Tests/Snapshots/AddEntityTests.TestAddNewSpWithDifferentRestAndGraphQLOptions_73f95f7e2cd3ed71.verified.txt +++ b/src/Cli.Tests/Snapshots/AddEntityTests.TestAddNewSpWithDifferentRestAndGraphQLOptions_73f95f7e2cd3ed71.verified.txt @@ -18,7 +18,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps + Provider: AppService } } }, diff --git a/src/Cli.Tests/Snapshots/AddEntityTests.TestAddNewSpWithDifferentRestAndGraphQLOptions_79d59edde7f6a272.verified.txt b/src/Cli.Tests/Snapshots/AddEntityTests.TestAddNewSpWithDifferentRestAndGraphQLOptions_79d59edde7f6a272.verified.txt index b87e804456..11d8adb815 100644 --- a/src/Cli.Tests/Snapshots/AddEntityTests.TestAddNewSpWithDifferentRestAndGraphQLOptions_79d59edde7f6a272.verified.txt +++ b/src/Cli.Tests/Snapshots/AddEntityTests.TestAddNewSpWithDifferentRestAndGraphQLOptions_79d59edde7f6a272.verified.txt @@ -18,7 +18,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps + Provider: AppService } } }, diff --git a/src/Cli.Tests/Snapshots/AddEntityTests.TestAddNewSpWithDifferentRestAndGraphQLOptions_7ec82512a1df5293.verified.txt b/src/Cli.Tests/Snapshots/AddEntityTests.TestAddNewSpWithDifferentRestAndGraphQLOptions_7ec82512a1df5293.verified.txt index c7cb996d03..9729cf4aae 100644 --- a/src/Cli.Tests/Snapshots/AddEntityTests.TestAddNewSpWithDifferentRestAndGraphQLOptions_7ec82512a1df5293.verified.txt +++ b/src/Cli.Tests/Snapshots/AddEntityTests.TestAddNewSpWithDifferentRestAndGraphQLOptions_7ec82512a1df5293.verified.txt @@ -18,7 +18,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps + Provider: AppService } } }, diff --git a/src/Cli.Tests/Snapshots/AddEntityTests.TestAddNewSpWithDifferentRestAndGraphQLOptions_cbb6e5548e4d3535.verified.txt b/src/Cli.Tests/Snapshots/AddEntityTests.TestAddNewSpWithDifferentRestAndGraphQLOptions_cbb6e5548e4d3535.verified.txt index c7cb996d03..9729cf4aae 100644 --- a/src/Cli.Tests/Snapshots/AddEntityTests.TestAddNewSpWithDifferentRestAndGraphQLOptions_cbb6e5548e4d3535.verified.txt +++ b/src/Cli.Tests/Snapshots/AddEntityTests.TestAddNewSpWithDifferentRestAndGraphQLOptions_cbb6e5548e4d3535.verified.txt @@ -18,7 +18,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps + Provider: AppService } } }, diff --git a/src/Cli.Tests/Snapshots/AddEntityTests.TestAddNewSpWithDifferentRestAndGraphQLOptions_dc629052f38cea32.verified.txt b/src/Cli.Tests/Snapshots/AddEntityTests.TestAddNewSpWithDifferentRestAndGraphQLOptions_dc629052f38cea32.verified.txt index 612a65c14a..aa074116d0 100644 --- a/src/Cli.Tests/Snapshots/AddEntityTests.TestAddNewSpWithDifferentRestAndGraphQLOptions_dc629052f38cea32.verified.txt +++ b/src/Cli.Tests/Snapshots/AddEntityTests.TestAddNewSpWithDifferentRestAndGraphQLOptions_dc629052f38cea32.verified.txt @@ -18,7 +18,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps + Provider: AppService } } }, diff --git a/src/Cli.Tests/Snapshots/AddEntityTests.TestAddNewSpWithDifferentRestAndGraphQLOptions_e4a97c7e3507d2c6.verified.txt b/src/Cli.Tests/Snapshots/AddEntityTests.TestAddNewSpWithDifferentRestAndGraphQLOptions_e4a97c7e3507d2c6.verified.txt index 87b7a7697c..9b7d9f96e9 100644 --- a/src/Cli.Tests/Snapshots/AddEntityTests.TestAddNewSpWithDifferentRestAndGraphQLOptions_e4a97c7e3507d2c6.verified.txt +++ b/src/Cli.Tests/Snapshots/AddEntityTests.TestAddNewSpWithDifferentRestAndGraphQLOptions_e4a97c7e3507d2c6.verified.txt @@ -18,7 +18,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps + Provider: AppService } } }, diff --git a/src/Cli.Tests/Snapshots/AddEntityTests.TestAddNewSpWithDifferentRestAndGraphQLOptions_f8d0d0c2a38bd3b8.verified.txt b/src/Cli.Tests/Snapshots/AddEntityTests.TestAddNewSpWithDifferentRestAndGraphQLOptions_f8d0d0c2a38bd3b8.verified.txt index 1cd138d10f..e3a6363fb1 100644 --- a/src/Cli.Tests/Snapshots/AddEntityTests.TestAddNewSpWithDifferentRestAndGraphQLOptions_f8d0d0c2a38bd3b8.verified.txt +++ b/src/Cli.Tests/Snapshots/AddEntityTests.TestAddNewSpWithDifferentRestAndGraphQLOptions_f8d0d0c2a38bd3b8.verified.txt @@ -18,7 +18,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps + Provider: AppService } } }, diff --git a/src/Cli.Tests/Snapshots/AddEntityTests.TestAddStoredProcedureWithRestMethodsAndGraphQLOperations.verified.txt b/src/Cli.Tests/Snapshots/AddEntityTests.TestAddStoredProcedureWithRestMethodsAndGraphQLOperations.verified.txt index 83d3882a96..11829a214c 100644 --- a/src/Cli.Tests/Snapshots/AddEntityTests.TestAddStoredProcedureWithRestMethodsAndGraphQLOperations.verified.txt +++ b/src/Cli.Tests/Snapshots/AddEntityTests.TestAddStoredProcedureWithRestMethodsAndGraphQLOperations.verified.txt @@ -18,7 +18,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps + Provider: AppService } } }, diff --git a/src/Cli.Tests/Snapshots/EndToEndTests.TestAddingStoredProcedureWithRestMethodsAndGraphQLOperations.verified.txt b/src/Cli.Tests/Snapshots/EndToEndTests.TestAddingStoredProcedureWithRestMethodsAndGraphQLOperations.verified.txt index b072d5e5a0..3fa1fbc14e 100644 --- a/src/Cli.Tests/Snapshots/EndToEndTests.TestAddingStoredProcedureWithRestMethodsAndGraphQLOperations.verified.txt +++ b/src/Cli.Tests/Snapshots/EndToEndTests.TestAddingStoredProcedureWithRestMethodsAndGraphQLOperations.verified.txt @@ -41,7 +41,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps + Provider: AppService } } }, diff --git a/src/Cli.Tests/Snapshots/EndToEndTests.TestConfigGeneratedAfterAddingEntityWithSourceAsStoredProcedure.verified.txt b/src/Cli.Tests/Snapshots/EndToEndTests.TestConfigGeneratedAfterAddingEntityWithSourceAsStoredProcedure.verified.txt index ca7211c485..76ea01dfca 100644 --- a/src/Cli.Tests/Snapshots/EndToEndTests.TestConfigGeneratedAfterAddingEntityWithSourceAsStoredProcedure.verified.txt +++ b/src/Cli.Tests/Snapshots/EndToEndTests.TestConfigGeneratedAfterAddingEntityWithSourceAsStoredProcedure.verified.txt @@ -41,7 +41,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps + Provider: AppService } } }, diff --git a/src/Cli.Tests/Snapshots/EndToEndTests.TestConfigGeneratedAfterAddingEntityWithSourceWithDefaultType.verified.txt b/src/Cli.Tests/Snapshots/EndToEndTests.TestConfigGeneratedAfterAddingEntityWithSourceWithDefaultType.verified.txt index d7aadee93c..3a8c738a70 100644 --- a/src/Cli.Tests/Snapshots/EndToEndTests.TestConfigGeneratedAfterAddingEntityWithSourceWithDefaultType.verified.txt +++ b/src/Cli.Tests/Snapshots/EndToEndTests.TestConfigGeneratedAfterAddingEntityWithSourceWithDefaultType.verified.txt @@ -41,7 +41,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps + Provider: AppService } } }, diff --git a/src/Cli.Tests/Snapshots/EndToEndTests.TestConfigGeneratedAfterAddingEntityWithoutIEnumerables.verified.txt b/src/Cli.Tests/Snapshots/EndToEndTests.TestConfigGeneratedAfterAddingEntityWithoutIEnumerables.verified.txt index 331a040a45..df2cd4b009 100644 --- a/src/Cli.Tests/Snapshots/EndToEndTests.TestConfigGeneratedAfterAddingEntityWithoutIEnumerables.verified.txt +++ b/src/Cli.Tests/Snapshots/EndToEndTests.TestConfigGeneratedAfterAddingEntityWithoutIEnumerables.verified.txt @@ -41,7 +41,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps + Provider: AppService }, Mode: Production } diff --git a/src/Cli.Tests/Snapshots/EndToEndTests.TestInitForCosmosDBNoSql.verified.txt b/src/Cli.Tests/Snapshots/EndToEndTests.TestInitForCosmosDBNoSql.verified.txt index ff3f25d357..1b14a3a7f0 100644 --- a/src/Cli.Tests/Snapshots/EndToEndTests.TestInitForCosmosDBNoSql.verified.txt +++ b/src/Cli.Tests/Snapshots/EndToEndTests.TestInitForCosmosDBNoSql.verified.txt @@ -46,7 +46,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps + Provider: AppService }, Mode: Production } diff --git a/src/Cli.Tests/Snapshots/EndToEndTests.TestUpdatingStoredProcedureWithRestMethods.verified.txt b/src/Cli.Tests/Snapshots/EndToEndTests.TestUpdatingStoredProcedureWithRestMethods.verified.txt index a04dc2fe36..62d9e237b5 100644 --- a/src/Cli.Tests/Snapshots/EndToEndTests.TestUpdatingStoredProcedureWithRestMethods.verified.txt +++ b/src/Cli.Tests/Snapshots/EndToEndTests.TestUpdatingStoredProcedureWithRestMethods.verified.txt @@ -41,7 +41,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps + Provider: AppService } } }, diff --git a/src/Cli.Tests/Snapshots/EndToEndTests.TestUpdatingStoredProcedureWithRestMethodsAndGraphQLOperations.verified.txt b/src/Cli.Tests/Snapshots/EndToEndTests.TestUpdatingStoredProcedureWithRestMethodsAndGraphQLOperations.verified.txt index cfa928a025..fa8b16e739 100644 --- a/src/Cli.Tests/Snapshots/EndToEndTests.TestUpdatingStoredProcedureWithRestMethodsAndGraphQLOperations.verified.txt +++ b/src/Cli.Tests/Snapshots/EndToEndTests.TestUpdatingStoredProcedureWithRestMethodsAndGraphQLOperations.verified.txt @@ -41,7 +41,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps + Provider: AppService } } }, diff --git a/src/Cli.Tests/Snapshots/InitTests.CosmosDbNoSqlDatabase.verified.txt b/src/Cli.Tests/Snapshots/InitTests.CosmosDbNoSqlDatabase.verified.txt index b9b040aa2f..9d5458c0ee 100644 --- a/src/Cli.Tests/Snapshots/InitTests.CosmosDbNoSqlDatabase.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.CosmosDbNoSqlDatabase.verified.txt @@ -42,7 +42,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps + Provider: AppService }, Mode: Production } diff --git a/src/Cli.Tests/Snapshots/InitTests.CosmosDbPostgreSqlDatabase.verified.txt b/src/Cli.Tests/Snapshots/InitTests.CosmosDbPostgreSqlDatabase.verified.txt index 65b03f6293..51f6ad8d95 100644 --- a/src/Cli.Tests/Snapshots/InitTests.CosmosDbPostgreSqlDatabase.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.CosmosDbPostgreSqlDatabase.verified.txt @@ -42,7 +42,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps + Provider: AppService } } }, diff --git a/src/Cli.Tests/Snapshots/InitTests.GraphQLPathWithoutStartingSlashWillHaveItAdded.verified.txt b/src/Cli.Tests/Snapshots/InitTests.GraphQLPathWithoutStartingSlashWillHaveItAdded.verified.txt index bc6b6cfecb..a3a056ac0a 100644 --- a/src/Cli.Tests/Snapshots/InitTests.GraphQLPathWithoutStartingSlashWillHaveItAdded.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.GraphQLPathWithoutStartingSlashWillHaveItAdded.verified.txt @@ -41,7 +41,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps + Provider: AppService }, Mode: Production } diff --git a/src/Cli.Tests/Snapshots/InitTests.MsSQLDatabase.verified.txt b/src/Cli.Tests/Snapshots/InitTests.MsSQLDatabase.verified.txt index 3078fb644f..f40350c4da 100644 --- a/src/Cli.Tests/Snapshots/InitTests.MsSQLDatabase.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.MsSQLDatabase.verified.txt @@ -45,7 +45,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps + Provider: AppService } } }, diff --git a/src/Cli.Tests/Snapshots/InitTests.RestPathWithoutStartingSlashWillHaveItAdded.verified.txt b/src/Cli.Tests/Snapshots/InitTests.RestPathWithoutStartingSlashWillHaveItAdded.verified.txt index c888431526..b792d41c9f 100644 --- a/src/Cli.Tests/Snapshots/InitTests.RestPathWithoutStartingSlashWillHaveItAdded.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.RestPathWithoutStartingSlashWillHaveItAdded.verified.txt @@ -41,7 +41,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps + Provider: AppService }, Mode: Production } diff --git a/src/Cli.Tests/Snapshots/InitTests.TestInitializingConfigWithoutConnectionString.verified.txt b/src/Cli.Tests/Snapshots/InitTests.TestInitializingConfigWithoutConnectionString.verified.txt index 0273dcc976..173960d7b1 100644 --- a/src/Cli.Tests/Snapshots/InitTests.TestInitializingConfigWithoutConnectionString.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.TestInitializingConfigWithoutConnectionString.verified.txt @@ -45,7 +45,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps + Provider: AppService } } }, diff --git a/src/Cli.Tests/Snapshots/InitTests.TestSpecialCharactersInConnectionString.verified.txt b/src/Cli.Tests/Snapshots/InitTests.TestSpecialCharactersInConnectionString.verified.txt index ab71a40f03..25e3976685 100644 --- a/src/Cli.Tests/Snapshots/InitTests.TestSpecialCharactersInConnectionString.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.TestSpecialCharactersInConnectionString.verified.txt @@ -41,7 +41,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps + Provider: AppService }, Mode: Production } diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_0546bef37027a950.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_0546bef37027a950.verified.txt index 86a0716003..63f0da701c 100644 --- a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_0546bef37027a950.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_0546bef37027a950.verified.txt @@ -45,7 +45,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps + Provider: AppService } } }, diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_0ac567dd32a2e8f5.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_0ac567dd32a2e8f5.verified.txt index 3078fb644f..f40350c4da 100644 --- a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_0ac567dd32a2e8f5.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_0ac567dd32a2e8f5.verified.txt @@ -45,7 +45,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps + Provider: AppService } } }, diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_0c06949221514e77.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_0c06949221514e77.verified.txt index aac85044f9..e59070d692 100644 --- a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_0c06949221514e77.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_0c06949221514e77.verified.txt @@ -50,7 +50,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps + Provider: AppService } } }, diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_18667ab7db033e9d.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_18667ab7db033e9d.verified.txt index c7904175e0..f7de35b7ae 100644 --- a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_18667ab7db033e9d.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_18667ab7db033e9d.verified.txt @@ -42,7 +42,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps + Provider: AppService } } }, diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_2f42f44c328eb020.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_2f42f44c328eb020.verified.txt index 86a0716003..63f0da701c 100644 --- a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_2f42f44c328eb020.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_2f42f44c328eb020.verified.txt @@ -45,7 +45,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps + Provider: AppService } } }, diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_3243d3f3441fdcc1.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_3243d3f3441fdcc1.verified.txt index c7904175e0..f7de35b7ae 100644 --- a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_3243d3f3441fdcc1.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_3243d3f3441fdcc1.verified.txt @@ -42,7 +42,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps + Provider: AppService } } }, diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_53350b8b47df2112.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_53350b8b47df2112.verified.txt index d70704315e..75613db959 100644 --- a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_53350b8b47df2112.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_53350b8b47df2112.verified.txt @@ -42,7 +42,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps + Provider: AppService } } }, diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_6584e0ec46b8a11d.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_6584e0ec46b8a11d.verified.txt index ac3815f949..d93aac7dc6 100644 --- a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_6584e0ec46b8a11d.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_6584e0ec46b8a11d.verified.txt @@ -46,7 +46,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps + Provider: AppService } } }, diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_81cc88db3d4eecfb.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_81cc88db3d4eecfb.verified.txt index fd6a494ba3..640815babb 100644 --- a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_81cc88db3d4eecfb.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_81cc88db3d4eecfb.verified.txt @@ -50,7 +50,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps + Provider: AppService } } }, diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_8ea187616dbb5577.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_8ea187616dbb5577.verified.txt index 8044f643ac..5900015d5a 100644 --- a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_8ea187616dbb5577.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_8ea187616dbb5577.verified.txt @@ -42,7 +42,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps + Provider: AppService } } }, diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_905845c29560a3ef.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_905845c29560a3ef.verified.txt index 86a0716003..63f0da701c 100644 --- a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_905845c29560a3ef.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_905845c29560a3ef.verified.txt @@ -45,7 +45,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps + Provider: AppService } } }, diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_b2fd24fab5b80917.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_b2fd24fab5b80917.verified.txt index ac3815f949..d93aac7dc6 100644 --- a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_b2fd24fab5b80917.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_b2fd24fab5b80917.verified.txt @@ -46,7 +46,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps + Provider: AppService } } }, diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_bd7cd088755287c9.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_bd7cd088755287c9.verified.txt index ac3815f949..d93aac7dc6 100644 --- a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_bd7cd088755287c9.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_bd7cd088755287c9.verified.txt @@ -46,7 +46,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps + Provider: AppService } } }, diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_d2eccba2f836b380.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_d2eccba2f836b380.verified.txt index d70704315e..75613db959 100644 --- a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_d2eccba2f836b380.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_d2eccba2f836b380.verified.txt @@ -42,7 +42,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps + Provider: AppService } } }, diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_d463eed7fe5e4bbe.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_d463eed7fe5e4bbe.verified.txt index 8044f643ac..5900015d5a 100644 --- a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_d463eed7fe5e4bbe.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_d463eed7fe5e4bbe.verified.txt @@ -42,7 +42,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps + Provider: AppService } } }, diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_d5520dd5c33f7b8d.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_d5520dd5c33f7b8d.verified.txt index d70704315e..75613db959 100644 --- a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_d5520dd5c33f7b8d.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_d5520dd5c33f7b8d.verified.txt @@ -42,7 +42,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps + Provider: AppService } } }, diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_eab4a6010e602b59.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_eab4a6010e602b59.verified.txt index c7904175e0..f7de35b7ae 100644 --- a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_eab4a6010e602b59.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_eab4a6010e602b59.verified.txt @@ -42,7 +42,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps + Provider: AppService } } }, diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_ecaa688829b4030e.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_ecaa688829b4030e.verified.txt index 8044f643ac..5900015d5a 100644 --- a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_ecaa688829b4030e.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_ecaa688829b4030e.verified.txt @@ -42,7 +42,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps + Provider: AppService } } }, diff --git a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestConversionOfSourceObject_036a859f50ce167c.verified.txt b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestConversionOfSourceObject_036a859f50ce167c.verified.txt index 260eecd0c9..003bc17a3e 100644 --- a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestConversionOfSourceObject_036a859f50ce167c.verified.txt +++ b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestConversionOfSourceObject_036a859f50ce167c.verified.txt @@ -18,7 +18,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps + Provider: AppService } } }, diff --git a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestConversionOfSourceObject_103655d39b48d89f.verified.txt b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestConversionOfSourceObject_103655d39b48d89f.verified.txt index 80f61e17ac..29d477944f 100644 --- a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestConversionOfSourceObject_103655d39b48d89f.verified.txt +++ b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestConversionOfSourceObject_103655d39b48d89f.verified.txt @@ -18,7 +18,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps + Provider: AppService } } }, diff --git a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestConversionOfSourceObject_442649c7ef2176bd.verified.txt b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestConversionOfSourceObject_442649c7ef2176bd.verified.txt index 260eecd0c9..003bc17a3e 100644 --- a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestConversionOfSourceObject_442649c7ef2176bd.verified.txt +++ b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestConversionOfSourceObject_442649c7ef2176bd.verified.txt @@ -18,7 +18,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps + Provider: AppService } } }, diff --git a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestConversionOfSourceObject_7f2338fdc84aafc3.verified.txt b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestConversionOfSourceObject_7f2338fdc84aafc3.verified.txt index b6b9269e87..edfefa5bac 100644 --- a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestConversionOfSourceObject_7f2338fdc84aafc3.verified.txt +++ b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestConversionOfSourceObject_7f2338fdc84aafc3.verified.txt @@ -18,7 +18,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps + Provider: AppService } } }, diff --git a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestConversionOfSourceObject_a70c086a74142c82.verified.txt b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestConversionOfSourceObject_a70c086a74142c82.verified.txt index 21759deeed..2418bbaf9e 100644 --- a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestConversionOfSourceObject_a70c086a74142c82.verified.txt +++ b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestConversionOfSourceObject_a70c086a74142c82.verified.txt @@ -18,7 +18,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps + Provider: AppService } } }, diff --git a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestConversionOfSourceObject_c26902b0e44f97cd.verified.txt b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestConversionOfSourceObject_c26902b0e44f97cd.verified.txt index 2d00804545..5339d06351 100644 --- a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestConversionOfSourceObject_c26902b0e44f97cd.verified.txt +++ b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestConversionOfSourceObject_c26902b0e44f97cd.verified.txt @@ -18,7 +18,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps + Provider: AppService } } }, diff --git a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateEntityByAddingNewRelationship.verified.txt b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateEntityByAddingNewRelationship.verified.txt index 2789572051..ef0e2b9151 100644 --- a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateEntityByAddingNewRelationship.verified.txt +++ b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateEntityByAddingNewRelationship.verified.txt @@ -18,7 +18,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps, + Provider: AppService, Jwt: { Audience: , Issuer: diff --git a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateEntityByModifyingRelationship.verified.txt b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateEntityByModifyingRelationship.verified.txt index 092dec7745..44828bd61a 100644 --- a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateEntityByModifyingRelationship.verified.txt +++ b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateEntityByModifyingRelationship.verified.txt @@ -18,7 +18,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps, + Provider: AppService, Jwt: { Audience: , Issuer: diff --git a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateEntityCaching.verified.txt b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateEntityCaching.verified.txt index 9ae0e33948..a3df8fd6c4 100644 --- a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateEntityCaching.verified.txt +++ b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateEntityCaching.verified.txt @@ -18,7 +18,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps, + Provider: AppService, Jwt: { Audience: , Issuer: diff --git a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateEntityPermission.verified.txt b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateEntityPermission.verified.txt index 1a77d65bcd..477f15db4a 100644 --- a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateEntityPermission.verified.txt +++ b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateEntityPermission.verified.txt @@ -18,7 +18,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps, + Provider: AppService, Jwt: { Audience: , Issuer: diff --git a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateEntityPermissionByAddingNewRole.verified.txt b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateEntityPermissionByAddingNewRole.verified.txt index 6775db8dde..63c4e24898 100644 --- a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateEntityPermissionByAddingNewRole.verified.txt +++ b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateEntityPermissionByAddingNewRole.verified.txt @@ -18,7 +18,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps, + Provider: AppService, Jwt: { Audience: , Issuer: diff --git a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateEntityPermissionHavingWildcardAction.verified.txt b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateEntityPermissionHavingWildcardAction.verified.txt index 64517082ad..f3db383d0f 100644 --- a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateEntityPermissionHavingWildcardAction.verified.txt +++ b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateEntityPermissionHavingWildcardAction.verified.txt @@ -18,7 +18,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps, + Provider: AppService, Jwt: { Audience: , Issuer: diff --git a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateEntityPermissionWithExistingAction.verified.txt b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateEntityPermissionWithExistingAction.verified.txt index 52dc99399e..c0818f3c84 100644 --- a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateEntityPermissionWithExistingAction.verified.txt +++ b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateEntityPermissionWithExistingAction.verified.txt @@ -18,7 +18,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps, + Provider: AppService, Jwt: { Audience: , Issuer: diff --git a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateEntityPermissionWithWildcardAction.verified.txt b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateEntityPermissionWithWildcardAction.verified.txt index 92b5917b92..19444d8105 100644 --- a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateEntityPermissionWithWildcardAction.verified.txt +++ b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateEntityPermissionWithWildcardAction.verified.txt @@ -18,7 +18,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps, + Provider: AppService, Jwt: { Audience: , Issuer: diff --git a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateEntityWithMappings.verified.txt b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateEntityWithMappings.verified.txt index 54d9077f1c..e6f2bec0c9 100644 --- a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateEntityWithMappings.verified.txt +++ b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateEntityWithMappings.verified.txt @@ -18,7 +18,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps, + Provider: AppService, Jwt: { Audience: , Issuer: diff --git a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateEntityWithPolicyAndFieldProperties_088d6237033e0a7c.verified.txt b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateEntityWithPolicyAndFieldProperties_088d6237033e0a7c.verified.txt index 7c701037fd..10324a7f50 100644 --- a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateEntityWithPolicyAndFieldProperties_088d6237033e0a7c.verified.txt +++ b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateEntityWithPolicyAndFieldProperties_088d6237033e0a7c.verified.txt @@ -18,7 +18,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps + Provider: AppService } } }, diff --git a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateEntityWithPolicyAndFieldProperties_3ea32fdef7aed1b4.verified.txt b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateEntityWithPolicyAndFieldProperties_3ea32fdef7aed1b4.verified.txt index c318861497..d21d3b195d 100644 --- a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateEntityWithPolicyAndFieldProperties_3ea32fdef7aed1b4.verified.txt +++ b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateEntityWithPolicyAndFieldProperties_3ea32fdef7aed1b4.verified.txt @@ -18,7 +18,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps + Provider: AppService } } }, diff --git a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateEntityWithPolicyAndFieldProperties_4d25c2c012107597.verified.txt b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateEntityWithPolicyAndFieldProperties_4d25c2c012107597.verified.txt index cdf30a00c9..f44cb7a9b4 100644 --- a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateEntityWithPolicyAndFieldProperties_4d25c2c012107597.verified.txt +++ b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateEntityWithPolicyAndFieldProperties_4d25c2c012107597.verified.txt @@ -18,7 +18,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps + Provider: AppService } } }, diff --git a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateEntityWithSpecialCharacterInMappings.verified.txt b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateEntityWithSpecialCharacterInMappings.verified.txt index 1906f87425..396d09edf3 100644 --- a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateEntityWithSpecialCharacterInMappings.verified.txt +++ b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateEntityWithSpecialCharacterInMappings.verified.txt @@ -18,7 +18,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps, + Provider: AppService, Jwt: { Audience: , Issuer: diff --git a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateExistingMappings.verified.txt b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateExistingMappings.verified.txt index 56ce5b55c3..2884cf540b 100644 --- a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateExistingMappings.verified.txt +++ b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateExistingMappings.verified.txt @@ -18,7 +18,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps, + Provider: AppService, Jwt: { Audience: , Issuer: diff --git a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdatePolicy.verified.txt b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdatePolicy.verified.txt index c8002a2fbc..57c5f18284 100644 --- a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdatePolicy.verified.txt +++ b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdatePolicy.verified.txt @@ -18,7 +18,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps + Provider: AppService } } }, diff --git a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateRestAndGraphQLSettingsForStoredProcedures_10ea92e3b25ab0c9.verified.txt b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateRestAndGraphQLSettingsForStoredProcedures_10ea92e3b25ab0c9.verified.txt index b87e804456..11d8adb815 100644 --- a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateRestAndGraphQLSettingsForStoredProcedures_10ea92e3b25ab0c9.verified.txt +++ b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateRestAndGraphQLSettingsForStoredProcedures_10ea92e3b25ab0c9.verified.txt @@ -18,7 +18,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps + Provider: AppService } } }, diff --git a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateRestAndGraphQLSettingsForStoredProcedures_127bb81593f835fe.verified.txt b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateRestAndGraphQLSettingsForStoredProcedures_127bb81593f835fe.verified.txt index 1cd138d10f..e3a6363fb1 100644 --- a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateRestAndGraphQLSettingsForStoredProcedures_127bb81593f835fe.verified.txt +++ b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateRestAndGraphQLSettingsForStoredProcedures_127bb81593f835fe.verified.txt @@ -18,7 +18,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps + Provider: AppService } } }, diff --git a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateRestAndGraphQLSettingsForStoredProcedures_386efa1a113fac6b.verified.txt b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateRestAndGraphQLSettingsForStoredProcedures_386efa1a113fac6b.verified.txt index c7cb996d03..9729cf4aae 100644 --- a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateRestAndGraphQLSettingsForStoredProcedures_386efa1a113fac6b.verified.txt +++ b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateRestAndGraphQLSettingsForStoredProcedures_386efa1a113fac6b.verified.txt @@ -18,7 +18,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps + Provider: AppService } } }, diff --git a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateRestAndGraphQLSettingsForStoredProcedures_53db4712d83be8e6.verified.txt b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateRestAndGraphQLSettingsForStoredProcedures_53db4712d83be8e6.verified.txt index 20d7bcd624..6ba80ba348 100644 --- a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateRestAndGraphQLSettingsForStoredProcedures_53db4712d83be8e6.verified.txt +++ b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateRestAndGraphQLSettingsForStoredProcedures_53db4712d83be8e6.verified.txt @@ -18,7 +18,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps + Provider: AppService } } }, diff --git a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateRestAndGraphQLSettingsForStoredProcedures_5e9ddd8c7c740efd.verified.txt b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateRestAndGraphQLSettingsForStoredProcedures_5e9ddd8c7c740efd.verified.txt index 13beb6bc7c..99d78eaa0e 100644 --- a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateRestAndGraphQLSettingsForStoredProcedures_5e9ddd8c7c740efd.verified.txt +++ b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateRestAndGraphQLSettingsForStoredProcedures_5e9ddd8c7c740efd.verified.txt @@ -18,7 +18,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps + Provider: AppService } } }, diff --git a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateRestAndGraphQLSettingsForStoredProcedures_6c5b3bfc72e5878a.verified.txt b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateRestAndGraphQLSettingsForStoredProcedures_6c5b3bfc72e5878a.verified.txt index c7cb996d03..9729cf4aae 100644 --- a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateRestAndGraphQLSettingsForStoredProcedures_6c5b3bfc72e5878a.verified.txt +++ b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateRestAndGraphQLSettingsForStoredProcedures_6c5b3bfc72e5878a.verified.txt @@ -18,7 +18,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps + Provider: AppService } } }, diff --git a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateRestAndGraphQLSettingsForStoredProcedures_8398059a743d7027.verified.txt b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateRestAndGraphQLSettingsForStoredProcedures_8398059a743d7027.verified.txt index c7cb996d03..9729cf4aae 100644 --- a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateRestAndGraphQLSettingsForStoredProcedures_8398059a743d7027.verified.txt +++ b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateRestAndGraphQLSettingsForStoredProcedures_8398059a743d7027.verified.txt @@ -18,7 +18,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps + Provider: AppService } } }, diff --git a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateRestAndGraphQLSettingsForStoredProcedures_a49380ce6d1fd8ba.verified.txt b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateRestAndGraphQLSettingsForStoredProcedures_a49380ce6d1fd8ba.verified.txt index bdd7c8f7a0..ec4549f38a 100644 --- a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateRestAndGraphQLSettingsForStoredProcedures_a49380ce6d1fd8ba.verified.txt +++ b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateRestAndGraphQLSettingsForStoredProcedures_a49380ce6d1fd8ba.verified.txt @@ -18,7 +18,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps + Provider: AppService } } }, diff --git a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateRestAndGraphQLSettingsForStoredProcedures_c9b12fe27be53878.verified.txt b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateRestAndGraphQLSettingsForStoredProcedures_c9b12fe27be53878.verified.txt index e8fc427339..92cb5caac0 100644 --- a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateRestAndGraphQLSettingsForStoredProcedures_c9b12fe27be53878.verified.txt +++ b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateRestAndGraphQLSettingsForStoredProcedures_c9b12fe27be53878.verified.txt @@ -18,7 +18,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps + Provider: AppService } } }, diff --git a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateRestAndGraphQLSettingsForStoredProcedures_d19603117eb8b51b.verified.txt b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateRestAndGraphQLSettingsForStoredProcedures_d19603117eb8b51b.verified.txt index 13beb6bc7c..99d78eaa0e 100644 --- a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateRestAndGraphQLSettingsForStoredProcedures_d19603117eb8b51b.verified.txt +++ b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateRestAndGraphQLSettingsForStoredProcedures_d19603117eb8b51b.verified.txt @@ -18,7 +18,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps + Provider: AppService } } }, diff --git a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateRestAndGraphQLSettingsForStoredProcedures_d770d682c5802737.verified.txt b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateRestAndGraphQLSettingsForStoredProcedures_d770d682c5802737.verified.txt index b87e804456..11d8adb815 100644 --- a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateRestAndGraphQLSettingsForStoredProcedures_d770d682c5802737.verified.txt +++ b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateRestAndGraphQLSettingsForStoredProcedures_d770d682c5802737.verified.txt @@ -18,7 +18,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps + Provider: AppService } } }, diff --git a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateRestAndGraphQLSettingsForStoredProcedures_ef8cc721c9dfc7e4.verified.txt b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateRestAndGraphQLSettingsForStoredProcedures_ef8cc721c9dfc7e4.verified.txt index 87b7a7697c..9b7d9f96e9 100644 --- a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateRestAndGraphQLSettingsForStoredProcedures_ef8cc721c9dfc7e4.verified.txt +++ b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateRestAndGraphQLSettingsForStoredProcedures_ef8cc721c9dfc7e4.verified.txt @@ -18,7 +18,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps + Provider: AppService } } }, diff --git a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateRestAndGraphQLSettingsForStoredProcedures_f3897e2254996db0.verified.txt b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateRestAndGraphQLSettingsForStoredProcedures_f3897e2254996db0.verified.txt index 18c5f966c5..5c6a0b7d6b 100644 --- a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateRestAndGraphQLSettingsForStoredProcedures_f3897e2254996db0.verified.txt +++ b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateRestAndGraphQLSettingsForStoredProcedures_f3897e2254996db0.verified.txt @@ -18,7 +18,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps + Provider: AppService } } }, diff --git a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateRestAndGraphQLSettingsForStoredProcedures_f4cadb897fc5b0fe.verified.txt b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateRestAndGraphQLSettingsForStoredProcedures_f4cadb897fc5b0fe.verified.txt index 612a65c14a..aa074116d0 100644 --- a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateRestAndGraphQLSettingsForStoredProcedures_f4cadb897fc5b0fe.verified.txt +++ b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateRestAndGraphQLSettingsForStoredProcedures_f4cadb897fc5b0fe.verified.txt @@ -18,7 +18,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps + Provider: AppService } } }, diff --git a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateRestAndGraphQLSettingsForStoredProcedures_f59b2a65fc1e18a3.verified.txt b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateRestAndGraphQLSettingsForStoredProcedures_f59b2a65fc1e18a3.verified.txt index 1cd138d10f..e3a6363fb1 100644 --- a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateRestAndGraphQLSettingsForStoredProcedures_f59b2a65fc1e18a3.verified.txt +++ b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateRestAndGraphQLSettingsForStoredProcedures_f59b2a65fc1e18a3.verified.txt @@ -18,7 +18,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps + Provider: AppService } } }, diff --git a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateSourceStringToDatabaseSourceObject_574e1995f787740f.verified.txt b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateSourceStringToDatabaseSourceObject_574e1995f787740f.verified.txt index 260eecd0c9..003bc17a3e 100644 --- a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateSourceStringToDatabaseSourceObject_574e1995f787740f.verified.txt +++ b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateSourceStringToDatabaseSourceObject_574e1995f787740f.verified.txt @@ -18,7 +18,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps + Provider: AppService } } }, diff --git a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateSourceStringToDatabaseSourceObject_a13a9ca73b21f261.verified.txt b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateSourceStringToDatabaseSourceObject_a13a9ca73b21f261.verified.txt index 80f61e17ac..29d477944f 100644 --- a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateSourceStringToDatabaseSourceObject_a13a9ca73b21f261.verified.txt +++ b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateSourceStringToDatabaseSourceObject_a13a9ca73b21f261.verified.txt @@ -18,7 +18,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps + Provider: AppService } } }, diff --git a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateSourceStringToDatabaseSourceObject_a5ce76c8bea25cc8.verified.txt b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateSourceStringToDatabaseSourceObject_a5ce76c8bea25cc8.verified.txt index 80f61e17ac..29d477944f 100644 --- a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateSourceStringToDatabaseSourceObject_a5ce76c8bea25cc8.verified.txt +++ b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateSourceStringToDatabaseSourceObject_a5ce76c8bea25cc8.verified.txt @@ -18,7 +18,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps + Provider: AppService } } }, diff --git a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateSourceStringToDatabaseSourceObject_bba111332a1f973f.verified.txt b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateSourceStringToDatabaseSourceObject_bba111332a1f973f.verified.txt index ba28cd7509..f5dd22534c 100644 --- a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateSourceStringToDatabaseSourceObject_bba111332a1f973f.verified.txt +++ b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateSourceStringToDatabaseSourceObject_bba111332a1f973f.verified.txt @@ -18,7 +18,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps + Provider: AppService } } }, diff --git a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateStoredProcedureWithBothMcpProperties.verified.txt b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateStoredProcedureWithBothMcpProperties.verified.txt new file mode 100644 index 0000000000..a2aa5c0f40 --- /dev/null +++ b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateStoredProcedureWithBothMcpProperties.verified.txt @@ -0,0 +1,64 @@ +{ + DataSource: { + DatabaseType: MSSQL + }, + Runtime: { + Rest: { + Enabled: true, + Path: /, + RequestBodyStrict: true + }, + GraphQL: { + Enabled: true, + Path: /graphql, + AllowIntrospection: true + }, + Host: { + Cors: { + AllowCredentials: false + }, + Authentication: { + Provider: AppService, + Jwt: { + Audience: , + Issuer: + } + } + } + }, + Entities: [ + { + UpdateBook: { + Source: { + Type: stored-procedure + }, + GraphQL: { + Singular: UpdateBook, + Plural: UpdateBooks, + Enabled: true, + Operation: Mutation + }, + Rest: { + Methods: [ + Post + ], + Enabled: true + }, + Permissions: [ + { + Role: anonymous, + Actions: [ + { + Action: Execute + } + ] + } + ], + Mcp: { + CustomToolEnabled: true, + DmlToolEnabled: false + } + } + } + ] +} \ No newline at end of file diff --git a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateStoredProcedureWithBothMcpPropertiesEnabled.verified.txt b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateStoredProcedureWithBothMcpPropertiesEnabled.verified.txt new file mode 100644 index 0000000000..6526bd5e38 --- /dev/null +++ b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateStoredProcedureWithBothMcpPropertiesEnabled.verified.txt @@ -0,0 +1,64 @@ +{ + DataSource: { + DatabaseType: MSSQL + }, + Runtime: { + Rest: { + Enabled: true, + Path: /, + RequestBodyStrict: true + }, + GraphQL: { + Enabled: true, + Path: /graphql, + AllowIntrospection: true + }, + Host: { + Cors: { + AllowCredentials: false + }, + Authentication: { + Provider: AppService, + Jwt: { + Audience: , + Issuer: + } + } + } + }, + Entities: [ + { + GetAllBooks: { + Source: { + Type: stored-procedure + }, + GraphQL: { + Singular: GetAllBooks, + Plural: GetAllBooks, + Enabled: true, + Operation: Mutation + }, + Rest: { + Methods: [ + Post + ], + Enabled: true + }, + Permissions: [ + { + Role: anonymous, + Actions: [ + { + Action: Execute + } + ] + } + ], + Mcp: { + CustomToolEnabled: true, + DmlToolEnabled: true + } + } + } + ] +} \ No newline at end of file diff --git a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateStoredProcedureWithMcpCustomToolEnabled.verified.txt b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateStoredProcedureWithMcpCustomToolEnabled.verified.txt new file mode 100644 index 0000000000..b74749dcf6 --- /dev/null +++ b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateStoredProcedureWithMcpCustomToolEnabled.verified.txt @@ -0,0 +1,64 @@ +{ + DataSource: { + DatabaseType: MSSQL + }, + Runtime: { + Rest: { + Enabled: true, + Path: /, + RequestBodyStrict: true + }, + GraphQL: { + Enabled: true, + Path: /graphql, + AllowIntrospection: true + }, + Host: { + Cors: { + AllowCredentials: false + }, + Authentication: { + Provider: AppService, + Jwt: { + Audience: , + Issuer: + } + } + } + }, + Entities: [ + { + GetBookById: { + Source: { + Type: stored-procedure + }, + GraphQL: { + Singular: GetBookById, + Plural: GetBookByIds, + Enabled: true, + Operation: Mutation + }, + Rest: { + Methods: [ + Post + ], + Enabled: true + }, + Permissions: [ + { + Role: anonymous, + Actions: [ + { + Action: Execute + } + ] + } + ], + Mcp: { + CustomToolEnabled: true, + DmlToolEnabled: true + } + } + } + ] +} \ No newline at end of file diff --git a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateTableEntityWithMcpDmlTools_newMcpDmlTools=false.verified.txt b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateTableEntityWithMcpDmlTools_newMcpDmlTools=false.verified.txt new file mode 100644 index 0000000000..9efb911f99 --- /dev/null +++ b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateTableEntityWithMcpDmlTools_newMcpDmlTools=false.verified.txt @@ -0,0 +1,61 @@ +{ + DataSource: { + DatabaseType: MSSQL + }, + Runtime: { + Rest: { + Enabled: true, + Path: /, + RequestBodyStrict: true + }, + GraphQL: { + Enabled: true, + Path: /graphql, + AllowIntrospection: true + }, + Host: { + Cors: { + AllowCredentials: false + }, + Authentication: { + Provider: AppService, + Jwt: { + Audience: , + Issuer: + } + } + } + }, + Entities: [ + { + MyEntity: { + Source: { + Object: MyTable, + Type: Table + }, + GraphQL: { + Singular: MyEntity, + Plural: MyEntities, + Enabled: true + }, + Rest: { + Enabled: true + }, + Permissions: [ + { + Role: anonymous, + Actions: [ + { + Action: * + } + ] + } + ], + Mcp: { + CustomToolEnabled: false, + DmlToolEnabled: false + } + } + } + ] +} \ No newline at end of file diff --git a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateTableEntityWithMcpDmlTools_newMcpDmlTools=true.verified.txt b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateTableEntityWithMcpDmlTools_newMcpDmlTools=true.verified.txt new file mode 100644 index 0000000000..8ffa3b4893 --- /dev/null +++ b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateTableEntityWithMcpDmlTools_newMcpDmlTools=true.verified.txt @@ -0,0 +1,61 @@ +{ + DataSource: { + DatabaseType: MSSQL + }, + Runtime: { + Rest: { + Enabled: true, + Path: /, + RequestBodyStrict: true + }, + GraphQL: { + Enabled: true, + Path: /graphql, + AllowIntrospection: true + }, + Host: { + Cors: { + AllowCredentials: false + }, + Authentication: { + Provider: AppService, + Jwt: { + Audience: , + Issuer: + } + } + } + }, + Entities: [ + { + MyEntity: { + Source: { + Object: MyTable, + Type: Table + }, + GraphQL: { + Singular: MyEntity, + Plural: MyEntities, + Enabled: true + }, + Rest: { + Enabled: true + }, + Permissions: [ + { + Role: anonymous, + Actions: [ + { + Action: * + } + ] + } + ], + Mcp: { + CustomToolEnabled: false, + DmlToolEnabled: true + } + } + } + ] +} \ No newline at end of file diff --git a/src/Cli.Tests/Snapshots/UpdateEntityTests.UpdateDatabaseSourceKeyFields.verified.txt b/src/Cli.Tests/Snapshots/UpdateEntityTests.UpdateDatabaseSourceKeyFields.verified.txt index 544a3484f9..73703220d8 100644 --- a/src/Cli.Tests/Snapshots/UpdateEntityTests.UpdateDatabaseSourceKeyFields.verified.txt +++ b/src/Cli.Tests/Snapshots/UpdateEntityTests.UpdateDatabaseSourceKeyFields.verified.txt @@ -18,7 +18,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps + Provider: AppService } } }, diff --git a/src/Cli.Tests/Snapshots/UpdateEntityTests.UpdateDatabaseSourceName.verified.txt b/src/Cli.Tests/Snapshots/UpdateEntityTests.UpdateDatabaseSourceName.verified.txt index 1719e1ade2..50c0a1786c 100644 --- a/src/Cli.Tests/Snapshots/UpdateEntityTests.UpdateDatabaseSourceName.verified.txt +++ b/src/Cli.Tests/Snapshots/UpdateEntityTests.UpdateDatabaseSourceName.verified.txt @@ -18,7 +18,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps + Provider: AppService } } }, diff --git a/src/Cli.Tests/Snapshots/UpdateEntityTests.UpdateDatabaseSourceParameters.verified.txt b/src/Cli.Tests/Snapshots/UpdateEntityTests.UpdateDatabaseSourceParameters.verified.txt index 0cbdc4347f..a34882cefe 100644 --- a/src/Cli.Tests/Snapshots/UpdateEntityTests.UpdateDatabaseSourceParameters.verified.txt +++ b/src/Cli.Tests/Snapshots/UpdateEntityTests.UpdateDatabaseSourceParameters.verified.txt @@ -18,7 +18,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps + Provider: AppService } } }, diff --git a/src/Cli.Tests/TestHelper.cs b/src/Cli.Tests/TestHelper.cs index a5a0079797..8224a079d4 100644 --- a/src/Cli.Tests/TestHelper.cs +++ b/src/Cli.Tests/TestHelper.cs @@ -143,7 +143,7 @@ public static Process ExecuteDabCommand(string command, string flags) ""allow-credentials"": false }, ""authentication"": { - ""provider"": ""StaticWebApps"" + ""provider"": ""AppService"" } } }, @@ -170,7 +170,7 @@ public static Process ExecuteDabCommand(string command, string flags) ""allow-credentials"": false }, ""authentication"": { - ""provider"": ""StaticWebApps"" + ""provider"": ""AppService"" } } }"; @@ -228,7 +228,7 @@ public static Process ExecuteDabCommand(string command, string flags) ""allow-credentials"": false }, ""authentication"": { - ""provider"": ""StaticWebApps"" + ""provider"": ""AppService"" } } }, @@ -1003,7 +1003,7 @@ public static Process ExecuteDabCommand(string command, string flags) ""allow-credentials"": false }, ""authentication"": { - ""provider"": ""StaticWebApps"" + ""provider"": ""AppService"" } } }, @@ -1048,7 +1048,7 @@ public static Process ExecuteDabCommand(string command, string flags) ""allow-credentials"": false }, ""authentication"": { - ""provider"": ""StaticWebApps"" + ""provider"": ""AppService"" } } }, @@ -1118,7 +1118,7 @@ public static Process ExecuteDabCommand(string command, string flags) ""allow-credentials"": false }, ""authentication"": { - ""provider"": ""StaticWebApps"" + ""provider"": ""AppService"" } } }, @@ -1199,7 +1199,7 @@ public static Process ExecuteDabCommand(string command, string flags) ""allow-credentials"": false }, ""authentication"": { - ""provider"": ""StaticWebApps"" + ""provider"": ""AppService"" } } }, @@ -1298,7 +1298,7 @@ public static string GenerateConfigWithGivenDepthLimit(string? depthLimitJson = ""allow-credentials"": false }}, ""authentication"": {{ - ""provider"": ""StaticWebApps"" + ""provider"": ""AppService"" }} }} }}, @@ -1323,7 +1323,7 @@ public static InitOptions CreateBasicInitOptionsForMsSqlWithConfig(string? confi setSessionContext: true, hostMode: HostMode.Development, corsOrigin: new List(), - authenticationProvider: EasyAuthType.StaticWebApps.ToString(), + authenticationProvider: EasyAuthType.AppService.ToString(), restRequestBodyStrict: CliBool.True, config: config); } diff --git a/src/Cli.Tests/UpdateEntityTests.cs b/src/Cli.Tests/UpdateEntityTests.cs index 3a106c0adc..b05392aa81 100644 --- a/src/Cli.Tests/UpdateEntityTests.cs +++ b/src/Cli.Tests/UpdateEntityTests.cs @@ -1125,7 +1125,7 @@ private static string GetInitialConfigString() ""allow-credentials"": false }, ""authentication"": { - ""provider"": ""StaticWebApps"", + ""provider"": ""AppService"", ""jwt"": { ""audience"": """", ""issuer"": """" diff --git a/src/Cli/Commands/ConfigureOptions.cs b/src/Cli/Commands/ConfigureOptions.cs index 60cb12c3f8..11dca2a4eb 100644 --- a/src/Cli/Commands/ConfigureOptions.cs +++ b/src/Cli/Commands/ConfigureOptions.cs @@ -216,7 +216,7 @@ public ConfigureOptions( [Option("runtime.host.cors.allow-credentials", Required = false, HelpText = "Set value for Access-Control-Allow-Credentials header in Host.Cors. Default: false (boolean).")] public bool? RuntimeHostCorsAllowCredentials { get; } - [Option("runtime.host.authentication.provider", Required = false, HelpText = "Configure the name of authentication provider. Default: StaticWebApps.")] + [Option("runtime.host.authentication.provider", Required = false, HelpText = "Configure the name of authentication provider. Default: AppService.")] public string? RuntimeHostAuthenticationProvider { get; } [Option("runtime.host.authentication.jwt.audience", Required = false, HelpText = "Configure the intended recipient(s) of the Jwt Token.")] diff --git a/src/Cli/Commands/InitOptions.cs b/src/Cli/Commands/InitOptions.cs index 91786d99ff..e01ad1b774 100644 --- a/src/Cli/Commands/InitOptions.cs +++ b/src/Cli/Commands/InitOptions.cs @@ -94,7 +94,7 @@ public InitOptions( [Option("cors-origin", Separator = ',', Required = false, HelpText = "Specify the list of allowed origins.")] public IEnumerable? CorsOrigin { get; } - [Option("auth.provider", Default = "StaticWebApps", Required = false, HelpText = "Specify the Identity Provider.")] + [Option("auth.provider", Default = "AppService", Required = false, HelpText = "Specify the Identity Provider.")] public string AuthenticationProvider { get; } [Option("auth.audience", Required = false, HelpText = "Identifies the recipients that the JWT is intended for.")] diff --git a/src/Cli/Commands/StartOptions.cs b/src/Cli/Commands/StartOptions.cs index c335c6bcc5..050f410801 100644 --- a/src/Cli/Commands/StartOptions.cs +++ b/src/Cli/Commands/StartOptions.cs @@ -19,12 +19,14 @@ public class StartOptions : Options { private const string LOGLEVEL_HELPTEXT = "Specifies logging level as provided value. For possible values, see: https://go.microsoft.com/fwlink/?linkid=2263106"; - public StartOptions(bool verbose, LogLevel? logLevel, bool isHttpsRedirectionDisabled, string config) + public StartOptions(bool verbose, LogLevel? logLevel, bool isHttpsRedirectionDisabled, bool mcpStdio, string? mcpRole, string config) : base(config) { // When verbose is true we set LogLevel to information. LogLevel = verbose is true ? Microsoft.Extensions.Logging.LogLevel.Information : logLevel; IsHttpsRedirectionDisabled = isHttpsRedirectionDisabled; + McpStdio = mcpStdio; + McpRole = mcpRole; } // SetName defines mutually exclusive sets, ie: can not have @@ -38,6 +40,12 @@ public StartOptions(bool verbose, LogLevel? logLevel, bool isHttpsRedirectionDis [Option("no-https-redirect", Required = false, HelpText = "Disables automatic https redirects.")] public bool IsHttpsRedirectionDisabled { get; } + [Option("mcp-stdio", Required = false, HelpText = "Run Data API Builder in MCP stdio mode while starting the engine.")] + public bool McpStdio { get; } + + [Value(0, MetaName = "role", Required = false, HelpText = "Optional MCP permissions role, e.g. role:anonymous. If omitted, defaults to anonymous.")] + public string? McpRole { get; } + public int Handler(ILogger logger, FileSystemRuntimeConfigLoader loader, IFileSystem fileSystem) { logger.LogInformation("{productName} {version}", PRODUCT_NAME, ProductInfo.GetProductVersion()); @@ -45,7 +53,8 @@ public int Handler(ILogger logger, FileSystemRuntimeConfigLoader loader, IFileSy if (!isSuccess) { - logger.LogError("Failed to start the engine."); + logger.LogError("Failed to start the engine{mode}.", + McpStdio ? " in MCP stdio mode" : string.Empty); } return isSuccess ? CliReturnCode.SUCCESS : CliReturnCode.GENERAL_ERROR; diff --git a/src/Cli/ConfigGenerator.cs b/src/Cli/ConfigGenerator.cs index 7c35335089..07dad2af8b 100644 --- a/src/Cli/ConfigGenerator.cs +++ b/src/Cli/ConfigGenerator.cs @@ -1260,7 +1260,8 @@ private static bool TryUpdateConfiguredHostValues( string? updatedProviderValue = options?.RuntimeHostAuthenticationProvider; if (updatedProviderValue != null) { - updatedValue = updatedProviderValue?.ToString() ?? nameof(EasyAuthType.StaticWebApps); + // Default to AppService when provider string is not provided + updatedValue = updatedProviderValue?.ToString() ?? nameof(EasyAuthType.AppService); AuthenticationOptions AuthOptions; if (updatedHostOptions?.Authentication == null) { @@ -2359,6 +2360,17 @@ public static bool TryStartEngineWithOptions(StartOptions options, FileSystemRun args.Add(Startup.NO_HTTPS_REDIRECT_FLAG); } + // If MCP stdio was requested, append the stdio-specific switches. + if (options.McpStdio) + { + string effectiveRole = string.IsNullOrWhiteSpace(options.McpRole) + ? "anonymous" + : options.McpRole; + + args.Add("--mcp-stdio"); + args.Add(effectiveRole); + } + return Azure.DataApiBuilder.Service.Program.StartEngine(args.ToArray()); } diff --git a/src/Cli/Exporter.cs b/src/Cli/Exporter.cs index 896b485692..e694317cd4 100644 --- a/src/Cli/Exporter.cs +++ b/src/Cli/Exporter.cs @@ -110,7 +110,13 @@ private static async Task ExportGraphQL( } else { - StartOptions startOptions = new(false, LogLevel.None, false, options.Config!); + StartOptions startOptions = new( + verbose: false, + logLevel: LogLevel.None, + isHttpsRedirectionDisabled: false, + config: options.Config!, + mcpStdio: false, + mcpRole: null); Task dabService = Task.Run(() => { diff --git a/src/Config/ObjectModel/AuthenticationOptions.cs b/src/Config/ObjectModel/AuthenticationOptions.cs index 6750d6e807..a937168493 100644 --- a/src/Config/ObjectModel/AuthenticationOptions.cs +++ b/src/Config/ObjectModel/AuthenticationOptions.cs @@ -6,12 +6,12 @@ namespace Azure.DataApiBuilder.Config.ObjectModel; /// /// Authentication configuration. /// -/// Identity Provider. Default is StaticWebApps. +/// Identity Provider. Default is AppService. /// With EasyAuth and Simulator, no Audience or Issuer are expected. /// /// Settings enabling validation of the received JWT token. /// Required only when Provider is other than EasyAuth. -public record AuthenticationOptions(string Provider = nameof(EasyAuthType.StaticWebApps), JwtOptions? Jwt = null) +public record AuthenticationOptions(string Provider = nameof(EasyAuthType.AppService), JwtOptions? Jwt = null) { public const string SIMULATOR_AUTHENTICATION = "Simulator"; public const string CLIENT_PRINCIPAL_HEADER = "X-MS-CLIENT-PRINCIPAL"; diff --git a/src/Config/ObjectModel/RuntimeConfig.cs b/src/Config/ObjectModel/RuntimeConfig.cs index 6896d82161..f1ab0d1f10 100644 --- a/src/Config/ObjectModel/RuntimeConfig.cs +++ b/src/Config/ObjectModel/RuntimeConfig.cs @@ -98,6 +98,17 @@ Runtime.Host is null || Runtime.Host.Authentication is null || EasyAuthType.StaticWebApps.ToString().Equals(Runtime.Host.Authentication.Provider, StringComparison.OrdinalIgnoreCase); + /// + /// A shorthand method to determine whether App Service is configured for the current authentication provider. + /// + /// True if the authentication provider is enabled for App Service, otherwise false. + [JsonIgnore] + public bool IsAppServiceIdentityProvider => + Runtime is null || + Runtime.Host is null || + Runtime.Host.Authentication is null || + EasyAuthType.AppService.ToString().Equals(Runtime.Host.Authentication.Provider, StringComparison.OrdinalIgnoreCase); + /// /// The path at which Rest APIs are available /// diff --git a/src/Core/AuthenticationHelpers/ClientRoleHeaderAuthenticationMiddleware.cs b/src/Core/AuthenticationHelpers/ClientRoleHeaderAuthenticationMiddleware.cs index a233355a7d..c83de9ed3a 100644 --- a/src/Core/AuthenticationHelpers/ClientRoleHeaderAuthenticationMiddleware.cs +++ b/src/Core/AuthenticationHelpers/ClientRoleHeaderAuthenticationMiddleware.cs @@ -62,7 +62,7 @@ public async Task InvokeAsync(HttpContext httpContext) // Determine the authentication scheme to use based on dab-config.json. // Compatible with both ConfigureAuthentication and ConfigureAuthenticationV2 in startup.cs. // This means that this code is resilient to whether or not the default authentication scheme is set in startup. - string scheme = EasyAuthAuthenticationDefaults.SWAAUTHSCHEME; + string scheme = EasyAuthAuthenticationDefaults.APPSERVICEAUTHSCHEME; if (!_runtimeConfigProvider.IsLateConfigured) { AuthenticationOptions? dabAuthNOptions = _runtimeConfigProvider.GetConfig().Runtime?.Host?.Authentication; diff --git a/src/Core/AuthenticationHelpers/EasyAuthAuthenticationBuilderExtensions.cs b/src/Core/AuthenticationHelpers/EasyAuthAuthenticationBuilderExtensions.cs index cb1d11a149..e4edf72a7c 100644 --- a/src/Core/AuthenticationHelpers/EasyAuthAuthenticationBuilderExtensions.cs +++ b/src/Core/AuthenticationHelpers/EasyAuthAuthenticationBuilderExtensions.cs @@ -54,7 +54,8 @@ public static AuthenticationBuilder AddEasyAuthAuthentication( /// /// Registers the StaticWebApps and AppService EasyAuth authentication schemes. /// Used for ConfigureAuthenticationV2() where all EasyAuth schemes are registered. - /// This function doesn't register EasyAuthType.AppService if the AppService environment is not detected. + /// AppService authentication is always registered, while StaticWebApps authentication + /// is also available and configured here. /// /// public static AuthenticationBuilder AddEnvDetectedEasyAuth(this AuthenticationBuilder builder) @@ -64,6 +65,7 @@ public static AuthenticationBuilder AddEnvDetectedEasyAuth(this AuthenticationBu throw new ArgumentNullException(nameof(builder)); } + // Always register Static Web Apps authentication scheme. builder.AddScheme( authenticationScheme: EasyAuthAuthenticationDefaults.SWAAUTHSCHEME, displayName: EasyAuthAuthenticationDefaults.SWAAUTHSCHEME, @@ -72,20 +74,16 @@ public static AuthenticationBuilder AddEnvDetectedEasyAuth(this AuthenticationBu options.EasyAuthProvider = EasyAuthType.StaticWebApps; }); - bool appServiceEnvironmentDetected = AppServiceAuthenticationInfo.AreExpectedAppServiceEnvVarsPresent(); - - if (appServiceEnvironmentDetected) - { - // Loggers not available at this point in startup. - Console.WriteLine("AppService environment detected, configuring EasyAuth.AppService authentication scheme."); - builder.AddScheme( - authenticationScheme: EasyAuthAuthenticationDefaults.APPSERVICEAUTHSCHEME, - displayName: EasyAuthAuthenticationDefaults.APPSERVICEAUTHSCHEME, - options => - { - options.EasyAuthProvider = EasyAuthType.AppService; - }); - } + // Always register App Service authentication scheme as well so that + // AppService can be treated as the default EasyAuth provider without + // relying on environment variable detection. + builder.AddScheme( + authenticationScheme: EasyAuthAuthenticationDefaults.APPSERVICEAUTHSCHEME, + displayName: EasyAuthAuthenticationDefaults.APPSERVICEAUTHSCHEME, + options => + { + options.EasyAuthProvider = EasyAuthType.AppService; + }); return builder; } diff --git a/src/Core/Configurations/RuntimeConfigValidator.cs b/src/Core/Configurations/RuntimeConfigValidator.cs index fd8f811c9e..ec97a48e4c 100644 --- a/src/Core/Configurations/RuntimeConfigValidator.cs +++ b/src/Core/Configurations/RuntimeConfigValidator.cs @@ -848,6 +848,14 @@ private void ValidateAuthenticationOptions(RuntimeConfig runtimeConfig) return; } + // Warn if the configured EasyAuth provider is StaticWebApps (deprecated) + if (!string.IsNullOrWhiteSpace(runtimeConfig.Runtime.Host.Authentication.Provider) && + Enum.TryParse(runtimeConfig.Runtime.Host.Authentication.Provider, ignoreCase: true, out EasyAuthType provider) && + provider == EasyAuthType.StaticWebApps) + { + _logger.LogWarning("The 'StaticWebApps' authentication provider is deprecated."); + } + bool isAudienceSet = !string.IsNullOrEmpty(runtimeConfig.Runtime.Host.Authentication.Jwt?.Audience); bool isIssuerSet = !string.IsNullOrEmpty(runtimeConfig.Runtime.Host.Authentication.Jwt?.Issuer); diff --git a/src/Core/Resolvers/SqlQueryEngine.cs b/src/Core/Resolvers/SqlQueryEngine.cs index 7b261ecb2b..6523589532 100644 --- a/src/Core/Resolvers/SqlQueryEngine.cs +++ b/src/Core/Resolvers/SqlQueryEngine.cs @@ -224,12 +224,18 @@ public JsonElement ResolveObject(JsonElement element, ObjectField fieldSchema, r parentMetadata = paginationObjectMetadata; } - PaginationMetadata currentMetadata = parentMetadata.Subqueries[fieldSchema.Name]; - metadata = currentMetadata; - - if (currentMetadata.IsPaginated) + // In some scenarios (for example when RBAC removes a relationship + // or when multiple sibling nested entities are present), we may not + // have pagination metadata for the current field. In those cases we + // should simply return the element as-is instead of throwing. + if (parentMetadata.Subqueries.TryGetValue(fieldSchema.Name, out PaginationMetadata? currentMetadata)) { - return SqlPaginationUtil.CreatePaginationConnectionFromJsonElement(element, currentMetadata); + metadata = currentMetadata; + + if (currentMetadata.IsPaginated) + { + return SqlPaginationUtil.CreatePaginationConnectionFromJsonElement(element, currentMetadata); + } } } diff --git a/src/Core/Services/ExecutionHelper.cs b/src/Core/Services/ExecutionHelper.cs index a0a81f02cc..79bdc6af9c 100644 --- a/src/Core/Services/ExecutionHelper.cs +++ b/src/Core/Services/ExecutionHelper.cs @@ -4,6 +4,7 @@ using System.Diagnostics; using System.Globalization; using System.Net; +using System.Text; using System.Text.Json; using Azure.DataApiBuilder.Config.ObjectModel; using Azure.DataApiBuilder.Core.Configurations; @@ -534,7 +535,24 @@ public static InputObjectType InputObjectTypeFromIInputField(IInputValueDefiniti // /books/items/items[idx]/authors -> Depth: 3 (0-indexed) which maps to the // pagination metadata for the "authors/items" subquery. string paginationObjectParentName = GetMetadataKey(context.Path) + "::" + context.Path.Parent.Depth(); - return (IMetadata?)context.ContextData[paginationObjectParentName]; + + // For nested list fields under relationships (e.g. reviews.items, authors.items), + // include the relationship path suffix so we look up the same key that + // SetNewMetadataChildren stored ("::depth::relationshipPath"). + string relationshipPath = GetRelationshipPathSuffix(context.Path.Parent); + if (!string.IsNullOrEmpty(relationshipPath)) + { + paginationObjectParentName = paginationObjectParentName + "::" + relationshipPath; + } + + if (context.ContextData.TryGetValue(key: paginationObjectParentName, out object? itemsPaginationMetadata) && itemsPaginationMetadata is not null) + { + return (IMetadata)itemsPaginationMetadata; + } + + // If metadata is missing (e.g. Cosmos DB or pruned relationship), return an empty + // pagination metadata object to avoid KeyNotFoundException. + return PaginationMetadata.MakeEmptyPaginationMetadata(); } // This section would be reached when processing a Cosmos query of the form: @@ -582,7 +600,24 @@ private static IMetadata GetMetadataObjectField(IResolverContext context) // pagination metadata from context.ContextData // The PaginationMetadata fetched has subquery metadata for "authors" from path "/books/items/authors" string objectParentName = GetMetadataKey(context.Path) + "::" + context.Path.Parent.Parent.Depth(); - return (IMetadata)context.ContextData[objectParentName]!; + + // Include relationship path suffix (for example, "addresses" or "phoneNumbers") so + // we look up the same key that SetNewMetadataChildren stored + // ("::depth::relationshipPath"). + string relationshipPath = GetRelationshipPathSuffix(context.Path.Parent.Parent); + if (!string.IsNullOrEmpty(relationshipPath)) + { + objectParentName = objectParentName + "::" + relationshipPath; + } + + if (context.ContextData.TryGetValue(objectParentName, out object? indexerMetadata) && indexerMetadata is not null) + { + return (IMetadata)indexerMetadata; + } + + // If no metadata is present (for example, for non-paginated relationships or when + // RBAC prunes a branch), return an empty pagination metadata object. + return PaginationMetadata.MakeEmptyPaginationMetadata(); } if (!context.Path.IsRootField() && ((NamePathSegment)context.Path.Parent).Name != PURE_RESOLVER_CONTEXT_SUFFIX) @@ -592,12 +627,35 @@ private static IMetadata GetMetadataObjectField(IResolverContext context) // e.g. metadata for index 4 will not exist. only 3. // Depth: / 0 / 1 / 2 / 3 / 4 // Path: /books/items/items[0]/publishers/books + // + // To handle arbitrary nesting depths with sibling relationships, we need to include + // the relationship field path in the key. For example: + // - /entity/items[0]/rel1/nested uses key ::3::rel1 + // - /entity/items[0]/rel2/nested uses key ::3::rel2 + // - /entity/items[0]/rel1/nested/deeper uses key ::4::rel1::nested + // - /entity/items[0]/rel1/nested2/deeper uses key ::4::rel1::nested2 string objectParentName = GetMetadataKey(context.Path.Parent) + "::" + context.Path.Parent.Depth(); - return (IMetadata)context.ContextData[objectParentName]!; + string relationshipPath = GetRelationshipPathSuffix(context.Path.Parent); + if (!string.IsNullOrEmpty(relationshipPath)) + { + objectParentName = objectParentName + "::" + relationshipPath; + } + + if (context.ContextData.TryGetValue(objectParentName, out object? nestedMetadata) && nestedMetadata is not null) + { + return (IMetadata)nestedMetadata; + } + + return PaginationMetadata.MakeEmptyPaginationMetadata(); } string metadataKey = GetMetadataKey(context.Path) + "::" + context.Path.Depth(); - return (IMetadata)context.ContextData[metadataKey]!; + if (context.ContextData.TryGetValue(metadataKey, out object? rootMetadata) && rootMetadata is not null) + { + return (IMetadata)rootMetadata; + } + + return PaginationMetadata.MakeEmptyPaginationMetadata(); } private static string GetMetadataKey(HotChocolate.Path path) @@ -614,6 +672,50 @@ private static string GetMetadataKey(HotChocolate.Path path) return GetMetadataKey(path: path.Parent); } + /// + /// Builds a suffix representing the relationship path from the IndexerPathSegment (items[n]) + /// up to (but not including) the current path segment. This is used to create unique metadata + /// keys for sibling relationships at any nesting depth. + /// + /// The path to build the suffix for + /// + /// A string like "rel1" for /entity/items[0]/rel1, + /// or "rel1::nested" for /entity/items[0]/rel1/nested, + /// or empty string if no IndexerPathSegment is found in the path ancestry. + /// + private static string GetRelationshipPathSuffix(HotChocolate.Path path) + { + List pathParts = new(); + HotChocolate.Path? current = path; + + // Walk up the path collecting relationship field names until we hit an IndexerPathSegment + while (current is not null && !current.IsRoot) + { + if (current is IndexerPathSegment) + { + // We've reached items[n], stop here + break; + } + + if (current is NamePathSegment nameSegment) + { + pathParts.Add(nameSegment.Name); + } + + current = current.Parent; + } + + // If we didn't find an IndexerPathSegment, return empty (this handles root-level queries) + if (current is not IndexerPathSegment) + { + return string.Empty; + } + + // Reverse because we walked up the tree, but we want the path from root to leaf + pathParts.Reverse(); + return string.Join("::", pathParts); + } + /// /// Resolves the name of the root object of a selection set to /// use as the beginning of a key used to index pagination metadata in the @@ -655,7 +757,25 @@ private static void SetNewMetadataChildren(IResolverContext context, IMetadata? // When context.Path takes the form: "/entity/items[index]/nestedEntity" HC counts the depth as // if the path took the form: "/entity/items/items[index]/nestedEntity" -> Depth of "nestedEntity" // is 3 because depth is 0-indexed. - string contextKey = GetMetadataKey(context.Path) + "::" + context.Path.Depth(); + StringBuilder contextKeyBuilder = new(); + contextKeyBuilder + .Append(GetMetadataKey(context.Path)) + .Append("::") + .Append(context.Path.Depth()); + + // For relationship fields at any depth, include the relationship path suffix to distinguish + // between sibling relationships. This handles arbitrary nesting depths. + // e.g., "/entity/items[0]/rel1" gets key ::3::rel1 + // e.g., "/entity/items[0]/rel1/nested" gets key ::4::rel1::nested + string relationshipPath = GetRelationshipPathSuffix(context.Path); + if (!string.IsNullOrEmpty(relationshipPath)) + { + contextKeyBuilder + .Append("::") + .Append(relationshipPath); + } + + string contextKey = contextKeyBuilder.ToString(); // It's okay to overwrite the context when we are visiting a different item in items e.g. books/items/items[1]/publishers since // context for books/items/items[0]/publishers processing is done and that context isn't needed anymore. diff --git a/src/Service.Tests/Authentication/Helpers/WebHostBuilderHelper.cs b/src/Service.Tests/Authentication/Helpers/WebHostBuilderHelper.cs index 4ccf7945d1..644bd338e3 100644 --- a/src/Service.Tests/Authentication/Helpers/WebHostBuilderHelper.cs +++ b/src/Service.Tests/Authentication/Helpers/WebHostBuilderHelper.cs @@ -75,6 +75,7 @@ public static async Task CreateWebHost( else { EasyAuthType easyAuthProvider = (EasyAuthType)Enum.Parse(typeof(EasyAuthType), provider, ignoreCase: true); + services.AddAuthentication() .AddEasyAuthAuthentication(easyAuthProvider); } diff --git a/src/Service.Tests/Caching/CachingConfigProcessingTests.cs b/src/Service.Tests/Caching/CachingConfigProcessingTests.cs index a6daebf3e4..00729476cd 100644 --- a/src/Service.Tests/Caching/CachingConfigProcessingTests.cs +++ b/src/Service.Tests/Caching/CachingConfigProcessingTests.cs @@ -383,7 +383,7 @@ private static string GetRawConfigJson(string globalCacheConfig, string entityCa ""allow-credentials"": false }, ""authentication"": { - ""provider"": ""StaticWebApps"" + ""provider"": ""AppService"" }, ""mode"": ""production"" }" + globalCacheConfig + diff --git a/src/Service.Tests/Caching/HealthEndpointCachingTests.cs b/src/Service.Tests/Caching/HealthEndpointCachingTests.cs index fcc3e097e5..664e6070e6 100644 --- a/src/Service.Tests/Caching/HealthEndpointCachingTests.cs +++ b/src/Service.Tests/Caching/HealthEndpointCachingTests.cs @@ -148,7 +148,7 @@ private static void CreateCustomConfigFile(Dictionary entityMap, ConfigurationTests.GetConnectionStringFromEnvironmentConfig(environment: TestCategory.MSSQL), Options: null, Health: new(true)); - HostOptions hostOptions = new(Mode: HostMode.Development, Cors: null, Authentication: new() { Provider = nameof(EasyAuthType.StaticWebApps) }); + HostOptions hostOptions = new(Mode: HostMode.Development, Cors: null, Authentication: new() { Provider = nameof(EasyAuthType.AppService) }); RuntimeConfig runtimeConfig = new( Schema: string.Empty, diff --git a/src/Service.Tests/Configuration/AuthenticationConfigValidatorUnitTests.cs b/src/Service.Tests/Configuration/AuthenticationConfigValidatorUnitTests.cs index 963211ae40..846a34ebee 100644 --- a/src/Service.Tests/Configuration/AuthenticationConfigValidatorUnitTests.cs +++ b/src/Service.Tests/Configuration/AuthenticationConfigValidatorUnitTests.cs @@ -38,7 +38,7 @@ public void TestInitialize() public void ValidateEasyAuthConfig() { RuntimeConfig config = - CreateRuntimeConfigWithOptionalAuthN(new AuthenticationOptions(EasyAuthType.StaticWebApps.ToString(), null)); + CreateRuntimeConfigWithOptionalAuthN(new AuthenticationOptions(EasyAuthType.AppService.ToString(), null)); _mockFileSystem.AddFile( FileSystemRuntimeConfigLoader.DEFAULT_CONFIG_FILE_NAME, diff --git a/src/Service.Tests/Configuration/ConfigurationTests.cs b/src/Service.Tests/Configuration/ConfigurationTests.cs index 0614e7688f..bd0ce7d1ff 100644 --- a/src/Service.Tests/Configuration/ConfigurationTests.cs +++ b/src/Service.Tests/Configuration/ConfigurationTests.cs @@ -50,6 +50,7 @@ using Serilog; using VerifyMSTest; using static Azure.DataApiBuilder.Config.FileSystemRuntimeConfigLoader; +using static Azure.DataApiBuilder.Core.AuthenticationHelpers.AppServiceAuthentication; using static Azure.DataApiBuilder.Service.Tests.Configuration.ConfigurationEndpoints; using static Azure.DataApiBuilder.Service.Tests.Configuration.TestConfigFileReader; @@ -393,7 +394,7 @@ public class ConfigurationTests ""allow-credentials"": false }, ""authentication"": { - ""provider"": ""StaticWebApps"" + ""provider"": ""AppService"" }, ""mode"": ""development"" } @@ -656,7 +657,7 @@ type Moon { }, ""host"": { ""authentication"": { - ""provider"": ""StaticWebApps"" + ""provider"": ""AppService"" } } }, @@ -1140,10 +1141,21 @@ public async Task TestSqlSettingPostStartupConfigurations(string configurationEn // Sends a GET request to a protected entity which requires a specific role to access. // Authorization will pass because proper auth headers are present. HttpRequestMessage message = new(method: HttpMethod.Get, requestUri: $"api/{POST_STARTUP_CONFIG_ENTITY}"); - string swaTokenPayload = AuthTestHelper.CreateStaticWebAppsEasyAuthToken( - addAuthenticated: true, - specificRole: POST_STARTUP_CONFIG_ROLE); - message.Headers.Add(Config.ObjectModel.AuthenticationOptions.CLIENT_PRINCIPAL_HEADER, swaTokenPayload); + + // Use an AppService EasyAuth principal carrying the required role when + // authentication is configured to use AppService. + string appServiceTokenPayload = AuthTestHelper.CreateAppServiceEasyAuthToken( + roleClaimType: Config.ObjectModel.AuthenticationOptions.ROLE_CLAIM_TYPE, + additionalClaims: + [ + new AppServiceClaim + { + Typ = Config.ObjectModel.AuthenticationOptions.ROLE_CLAIM_TYPE, + Val = POST_STARTUP_CONFIG_ROLE + } + ]); + + message.Headers.Add(Config.ObjectModel.AuthenticationOptions.CLIENT_PRINCIPAL_HEADER, appServiceTokenPayload); message.Headers.Add(AuthorizationResolver.CLIENT_ROLE_HEADER, POST_STARTUP_CONFIG_ROLE); HttpResponseMessage authorizedResponse = await httpClient.SendAsync(message); Assert.AreEqual(expected: HttpStatusCode.OK, actual: authorizedResponse.StatusCode); @@ -2498,7 +2510,7 @@ public async Task TestRuntimeBaseRouteInNextLinkForPaginatedRestResponse() { const string CUSTOM_CONFIG = "custom-config.json"; string runtimeBaseRoute = "/base-route"; - TestHelper.ConstructNewConfigWithSpecifiedHostMode(CUSTOM_CONFIG, HostMode.Production, TestCategory.MSSQL, runtimeBaseRoute: runtimeBaseRoute); + TestHelper.ConstructNewConfigWithSpecifiedHostMode(CUSTOM_CONFIG, HostMode.Production, TestCategory.MSSQL, runtimeBaseRoute: runtimeBaseRoute, "StaticWebApps"); string[] args = new[] { $"--ConfigFileName={CUSTOM_CONFIG}" @@ -2673,12 +2685,30 @@ public async Task ValidateErrorMessageForMutationWithoutReadPermission() $"--ConfigFileName={CUSTOM_CONFIG}" }; - string authToken = AuthTestHelper.CreateStaticWebAppsEasyAuthToken(); + string authToken = AuthTestHelper.CreateAppServiceEasyAuthToken(); using (TestServer server = new(Program.CreateWebHostBuilder(args))) using (HttpClient client = server.CreateClient()) { try { + // Pre-clean to avoid PK violation if a previous run left the row behind. + string preCleanupDeleteMutation = @" + mutation { + deleteStock(categoryid: 5001, pieceid: 5001) { + categoryid + pieceid + } + }"; + + _ = await GraphQLRequestExecutor.PostGraphQLRequestAsync( + client, + server.Services.GetRequiredService(), + query: preCleanupDeleteMutation, + queryName: "deleteStock", + variables: null, + authToken: authToken, + clientRoleHeader: AuthorizationResolver.ROLE_AUTHENTICATED); + // A create mutation operation is executed in the context of Anonymous role. The Anonymous role has create action configured but lacks // read action. As a result, a new record should be created in the database but the mutation operation should return an error message. string graphQLMutation = @" @@ -2703,7 +2733,8 @@ public async Task ValidateErrorMessageForMutationWithoutReadPermission() query: graphQLMutation, queryName: "createStock", variables: null, - clientRoleHeader: null + authToken: null, + clientRoleHeader: AuthorizationResolver.ROLE_ANONYMOUS ); Assert.IsNotNull(mutationResponse); @@ -3005,7 +3036,7 @@ public async Task ValidateInheritanceOfReadPermissionFromAnonymous() query: graphQLMutation, queryName: "createStock", variables: null, - authToken: AuthTestHelper.CreateStaticWebAppsEasyAuthToken(), + authToken: AuthTestHelper.CreateAppServiceEasyAuthToken(), clientRoleHeader: AuthorizationResolver.ROLE_AUTHENTICATED ); @@ -3572,7 +3603,7 @@ type Planet @model(name:""PlanetAlias"") { [TestCategory(TestCategory.MSSQL)] [DataRow(HostMode.Development, EasyAuthType.AppService, false, false, DisplayName = "AppService Dev - No EnvVars - No Error")] [DataRow(HostMode.Development, EasyAuthType.AppService, true, false, DisplayName = "AppService Dev - EnvVars - No Error")] - [DataRow(HostMode.Production, EasyAuthType.AppService, false, true, DisplayName = "AppService Prod - No EnvVars - Error")] + [DataRow(HostMode.Production, EasyAuthType.AppService, false, false, DisplayName = "AppService Prod - No EnvVars - Error")] [DataRow(HostMode.Production, EasyAuthType.AppService, true, false, DisplayName = "AppService Prod - EnvVars - Error")] [DataRow(HostMode.Development, EasyAuthType.StaticWebApps, false, false, DisplayName = "SWA Dev - No EnvVars - No Error")] [DataRow(HostMode.Development, EasyAuthType.StaticWebApps, true, false, DisplayName = "SWA Dev - EnvVars - No Error")] @@ -3606,8 +3637,10 @@ public void TestProductionModeAppServiceEnvironmentCheck(HostMode hostMode, Easy string[] args = new[] { $"--ConfigFileName={CUSTOM_CONFIG}" - }; + }; + // When host is in Production mode with AppService as Identity Provider and the environment variables are not set + // we do not throw an exception any longer(PR: 2943), instead log a warning to the user. In this case expectError is false. // This test only checks for startup errors, so no requests are sent to the test server. try { @@ -5260,10 +5293,29 @@ private static JsonContent GetPostStartupConfigParams(string environment, Runtim /// ServiceUnavailable if service is not successfully hydrated with config private static async Task HydratePostStartupConfiguration(HttpClient httpClient, JsonContent content, string configurationEndpoint) { - // Hydrate configuration post-startup - HttpResponseMessage postResult = - await httpClient.PostAsync(configurationEndpoint, content); - Assert.AreEqual(HttpStatusCode.OK, postResult.StatusCode); + string appServiceTokenPayload = AuthTestHelper.CreateAppServiceEasyAuthToken( + roleClaimType: Config.ObjectModel.AuthenticationOptions.ROLE_CLAIM_TYPE, + additionalClaims: + [ + new AppServiceClaim + { + Typ = Config.ObjectModel.AuthenticationOptions.ROLE_CLAIM_TYPE, + Val = POST_STARTUP_CONFIG_ROLE + } + ]); + + using HttpRequestMessage postRequest = new(HttpMethod.Post, configurationEndpoint) + { + Content = content + }; + + postRequest.Headers.Add( + Config.ObjectModel.AuthenticationOptions.CLIENT_PRINCIPAL_HEADER, + appServiceTokenPayload); + + HttpResponseMessage postResult = await httpClient.SendAsync(postRequest); + string body = await postResult.Content.ReadAsStringAsync(); + Assert.AreEqual(HttpStatusCode.OK, postResult.StatusCode, body); return await GetRestResponsePostConfigHydration(httpClient); } @@ -5465,7 +5517,7 @@ public static RuntimeConfig InitMinimalRuntimeConfig( ); entityMap.Add("Publisher", anotherEntity); - Config.ObjectModel.AuthenticationOptions authenticationOptions = new(Provider: nameof(EasyAuthType.StaticWebApps), null); + Config.ObjectModel.AuthenticationOptions authenticationOptions = new(Provider: nameof(EasyAuthType.AppService), null); return new( Schema: "IntegrationTestMinimalSchema", diff --git a/src/Service.Tests/Configuration/HealthEndpointRolesTests.cs b/src/Service.Tests/Configuration/HealthEndpointRolesTests.cs index 9ad36bfa15..99f3284a8a 100644 --- a/src/Service.Tests/Configuration/HealthEndpointRolesTests.cs +++ b/src/Service.Tests/Configuration/HealthEndpointRolesTests.cs @@ -11,6 +11,7 @@ using Azure.DataApiBuilder.Core.Authorization; using Microsoft.AspNetCore.TestHost; using Microsoft.VisualStudio.TestTools.UnitTesting; +using static Azure.DataApiBuilder.Core.AuthenticationHelpers.AppServiceAuthentication; namespace Azure.DataApiBuilder.Service.Tests.Configuration { @@ -75,10 +76,20 @@ public async Task ComprehensiveHealthEndpoint_RolesTests(string role, HostMode h // Sends a GET request to a protected entity which requires a specific role to access. // Authorization checks HttpRequestMessage message = new(method: HttpMethod.Get, requestUri: $"/health"); - string swaTokenPayload = AuthTestHelper.CreateStaticWebAppsEasyAuthToken( - addAuthenticated: true, - specificRole: STARTUP_CONFIG_ROLE); - message.Headers.Add(AuthenticationOptions.CLIENT_PRINCIPAL_HEADER, swaTokenPayload); + string appServiceTokenPayload = AuthTestHelper.CreateAppServiceEasyAuthToken( + roleClaimType: AuthenticationOptions.ROLE_CLAIM_TYPE, + additionalClaims: !string.IsNullOrEmpty(STARTUP_CONFIG_ROLE) + ? + [ + new AppServiceClaim + { + Typ = AuthenticationOptions.ROLE_CLAIM_TYPE, + Val = STARTUP_CONFIG_ROLE + } + ] + : null); + + message.Headers.Add(AuthenticationOptions.CLIENT_PRINCIPAL_HEADER, appServiceTokenPayload); message.Headers.Add(AuthorizationResolver.CLIENT_ROLE_HEADER, STARTUP_CONFIG_ROLE); HttpResponseMessage authorizedResponse = await client.SendAsync(message); @@ -119,7 +130,7 @@ private static void CreateCustomConfigFile(Dictionary entityMap, ConfigurationTests.GetConnectionStringFromEnvironmentConfig(environment: TestCategory.MSSQL), Options: null, Health: new(true)); - HostOptions hostOptions = new(Mode: hostMode, Cors: null, Authentication: new() { Provider = nameof(EasyAuthType.StaticWebApps) }); + HostOptions hostOptions = new(Mode: hostMode, Cors: null, Authentication: new() { Provider = nameof(EasyAuthType.AppService) }); RuntimeConfig runtimeConfig = new( Schema: string.Empty, diff --git a/src/Service.Tests/Configuration/HealthEndpointTests.cs b/src/Service.Tests/Configuration/HealthEndpointTests.cs index 1eac7416e3..7f84d4fa15 100644 --- a/src/Service.Tests/Configuration/HealthEndpointTests.cs +++ b/src/Service.Tests/Configuration/HealthEndpointTests.cs @@ -547,7 +547,7 @@ private static RuntimeConfig CreateRuntimeConfig(Dictionary enti ConfigurationTests.GetConnectionStringFromEnvironmentConfig(environment: TestCategory.MSSQL), Options: null, Health: new(enableDatasourceHealth)); - HostOptions hostOptions = new(Mode: hostMode, Cors: null, Authentication: new() { Provider = nameof(EasyAuthType.StaticWebApps) }); + HostOptions hostOptions = new(Mode: hostMode, Cors: null, Authentication: new() { Provider = nameof(EasyAuthType.AppService) }); RuntimeConfig runtimeConfig = new( Schema: string.Empty, diff --git a/src/Service.Tests/Configuration/HotReload/ConfigurationHotReloadTests.cs b/src/Service.Tests/Configuration/HotReload/ConfigurationHotReloadTests.cs index d2ea7708ce..0a5f460759 100644 --- a/src/Service.Tests/Configuration/HotReload/ConfigurationHotReloadTests.cs +++ b/src/Service.Tests/Configuration/HotReload/ConfigurationHotReloadTests.cs @@ -90,7 +90,7 @@ private static void GenerateConfigFile( ""allow-credentials"": false }, ""authentication"": { - ""provider"": ""StaticWebApps"" + ""provider"": ""AppService"" }, ""mode"": ""development"" }, diff --git a/src/Service.Tests/CosmosTests/MutationTests.cs b/src/Service.Tests/CosmosTests/MutationTests.cs index de931dcf22..ec611da59e 100644 --- a/src/Service.Tests/CosmosTests/MutationTests.cs +++ b/src/Service.Tests/CosmosTests/MutationTests.cs @@ -6,6 +6,7 @@ using System.Globalization; using System.IO; using System.Net.Http; +using System.Security.Claims; using System.Text.Json; using System.Threading.Tasks; using Azure.DataApiBuilder.Config.NamingPolicies; @@ -19,6 +20,7 @@ using Microsoft.Azure.Cosmos; using Microsoft.Extensions.DependencyInjection; using Microsoft.VisualStudio.TestTools.UnitTesting; +using static Azure.DataApiBuilder.Core.AuthenticationHelpers.AppServiceAuthentication; namespace Azure.DataApiBuilder.Service.Tests.CosmosTests { @@ -278,7 +280,18 @@ public async Task CreateItemWithAuthPermissions(string roleName, string expected name }} }}"; - string authToken = AuthTestHelper.CreateStaticWebAppsEasyAuthToken(specificRole: roleName); + // For App Service, the effective role must be present as a role + // claim on the principal (in addition to X-MS-API-ROLE) so that + // DAB authZ can evaluate permissions consistently with SWA tests. + AppServiceClaim roleClaim = new() + { + Val = roleName, + Typ = ClaimTypes.Role + }; + + string authToken = AuthTestHelper.CreateAppServiceEasyAuthToken( + additionalClaims: [roleClaim]); + JsonElement response = await ExecuteGraphQLRequestAsync("createPlanetAgain", mutation, variables: new(), authToken: authToken, clientRoleHeader: roleName); // Validate the result contains the GraphQL authorization error code. @@ -317,10 +330,21 @@ public async Task UpdateItemWithAuthPermissions(string roleName, string expected name }} }}"; + // For App Service, the effective role must be present as a role + // claim on the principal (in addition to X-MS-API-ROLE) so that + // DAB authZ can evaluate permissions consistently with SWA tests. + AppServiceClaim roleClaim = new() + { + Val = roleName, + Typ = ClaimTypes.Role + }; + + string authToken = AuthTestHelper.CreateAppServiceEasyAuthToken( + additionalClaims: [roleClaim]); JsonElement createResponse = await ExecuteGraphQLRequestAsync("createPlanetAgain", createMutation, variables: new(), - authToken: AuthTestHelper.CreateStaticWebAppsEasyAuthToken(specificRole: AuthorizationType.Authenticated.ToString()), + authToken: authToken, clientRoleHeader: AuthorizationType.Authenticated.ToString()); // Making sure item is created successfully @@ -340,7 +364,6 @@ public async Task UpdateItemWithAuthPermissions(string roleName, string expected name = "new_name" }; - string authToken = AuthTestHelper.CreateStaticWebAppsEasyAuthToken(specificRole: roleName); JsonElement response = await ExecuteGraphQLRequestAsync( queryName: "updatePlanetAgain", query: mutation, @@ -384,9 +407,21 @@ public async Task DeleteItemWithAuthPermissions(string roleName, string expected }} }}"; + // For App Service, the effective role must be present as a role + // claim on the principal (in addition to X-MS-API-ROLE) so that + // DAB authZ can evaluate permissions consistently with SWA tests. + AppServiceClaim roleClaim = new() + { + Val = roleName, + Typ = ClaimTypes.Role + }; + + string authToken = AuthTestHelper.CreateAppServiceEasyAuthToken( + additionalClaims: [roleClaim]); + JsonElement createResponse = await ExecuteGraphQLRequestAsync("createPlanetAgain", createMutation, variables: new(), - authToken: AuthTestHelper.CreateStaticWebAppsEasyAuthToken(specificRole: AuthorizationType.Authenticated.ToString()), + authToken: authToken, clientRoleHeader: AuthorizationType.Authenticated.ToString()); // Making sure item is created successfully @@ -400,7 +435,6 @@ public async Task DeleteItemWithAuthPermissions(string roleName, string expected name } }"; - string authToken = AuthTestHelper.CreateStaticWebAppsEasyAuthToken(specificRole: roleName); JsonElement response = await ExecuteGraphQLRequestAsync( queryName: "deletePlanetAgain", query: mutation, @@ -563,7 +597,7 @@ type Planet @model(name:""Planet"") { }; string id = Guid.NewGuid().ToString(); - string authToken = AuthTestHelper.CreateStaticWebAppsEasyAuthToken(); + string authToken = AuthTestHelper.CreateAppServiceEasyAuthToken(); using (TestServer server = new(Program.CreateWebHostBuilder(args))) using (HttpClient client = server.CreateClient()) { @@ -713,7 +747,7 @@ type Planet @model(name:""Planet"") { query: _createPlanetMutation, queryName: "createPlanet", variables: new() { { "item", input } }, - authToken: AuthTestHelper.CreateStaticWebAppsEasyAuthToken(), + authToken: AuthTestHelper.CreateAppServiceEasyAuthToken(), clientRoleHeader: AuthorizationResolver.ROLE_AUTHENTICATED ); @@ -762,6 +796,7 @@ public async Task CanPatchItemWithoutVariables() }} }}"; JsonElement response = await ExecuteGraphQLRequestAsync("patchPlanet", mutation, variables: new()); + // Validate results Assert.AreEqual(id, response.GetProperty("id").GetString()); Assert.AreEqual(newName, response.GetProperty("name").GetString()); @@ -906,7 +941,7 @@ public async Task CanPatchNestedItemWithVariables() public async Task CanPatchMoreThan10AttributesInAnItemWithVariables() { string roleName = "anonymous"; - string authToken = AuthTestHelper.CreateStaticWebAppsEasyAuthToken(specificRole: roleName); + string authToken = AuthTestHelper.CreateAppServiceEasyAuthToken(); // Run mutation Add planet; string id = Guid.NewGuid().ToString(); diff --git a/src/Service.Tests/CosmosTests/QueryFilterTests.cs b/src/Service.Tests/CosmosTests/QueryFilterTests.cs index 0dac6a1c1c..187b447973 100644 --- a/src/Service.Tests/CosmosTests/QueryFilterTests.cs +++ b/src/Service.Tests/CosmosTests/QueryFilterTests.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System.Collections.Generic; +using System.Security.Claims; using System.Text.Json; using System.Threading.Tasks; using Azure.DataApiBuilder.Config.ObjectModel; @@ -11,6 +12,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.VisualStudio.TestTools.UnitTesting; using Newtonsoft.Json.Linq; +using static Azure.DataApiBuilder.Core.AuthenticationHelpers.AppServiceAuthentication; using QueryBuilder = Azure.DataApiBuilder.Service.GraphQLBuilder.Queries.QueryBuilder; namespace Azure.DataApiBuilder.Service.Tests.CosmosTests @@ -280,7 +282,7 @@ public async Task TestStringMultiFiltersOnArrayTypeWithOrCondition() private async Task ExecuteAndValidateResult(string graphQLQueryName, string gqlQuery, string dbQuery, bool ignoreBlankResults = false, Dictionary variables = null) { - string authToken = AuthTestHelper.CreateStaticWebAppsEasyAuthToken(specificRole: AuthorizationType.Authenticated.ToString()); + string authToken = AuthTestHelper.CreateAppServiceEasyAuthToken(); JsonElement actual = await ExecuteGraphQLRequestAsync(graphQLQueryName, query: gqlQuery, authToken: authToken, variables: variables); JsonDocument expected = await ExecuteCosmosRequestAsync(dbQuery, _pageSize, null, _containerName); ValidateResults(actual.GetProperty("items"), expected.RootElement, ignoreBlankResults); @@ -918,11 +920,19 @@ public async Task TestQueryFilterFieldAuth_Only_AuthorizedArrayItem() // Now get the item with item level permission string clientRoleHeader = "item-level-permission-role"; - // string clientRoleHeader = "authenticated"; + + AppServiceClaim roleClaim = new() + { + Val = clientRoleHeader, + Typ = ClaimTypes.Role + }; + + // For App Service, roles must exist as claims on the principal + // (in addition to X-MS-API-ROLE) for DAB authZ to allow the request. JsonElement actual = await ExecuteGraphQLRequestAsync( queryName: _graphQLQueryName, query: gqlQuery, - authToken: AuthTestHelper.CreateStaticWebAppsEasyAuthToken(specificRole: clientRoleHeader), + authToken: AuthTestHelper.CreateAppServiceEasyAuthToken(additionalClaims: [roleClaim]), clientRoleHeader: clientRoleHeader); string dbQuery = $"SELECT c.id " + @@ -980,11 +990,22 @@ public async Task TestQueryFilterFieldAuth_UnauthorizedField() } }"; string clientRoleHeader = "limited-read-role"; + + AppServiceClaim roleClaim = new() + { + Val = clientRoleHeader, + Typ = ClaimTypes.Role + }; + + // For App Service, roles must exist as claims on the principal + // (in addition to X-MS-API-ROLE) for DAB authZ to allow the request. + string authToken = AuthTestHelper.CreateAppServiceEasyAuthToken(additionalClaims: [roleClaim]); + JsonElement response = await ExecuteGraphQLRequestAsync( queryName: _graphQLQueryName, query: gqlQuery, variables: new() { { "name", "test name" } }, - authToken: AuthTestHelper.CreateStaticWebAppsEasyAuthToken(specificRole: clientRoleHeader), + authToken: authToken, clientRoleHeader: clientRoleHeader); // Validate the result contains the GraphQL authorization error code. @@ -1012,7 +1033,7 @@ public async Task TestQueryFilterFieldAuth_AuthorizedWildCard() queryName: "planets", query: gqlQuery, variables: new() { }, - authToken: AuthTestHelper.CreateStaticWebAppsEasyAuthToken(specificRole: clientRoleHeader), + authToken: AuthTestHelper.CreateAppServiceEasyAuthToken(), clientRoleHeader: clientRoleHeader); Assert.AreEqual(response.GetProperty("items")[0].GetProperty("name").ToString(), "Earth"); @@ -1037,7 +1058,7 @@ public async Task TestQueryFilterNestedFieldAuth_AuthorizedNestedField() } }"; - string authToken = AuthTestHelper.CreateStaticWebAppsEasyAuthToken(specificRole: AuthorizationType.Authenticated.ToString()); + string authToken = AuthTestHelper.CreateAppServiceEasyAuthToken(); JsonElement actual = await ExecuteGraphQLRequestAsync(_graphQLQueryName, query: gqlQuery, authToken: authToken); Assert.AreEqual(actual.GetProperty("items")[0].GetProperty("earth").GetProperty("id").ToString(), _idList[0]); } @@ -1065,11 +1086,22 @@ public async Task TestQueryFilterNestedFieldAuth_UnauthorizedNestedField() }"; string clientRoleHeader = "limited-read-role"; + + AppServiceClaim roleClaim = new() + { + Val = clientRoleHeader, + Typ = ClaimTypes.Role + }; + + // For App Service, roles must exist as claims on the principal + // (in addition to X-MS-API-ROLE) for DAB authZ to allow the request. + string authToken = AuthTestHelper.CreateAppServiceEasyAuthToken(additionalClaims: [roleClaim]); + JsonElement response = await ExecuteGraphQLRequestAsync( queryName: _graphQLQueryName, query: gqlQuery, variables: new() { { "name", "test name" } }, - authToken: AuthTestHelper.CreateStaticWebAppsEasyAuthToken(specificRole: clientRoleHeader), + authToken: authToken, clientRoleHeader: clientRoleHeader); // Validate the result contains the GraphQL authorization error code. @@ -1100,7 +1132,7 @@ public async Task TestQueryFilterNestedArrayFieldAuth_UnauthorizedNestedField() JsonElement response = await ExecuteGraphQLRequestAsync( queryName: _graphQLQueryName, query: gqlQuery, - authToken: AuthTestHelper.CreateStaticWebAppsEasyAuthToken(specificRole: clientRoleHeader), + authToken: AuthTestHelper.CreateAppServiceEasyAuthToken(), clientRoleHeader: clientRoleHeader); // Validate the result contains the GraphQL authorization error code. @@ -1131,7 +1163,17 @@ public async Task TestQueryFieldAuthConflictingWithFilterFieldAuth_Unauthorized( }"; string clientRoleHeader = "limited-read-role"; - string authToken = AuthTestHelper.CreateStaticWebAppsEasyAuthToken(specificRole: clientRoleHeader); + + AppServiceClaim roleClaim = new() + { + Val = clientRoleHeader, + Typ = ClaimTypes.Role + }; + + // For App Service, roles must exist as claims on the principal + // (in addition to X-MS-API-ROLE) for DAB authZ to allow the request. + // SWA tests passed without this because SWA uses a looser, header-driven role model. + string authToken = AuthTestHelper.CreateAppServiceEasyAuthToken(additionalClaims: [roleClaim]); JsonElement response = await ExecuteGraphQLRequestAsync(_graphQLQueryName, query: gqlQuery, authToken: authToken, @@ -1190,7 +1232,7 @@ public async Task TestQueryFilterFieldAuth_ExcludeTakesPredecence() queryName: _graphQLQueryName, query: gqlQuery, variables: new() { { "name", "test name" } }, - authToken: AuthTestHelper.CreateStaticWebAppsEasyAuthToken(specificRole: clientRoleHeader), + authToken: AuthTestHelper.CreateAppServiceEasyAuthToken(), clientRoleHeader: clientRoleHeader); // Validate the result contains the GraphQL authorization error code. diff --git a/src/Service.Tests/CosmosTests/QueryTests.cs b/src/Service.Tests/CosmosTests/QueryTests.cs index 52afa8e788..bfcdb2cf91 100644 --- a/src/Service.Tests/CosmosTests/QueryTests.cs +++ b/src/Service.Tests/CosmosTests/QueryTests.cs @@ -105,7 +105,7 @@ public async Task GetWithInvalidAuthorizationPolicyInSchema() queryName: "invalidAuthModel_by_pk", query: MoonWithInvalidAuthorizationPolicy, variables: new() { { "id", id }, { "partitionKeyValue", id } }, - authToken: AuthTestHelper.CreateStaticWebAppsEasyAuthToken(specificRole: clientRoleHeader), + authToken: AuthTestHelper.CreateAppServiceEasyAuthToken(), clientRoleHeader: clientRoleHeader); // Validate the result contains the GraphQL authorization error code. diff --git a/src/Service.Tests/ModuleInitializer.cs b/src/Service.Tests/ModuleInitializer.cs index ba0407ecd5..907c60f652 100644 --- a/src/Service.Tests/ModuleInitializer.cs +++ b/src/Service.Tests/ModuleInitializer.cs @@ -77,6 +77,8 @@ public static void Init() VerifierSettings.IgnoreMember(config => config.McpDmlTools); // Ignore the IsStaticWebAppsIdentityProvider as that's unimportant from a test standpoint. VerifierSettings.IgnoreMember(config => config.IsStaticWebAppsIdentityProvider); + // Ignore the IsAppServiceIdentityProvider as that's unimportant from a test standpoint. + VerifierSettings.IgnoreMember(config => config.IsAppServiceIdentityProvider); // Ignore the RestPath as that's unimportant from a test standpoint. VerifierSettings.IgnoreMember(config => config.RestPath); // Ignore the GraphQLPath as that's unimportant from a test standpoint. diff --git a/src/Service.Tests/Multidab-config.MsSql.json b/src/Service.Tests/Multidab-config.MsSql.json index a428c0a27b..b54b629023 100644 --- a/src/Service.Tests/Multidab-config.MsSql.json +++ b/src/Service.Tests/Multidab-config.MsSql.json @@ -30,7 +30,7 @@ "allow-credentials": false }, "authentication": { - "provider": "StaticWebApps" + "provider": "AppService" }, "mode": "development" } diff --git a/src/Service.Tests/Multidab-config.MySql.json b/src/Service.Tests/Multidab-config.MySql.json index efd7e7c973..5a9bc4cee0 100644 --- a/src/Service.Tests/Multidab-config.MySql.json +++ b/src/Service.Tests/Multidab-config.MySql.json @@ -22,7 +22,7 @@ "allow-credentials": false }, "authentication": { - "provider": "StaticWebApps" + "provider": "AppService" }, "mode": "development" } diff --git a/src/Service.Tests/Multidab-config.PostgreSql.json b/src/Service.Tests/Multidab-config.PostgreSql.json index 106e6d5ac2..0279076273 100644 --- a/src/Service.Tests/Multidab-config.PostgreSql.json +++ b/src/Service.Tests/Multidab-config.PostgreSql.json @@ -22,7 +22,7 @@ "allow-credentials": false }, "authentication": { - "provider": "StaticWebApps" + "provider": "AppService" }, "mode": "development" } diff --git a/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForCosmos.verified.txt b/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForCosmos.verified.txt index c75d645d13..9279da9d59 100644 --- a/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForCosmos.verified.txt +++ b/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForCosmos.verified.txt @@ -45,7 +45,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps + Provider: AppService } } }, diff --git a/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMsSql.verified.txt b/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMsSql.verified.txt index 52f4035868..35fd562c87 100644 --- a/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMsSql.verified.txt +++ b/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMsSql.verified.txt @@ -49,7 +49,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps + Provider: AppService } } }, diff --git a/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMySql.verified.txt b/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMySql.verified.txt index 6a3a4c226c..1490309ece 100644 --- a/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMySql.verified.txt +++ b/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMySql.verified.txt @@ -41,7 +41,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps + Provider: AppService } } }, diff --git a/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForPostgreSql.verified.txt b/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForPostgreSql.verified.txt index 4373b266f4..ceba40ae63 100644 --- a/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForPostgreSql.verified.txt +++ b/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForPostgreSql.verified.txt @@ -41,7 +41,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps + Provider: AppService } } }, diff --git a/src/Service.Tests/Snapshots/CorsUnitTests.TestCorsConfigReadCorrectly.verified.txt b/src/Service.Tests/Snapshots/CorsUnitTests.TestCorsConfigReadCorrectly.verified.txt index d31e71399b..ce6395909b 100644 --- a/src/Service.Tests/Snapshots/CorsUnitTests.TestCorsConfigReadCorrectly.verified.txt +++ b/src/Service.Tests/Snapshots/CorsUnitTests.TestCorsConfigReadCorrectly.verified.txt @@ -3,6 +3,6 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps + Provider: AppService } } \ No newline at end of file diff --git a/src/Service.Tests/SqlTests/GraphQLQueryTests/MsSqlGraphQLQueryTests.cs b/src/Service.Tests/SqlTests/GraphQLQueryTests/MsSqlGraphQLQueryTests.cs index 00930103c7..f51dce0c24 100644 --- a/src/Service.Tests/SqlTests/GraphQLQueryTests/MsSqlGraphQLQueryTests.cs +++ b/src/Service.Tests/SqlTests/GraphQLQueryTests/MsSqlGraphQLQueryTests.cs @@ -4,7 +4,9 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text; using System.Text.Json; +using System.Text.Json.Nodes; using System.Threading.Tasks; using Azure.DataApiBuilder.Config.ObjectModel; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -229,6 +231,199 @@ FOR JSON PATH await InFilterOneToOneJoinQuery(msSqlQuery); } + /// + /// Verifies that the nested reviews connection under books correctly paginates + /// when there are more than 100 reviews for a single book, while also + /// exercising sibling navigation properties (websiteplacement and authors) + /// under RBAC in the same request. The first page should contain 100 reviews + /// and the endCursor should encode the id of the last review on that page. + /// + [TestMethod] + public async Task NestedReviewsConnection_WithSiblings_PaginatesMoreThanHundredItems() + { + // Seed > 100 reviews for book id 1. Use a distinct id range so we can + // clean up without impacting existing rows used by other tests. + StringBuilder sb = new(); + sb.AppendLine("SET IDENTITY_INSERT reviews ON;"); + sb.AppendLine("INSERT INTO reviews(id, book_id, content) VALUES"); + + for (int id = 2000; id <= 2100; id++) + { + string line = $" ({id}, 1, 'Bulk review {id}')"; + if (id < 2100) + { + line += ","; + } + else + { + line += ";"; + } + + sb.AppendLine(line); + } + + sb.AppendLine("SET IDENTITY_INSERT reviews OFF;"); + string seedReviewsSql = sb.ToString(); + + string cleanupReviewsSql = "DELETE FROM reviews WHERE id BETWEEN 2000 AND 2100;"; + + try + { + // Seed additional data for this test only. + await _queryExecutor.ExecuteQueryAsync( + seedReviewsSql, + dataSourceName: string.Empty, + parameters: null, + dataReaderHandler: null); + + string graphQLQueryName = "books"; + string graphQLQuery = @"query { + books(filter: { id: { eq: 1 } }) { + items { + id + title + websiteplacement { + price + } + reviews(first: 100) { + items { + id + } + endCursor + hasNextPage + } + authors { + items { + name + } + } + } + } + }"; + + JsonElement actual = await ExecuteGraphQLRequestAsync( + graphQLQuery, + graphQLQueryName, + isAuthenticated: true, + clientRoleHeader: "authenticated"); + + JsonElement book = actual.GetProperty("items")[0]; + JsonElement websiteplacement = book.GetProperty("websiteplacement"); + JsonElement authorsConnection = book.GetProperty("authors"); + JsonElement reviewsConnection = book.GetProperty("reviews"); + JsonElement reviewItems = reviewsConnection.GetProperty("items"); + + // First page should contain exactly 100 reviews when more than 100 exist. + Assert.AreEqual(100, reviewItems.GetArrayLength(), "Expected first page of reviews to contain 100 items."); + + bool hasNextPage = reviewsConnection.GetProperty("hasNextPage").GetBoolean(); + Assert.IsTrue(hasNextPage, "Expected hasNextPage to be true when more than 100 reviews exist."); + + string endCursor = reviewsConnection.GetProperty("endCursor").GetString(); + Assert.IsFalse(string.IsNullOrEmpty(endCursor), "Expected endCursor to be populated when hasNextPage is true."); + + // Decode the opaque cursor and verify it encodes the id of the + // last review on the first page. + int lastReviewIdOnPage = reviewItems[reviewItems.GetArrayLength() - 1].GetProperty("id").GetInt32(); + + string decodedCursorJson = System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(endCursor)); + using JsonDocument cursorDoc = JsonDocument.Parse(decodedCursorJson); + JsonElement cursorArray = cursorDoc.RootElement; + + bool foundIdFieldInCursor = false; + foreach (JsonElement field in cursorArray.EnumerateArray()) + { + if (field.GetProperty("FieldName").GetString() == "id") + { + int cursorId = field.GetProperty("FieldValue").GetInt32(); + Assert.AreEqual(lastReviewIdOnPage, cursorId, "endCursor should encode the id of the last review on the first page."); + foundIdFieldInCursor = true; + break; + } + } + + Assert.IsTrue(foundIdFieldInCursor, "endCursor payload should include a pagination field for id."); + + // Also validate that sibling navigation properties under RBAC are + // materialized as part of the same query and that their results + // match the outcome of equivalent SQL JSON queries. + Assert.AreEqual(JsonValueKind.Object, websiteplacement.ValueKind, "Expected websiteplacement object to be materialized."); + + JsonElement authorsItems = authorsConnection.GetProperty("items"); + Assert.IsTrue(authorsItems.GetArrayLength() > 0, "Expected authors collection to be materialized with at least one author."); + + // Compare the full nested tree (id, title, websiteplacement, reviews.items, authors.items) + // against an equivalent SQL JSON query. Shape the SQL JSON to match the GraphQL + // "book" object so we can use the actual result directly for comparison. + string fullTreeSql = @" + SELECT + [b].[id] AS [id], + [b].[title] AS [title], + JSON_QUERY(( + SELECT TOP 1 [wp].[price] AS [price] + FROM [dbo].[book_website_placements] AS [wp] + WHERE [wp].[book_id] = [b].[id] + ORDER BY [wp].[id] ASC + FOR JSON PATH, INCLUDE_NULL_VALUES, WITHOUT_ARRAY_WRAPPER + )) AS [websiteplacement], + JSON_QUERY(( + SELECT + JSON_QUERY(( + SELECT TOP 100 [r].[id] AS [id] + FROM [dbo].[reviews] AS [r] + WHERE [r].[book_id] = [b].[id] + ORDER BY [r].[id] ASC + FOR JSON PATH, INCLUDE_NULL_VALUES + )) AS [items] + FOR JSON PATH, INCLUDE_NULL_VALUES, WITHOUT_ARRAY_WRAPPER + )) AS [reviews], + JSON_QUERY(( + SELECT + JSON_QUERY(( + SELECT [a].[name] AS [name] + FROM [dbo].[authors] AS [a] + INNER JOIN [dbo].[book_author_link] AS [bal] + ON [bal].[author_id] = [a].[id] + WHERE [bal].[book_id] = [b].[id] + ORDER BY [a].[id] ASC + FOR JSON PATH, INCLUDE_NULL_VALUES + )) AS [items] + FOR JSON PATH, INCLUDE_NULL_VALUES, WITHOUT_ARRAY_WRAPPER + )) AS [authors] + FROM [dbo].[books] AS [b] + WHERE [b].[id] = 1 + FOR JSON PATH, INCLUDE_NULL_VALUES, WITHOUT_ARRAY_WRAPPER"; + + string expectedFullTreeJson = await GetDatabaseResultAsync(fullTreeSql); + + // Use the actual GraphQL "book" object for comparison, trimming pagination metadata + // (endCursor, hasNextPage) that cannot be reproduced via SQL. + JsonNode actualBookNode = JsonNode.Parse(book.ToString()); + if (actualBookNode is JsonObject bookObject && + bookObject["reviews"] is JsonObject reviewsObject) + { + reviewsObject.Remove("endCursor"); + reviewsObject.Remove("hasNextPage"); + } + + string actualComparableJson = actualBookNode.ToJsonString(); + + SqlTestHelper.PerformTestEqualJsonStrings( + expectedFullTreeJson, + actualComparableJson); + } + finally + { + // Clean up the seeded reviews so other tests relying on the base + // fixture data remain unaffected. + await _queryExecutor.ExecuteQueryAsync( + cleanupReviewsSql, + dataSourceName: string.Empty, + parameters: null, + dataReaderHandler: null); + } + } + /// /// Test query on One-To-One relationship when the fields defining /// the relationship in the entity include fields that are mapped in diff --git a/src/Service.Tests/SqlTests/SqlTestBase.cs b/src/Service.Tests/SqlTests/SqlTestBase.cs index bb8ed0628d..16e804f117 100644 --- a/src/Service.Tests/SqlTests/SqlTestBase.cs +++ b/src/Service.Tests/SqlTests/SqlTestBase.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System; using System.Collections.Generic; using System.IO; using System.Net; @@ -36,6 +37,7 @@ using MySqlConnector; using Npgsql; using ZiggyCreatures.Caching.Fusion; +using static Azure.DataApiBuilder.Core.AuthenticationHelpers.AppServiceAuthentication; namespace Azure.DataApiBuilder.Service.Tests.SqlTests { @@ -84,6 +86,7 @@ protected async static Task InitializeTestFixture( bool isRestBodyStrict = true) { TestHelper.SetupDatabaseEnvironment(DatabaseEngine); + // Get the base config file from disk RuntimeConfig runtimeConfig = SqlTestHelper.SetupRuntimeConfig(); @@ -508,9 +511,37 @@ protected static async Task SetupAndRunRestApiTest( if (clientRoleHeader is not null) { - request.Headers.Add(AuthorizationResolver.CLIENT_ROLE_HEADER, clientRoleHeader.ToString()); - request.Headers.Add(AuthenticationOptions.CLIENT_PRINCIPAL_HEADER, - AuthTestHelper.CreateStaticWebAppsEasyAuthToken(addAuthenticated: true, specificRole: clientRoleHeader)); + request.Headers.Add(AuthorizationResolver.CLIENT_ROLE_HEADER, clientRoleHeader); + + // Detect runtime auth provider once per call + RuntimeConfigProvider configProvider = _application.Services.GetRequiredService(); + string provider = configProvider.GetConfig().Runtime.Host.Authentication.Provider; + + if (string.Equals(provider, nameof(EasyAuthType.AppService), StringComparison.OrdinalIgnoreCase)) + { + // AppService EasyAuth principal with this role + request.Headers.Add( + AuthenticationOptions.CLIENT_PRINCIPAL_HEADER, + AuthTestHelper.CreateAppServiceEasyAuthToken( + roleClaimType: AuthenticationOptions.ROLE_CLAIM_TYPE, + additionalClaims: + [ + new AppServiceClaim + { + Typ = AuthenticationOptions.ROLE_CLAIM_TYPE, + Val = clientRoleHeader + } + ])); + } + else + { + // Static Web Apps principal as before + request.Headers.Add( + AuthenticationOptions.CLIENT_PRINCIPAL_HEADER, + AuthTestHelper.CreateStaticWebAppsEasyAuthToken( + addAuthenticated: true, + specificRole: clientRoleHeader)); + } } // Send request to the engine. @@ -614,13 +645,43 @@ protected virtual async Task ExecuteGraphQLRequestAsync( bool expectsError = false) { RuntimeConfigProvider configProvider = _application.Services.GetService(); + + string authToken = null; + + if (isAuthenticated) + { + string provider = configProvider.GetConfig().Runtime.Host.Authentication.Provider; + + if (string.Equals(provider, nameof(EasyAuthType.AppService), StringComparison.OrdinalIgnoreCase)) + { + authToken = AuthTestHelper.CreateAppServiceEasyAuthToken( + roleClaimType: AuthenticationOptions.ROLE_CLAIM_TYPE, + additionalClaims: !string.IsNullOrEmpty(clientRoleHeader) + ? + [ + new AppServiceClaim + { + Typ = AuthenticationOptions.ROLE_CLAIM_TYPE, + Val = clientRoleHeader + } + ] + : null); + } + else + { + authToken = AuthTestHelper.CreateStaticWebAppsEasyAuthToken( + addAuthenticated: true, + specificRole: clientRoleHeader); + } + } + return await GraphQLRequestExecutor.PostGraphQLRequestAsync( HttpClient, configProvider, queryName, query, variables, - isAuthenticated ? AuthTestHelper.CreateStaticWebAppsEasyAuthToken(specificRole: clientRoleHeader) : null, + authToken, clientRoleHeader: clientRoleHeader); } diff --git a/src/Service.Tests/SqlTests/SqlTestHelper.cs b/src/Service.Tests/SqlTests/SqlTestHelper.cs index e739f6cc8c..cf65c9a9f5 100644 --- a/src/Service.Tests/SqlTests/SqlTestHelper.cs +++ b/src/Service.Tests/SqlTests/SqlTestHelper.cs @@ -381,7 +381,7 @@ public static RuntimeConfig InitBasicRuntimeConfigWithNoEntity( string testCategory = TestCategory.MSSQL) { DataSource dataSource = new(dbType, GetConnectionStringFromEnvironmentConfig(environment: testCategory), new()); - Config.ObjectModel.AuthenticationOptions authenticationOptions = new(Provider: nameof(EasyAuthType.StaticWebApps), null); + Config.ObjectModel.AuthenticationOptions authenticationOptions = new(Provider: nameof(EasyAuthType.AppService), null); RuntimeConfig runtimeConfig = new( Schema: "IntegrationTestMinimalSchema", diff --git a/src/Service.Tests/TestHelper.cs b/src/Service.Tests/TestHelper.cs index b94470b96b..035ee3e6ee 100644 --- a/src/Service.Tests/TestHelper.cs +++ b/src/Service.Tests/TestHelper.cs @@ -185,7 +185,7 @@ public static string GenerateInvalidSchema() ""allow-credentials"": false }, ""authentication"": { - ""provider"": ""StaticWebApps"" + ""provider"": ""AppService"" } } }, @@ -217,7 +217,7 @@ public static string GenerateInvalidSchema() ""allow-credentials"": false }, ""authentication"": { - ""provider"": ""StaticWebApps"" + ""provider"": ""AppService"" } } }, @@ -259,7 +259,7 @@ public static string GenerateInvalidSchema() ""allow-credentials"": false }, ""authentication"": { - ""provider"": ""StaticWebApps"" + ""provider"": ""AppService"" } } }" + "," + @@ -330,27 +330,39 @@ public static RuntimeConfigProvider GenerateInMemoryRuntimeConfigProvider(Runtim /// HostMode for the engine /// Database type /// Base route for API requests. - public static void ConstructNewConfigWithSpecifiedHostMode(string configFileName, HostMode hostModeType, string databaseType, string runtimeBaseRoute = "/") + /// Provider for authentication + public static void ConstructNewConfigWithSpecifiedHostMode( + string configFileName, + HostMode hostModeType, + string databaseType, + string runtimeBaseRoute = "/", + string provider = "AppService") { SetupDatabaseEnvironment(databaseType); RuntimeConfigProvider configProvider = GetRuntimeConfigProvider(GetRuntimeConfigLoader()); RuntimeConfig config = configProvider.GetConfig(); + AuthenticationOptions auth = config.Runtime?.Host?.Authentication with { Provider = provider }; + + bool isStaticWebApps = string.Equals(provider, "StaticWebApps", StringComparison.OrdinalIgnoreCase); + RuntimeConfig configWithCustomHostMode = - config - with + config with { - Runtime = config.Runtime - with + Runtime = config.Runtime with { - Host = config.Runtime?.Host - with - { Mode = hostModeType }, - BaseRoute = runtimeBaseRoute + Host = config.Runtime?.Host with + { + Mode = hostModeType, + // For tests that explicitly set SWA, we’ll keep BaseRoute. + // For others (AppService), BaseRoute will be null. + Authentication = auth + }, + BaseRoute = isStaticWebApps ? runtimeBaseRoute : null } }; - File.WriteAllText(configFileName, configWithCustomHostMode.ToJson()); + File.WriteAllText(configFileName, configWithCustomHostMode.ToJson()); } /// diff --git a/src/Service.Tests/UnitTests/ConfigFileWatcherUnitTests.cs b/src/Service.Tests/UnitTests/ConfigFileWatcherUnitTests.cs index 4807fcfe6f..b0ae580828 100644 --- a/src/Service.Tests/UnitTests/ConfigFileWatcherUnitTests.cs +++ b/src/Service.Tests/UnitTests/ConfigFileWatcherUnitTests.cs @@ -54,7 +54,7 @@ HostMode mode ""allow-credentials"": false }, ""authentication"": { - ""provider"": ""StaticWebApps"" + ""provider"": ""AppService"" }, ""mode"": """ + mode + @""" } diff --git a/src/Service.Tests/UnitTests/RuntimeConfigLoaderJsonDeserializerTests.cs b/src/Service.Tests/UnitTests/RuntimeConfigLoaderJsonDeserializerTests.cs index 8329dc2134..3f56b85901 100644 --- a/src/Service.Tests/UnitTests/RuntimeConfigLoaderJsonDeserializerTests.cs +++ b/src/Service.Tests/UnitTests/RuntimeConfigLoaderJsonDeserializerTests.cs @@ -656,7 +656,7 @@ private static bool TryParseAndAssertOnDefaults(string json, out RuntimeConfig p Assert.AreEqual(McpRuntimeOptions.DEFAULT_PATH, parsedConfig.McpPath); Assert.IsTrue(parsedConfig.AllowIntrospection); Assert.IsFalse(parsedConfig.IsDevelopmentMode()); - Assert.IsTrue(parsedConfig.IsStaticWebAppsIdentityProvider); + Assert.IsTrue(parsedConfig.IsAppServiceIdentityProvider); Assert.IsTrue(parsedConfig.IsRequestBodyStrict); Assert.IsTrue(parsedConfig.IsLogLevelNull()); Assert.IsTrue(parsedConfig.Runtime?.Telemetry?.ApplicationInsights is null diff --git a/src/Service.Tests/dab-config.CosmosDb_NoSql.json b/src/Service.Tests/dab-config.CosmosDb_NoSql.json index 5704dc19be..26f2e3fc3c 100644 --- a/src/Service.Tests/dab-config.CosmosDb_NoSql.json +++ b/src/Service.Tests/dab-config.CosmosDb_NoSql.json @@ -28,7 +28,7 @@ "allow-credentials": false }, "authentication": { - "provider": "StaticWebApps" + "provider": "AppService" }, "mode": "development" }, diff --git a/src/Service.Tests/dab-config.DwSql.json b/src/Service.Tests/dab-config.DwSql.json index 78f9e91480..bb0d6a5893 100644 --- a/src/Service.Tests/dab-config.DwSql.json +++ b/src/Service.Tests/dab-config.DwSql.json @@ -26,7 +26,7 @@ "allow-credentials": false }, "authentication": { - "provider": "StaticWebApps" + "provider": "AppService" }, "mode": "development" } diff --git a/src/Service.Tests/dab-config.MsSql.json b/src/Service.Tests/dab-config.MsSql.json index d5e903d4f3..e7f73702ba 100644 --- a/src/Service.Tests/dab-config.MsSql.json +++ b/src/Service.Tests/dab-config.MsSql.json @@ -36,7 +36,7 @@ "allow-credentials": false }, "authentication": { - "provider": "StaticWebApps" + "provider": "AppService" }, "mode": "development" } diff --git a/src/Service.Tests/dab-config.MySql.json b/src/Service.Tests/dab-config.MySql.json index a0b36b0388..c08069a5c8 100644 --- a/src/Service.Tests/dab-config.MySql.json +++ b/src/Service.Tests/dab-config.MySql.json @@ -24,7 +24,7 @@ "allow-credentials": false }, "authentication": { - "provider": "StaticWebApps" + "provider": "AppService" }, "mode": "development" } diff --git a/src/Service.Tests/dab-config.PostgreSql.json b/src/Service.Tests/dab-config.PostgreSql.json index e9a8ddf5ab..48f9700754 100644 --- a/src/Service.Tests/dab-config.PostgreSql.json +++ b/src/Service.Tests/dab-config.PostgreSql.json @@ -24,7 +24,7 @@ "allow-credentials": false }, "authentication": { - "provider": "StaticWebApps" + "provider": "AppService" }, "mode": "development" } diff --git a/src/Service/Program.cs b/src/Service/Program.cs index 1059fd52ff..e23fb98cd9 100644 --- a/src/Service/Program.cs +++ b/src/Service/Program.cs @@ -5,16 +5,19 @@ using System.CommandLine; using System.CommandLine.Parsing; using System.Runtime.InteropServices; +using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; using Azure.DataApiBuilder.Config; using Azure.DataApiBuilder.Service.Exceptions; using Azure.DataApiBuilder.Service.Telemetry; +using Azure.DataApiBuilder.Service.Utilities; using Microsoft.ApplicationInsights; using Microsoft.AspNetCore; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.ApplicationInsights; @@ -33,6 +36,14 @@ public class Program public static void Main(string[] args) { + bool runMcpStdio = McpStdioHelper.ShouldRunMcpStdio(args, out string? mcpRole); + + if (runMcpStdio) + { + Console.OutputEncoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false); + Console.InputEncoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false); + } + if (!ValidateAspNetCoreUrls()) { Console.Error.WriteLine("Invalid ASPNETCORE_URLS format. e.g.: ASPNETCORE_URLS=\"http://localhost:5000;https://localhost:5001\""); @@ -40,20 +51,26 @@ public static void Main(string[] args) return; } - if (!StartEngine(args)) + if (!StartEngine(args, runMcpStdio, mcpRole)) { Environment.ExitCode = -1; } } - public static bool StartEngine(string[] args) + public static bool StartEngine(string[] args, bool runMcpStdio, string? mcpRole) { - // Unable to use ILogger because this code is invoked before LoggerFactory - // is instantiated. Console.WriteLine("Starting the runtime engine..."); try { - CreateHostBuilder(args).Build().Run(); + IHost host = CreateHostBuilder(args, runMcpStdio, mcpRole).Build(); + + if (runMcpStdio) + { + return McpStdioHelper.RunMcpStdioHost(host); + } + + // Normal web mode + host.Run(); return true; } // Catch exception raised by explicit call to IHostApplicationLifetime.StopApplication() @@ -72,17 +89,28 @@ public static bool StartEngine(string[] args) } } - public static IHostBuilder CreateHostBuilder(string[] args) + // Compatibility overload used by external callers that do not pass the runMcpStdio flag. + public static bool StartEngine(string[] args) + { + bool runMcpStdio = McpStdioHelper.ShouldRunMcpStdio(args, out string? mcpRole); + return StartEngine(args, runMcpStdio, mcpRole: mcpRole); + } + + public static IHostBuilder CreateHostBuilder(string[] args, bool runMcpStdio, string? mcpRole) { return Host.CreateDefaultBuilder(args) .ConfigureAppConfiguration(builder => { AddConfigurationProviders(builder, args); + if (runMcpStdio) + { + McpStdioHelper.ConfigureMcpStdio(builder, mcpRole); + } }) .ConfigureWebHostDefaults(webBuilder => { Startup.MinimumLogLevel = GetLogLevelFromCommandLineArgs(args, out Startup.IsLogLevelOverriddenByCli); - ILoggerFactory loggerFactory = GetLoggerFactoryForLogLevel(Startup.MinimumLogLevel); + ILoggerFactory loggerFactory = GetLoggerFactoryForLogLevel(Startup.MinimumLogLevel, stdio: runMcpStdio); ILogger startupLogger = loggerFactory.CreateLogger(); DisableHttpsRedirectionIfNeeded(args); webBuilder.UseStartup(builder => new Startup(builder.Configuration, startupLogger)); @@ -140,7 +168,14 @@ private static ParseResult GetParseResult(Command cmd, string[] args) /// Telemetry client /// Hot-reloadable log level /// Core Serilog logging pipeline - public static ILoggerFactory GetLoggerFactoryForLogLevel(LogLevel logLevel, TelemetryClient? appTelemetryClient = null, LogLevelInitializer? logLevelInitializer = null, Logger? serilogLogger = null) + /// Whether the logger is for stdio mode + /// ILoggerFactory + public static ILoggerFactory GetLoggerFactoryForLogLevel( + LogLevel logLevel, + TelemetryClient? appTelemetryClient = null, + LogLevelInitializer? logLevelInitializer = null, + Logger? serilogLogger = null, + bool stdio = false) { return LoggerFactory .Create(builder => @@ -229,7 +264,19 @@ public static ILoggerFactory GetLoggerFactoryForLogLevel(LogLevel logLevel, Tele } } - builder.AddConsole(); + // In stdio mode, route console logs to STDERR to keep STDOUT clean for MCP JSON + if (stdio) + { + builder.ClearProviders(); + builder.AddConsole(options => + { + options.LogToStandardErrorThreshold = LogLevel.Trace; + }); + } + else + { + builder.AddConsole(); + } }); } diff --git a/src/Service/Startup.cs b/src/Service/Startup.cs index 48a39d31d0..333bf57234 100644 --- a/src/Service/Startup.cs +++ b/src/Service/Startup.cs @@ -348,7 +348,16 @@ public void ConfigureServices(IServiceCollection services) return handler; }); - if (runtimeConfig is not null && runtimeConfig.Runtime?.Host?.Mode is HostMode.Development) + bool isMcpStdio = Configuration.GetValue("MCP:StdioMode"); + + if (isMcpStdio) + { + // Explicitly force Simulator when running in MCP stdio mode. + services.AddAuthentication( + defaultScheme: SimulatorAuthenticationDefaults.AUTHENTICATIONSCHEME) + .AddSimulatorAuthentication(); + } + else if (runtimeConfig is not null && runtimeConfig.Runtime?.Host?.Mode is HostMode.Development) { // Development mode implies support for "Hot Reload". The V2 authentication function // wires up all DAB supported authentication providers (schemes) so that at request time, @@ -456,6 +465,8 @@ public void ConfigureServices(IServiceCollection services) services.AddDabMcpServer(configProvider); + services.AddSingleton(); + services.AddControllers(); } @@ -785,21 +796,18 @@ private void ConfigureAuthentication(IServiceCollection services, RuntimeConfigP if (easyAuthType == EasyAuthType.AppService && !appServiceEnvironmentDetected) { - if (isProductionMode) - { - throw new DataApiBuilderException( - message: AppServiceAuthenticationInfo.APPSERVICE_PROD_MISSING_ENV_CONFIG, - statusCode: System.Net.HttpStatusCode.ServiceUnavailable, - subStatusCode: DataApiBuilderException.SubStatusCodes.ErrorInInitialization); - } - else - { - _logger.LogWarning(AppServiceAuthenticationInfo.APPSERVICE_DEV_MISSING_ENV_CONFIG); - } + _logger.LogWarning(AppServiceAuthenticationInfo.APPSERVICE_DEV_MISSING_ENV_CONFIG); } - services.AddAuthentication(EasyAuthAuthenticationDefaults.AUTHENTICATIONSCHEME) - .AddEasyAuthAuthentication(easyAuthAuthenticationProvider: easyAuthType); + string defaultScheme = easyAuthType == EasyAuthType.AppService + ? EasyAuthAuthenticationDefaults.APPSERVICEAUTHSCHEME + : EasyAuthAuthenticationDefaults.SWAAUTHSCHEME; + + services.AddAuthentication(defaultScheme) + .AddEnvDetectedEasyAuth(); + + _logger.LogInformation("Registered EasyAuth scheme: {Scheme}", defaultScheme); + } else if (mode == HostMode.Development && authOptions.IsAuthenticationSimulatorEnabled()) { @@ -820,7 +828,7 @@ private void ConfigureAuthentication(IServiceCollection services, RuntimeConfigP { // Sets EasyAuth as the default authentication scheme when runtime configuration // is not present. - SetStaticWebAppsAuthentication(services); + SetAppServiceAuthentication(services); } } @@ -1012,13 +1020,13 @@ private void ConfigureFileSink(IApplicationBuilder app, RuntimeConfig runtimeCon } /// - /// Sets Static Web Apps EasyAuth as the authentication scheme for the engine. + /// Sets App Service EasyAuth as the authentication scheme for the engine. /// /// The service collection where authentication services are added. - private static void SetStaticWebAppsAuthentication(IServiceCollection services) + private static void SetAppServiceAuthentication(IServiceCollection services) { - services.AddAuthentication(EasyAuthAuthenticationDefaults.AUTHENTICATIONSCHEME) - .AddEasyAuthAuthentication(EasyAuthType.StaticWebApps); + services.AddAuthentication(EasyAuthAuthenticationDefaults.APPSERVICEAUTHSCHEME) + .AddEnvDetectedEasyAuth(); } /// diff --git a/src/Service/Utilities/McpStdioHelper.cs b/src/Service/Utilities/McpStdioHelper.cs new file mode 100644 index 0000000000..043e9dd85d --- /dev/null +++ b/src/Service/Utilities/McpStdioHelper.cs @@ -0,0 +1,102 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace Azure.DataApiBuilder.Service.Utilities +{ + /// + /// Helper methods for configuring and running MCP in stdio mode. + /// + internal static class McpStdioHelper + { + /// + /// Determines if MCP stdio mode should be run based on command line arguments. + /// + /// The command line arguments. + /// The role for MCP stdio mode. When this method returns true, the role defaults to anonymous. + /// True when MCP stdio mode should be enabled; otherwise false. + public static bool ShouldRunMcpStdio(string[] args, [NotNullWhen(true)] out string? mcpRole) + { + mcpRole = null; + + bool runMcpStdio = Array.Exists( + args, + a => string.Equals(a, "--mcp-stdio", StringComparison.OrdinalIgnoreCase)); + + if (!runMcpStdio) + { + return false; + } + + string? roleArg = Array.Find( + args, + a => a != null && a.StartsWith("role:", StringComparison.OrdinalIgnoreCase)); + + if (!string.IsNullOrEmpty(roleArg)) + { + string roleValue = roleArg[(roleArg.IndexOf(':') + 1)..]; + if (!string.IsNullOrWhiteSpace(roleValue)) + { + mcpRole = roleValue; + } + } + + // Ensure that when MCP stdio is enabled, mcpRole is always non-null. + // This matches the NotNullWhen(true) contract and avoids nullable warnings + // for callers while still allowing an implicit default when no role is provided. + mcpRole ??= "anonymous"; + + return true; + } + + /// + /// Configures the IConfigurationBuilder for MCP stdio mode. + /// + /// + /// + public static void ConfigureMcpStdio(IConfigurationBuilder builder, string? mcpRole) + { + builder.AddInMemoryCollection(new Dictionary + { + ["MCP:StdioMode"] = "true", + ["MCP:Role"] = mcpRole ?? "anonymous", + ["Runtime:Host:Authentication:Provider"] = "Simulator" + }); + } + + /// + /// Runs the MCP stdio host. + /// + /// The host to run. + public static bool RunMcpStdioHost(IHost host) + { + host.Start(); + + Mcp.Core.McpToolRegistry registry = + host.Services.GetRequiredService(); + IEnumerable tools = + host.Services.GetServices(); + + foreach (Mcp.Model.IMcpTool tool in tools) + { + registry.RegisterTool(tool); + } + + IHostApplicationLifetime lifetime = + host.Services.GetRequiredService(); + Mcp.Core.IMcpStdioServer stdio = + host.Services.GetRequiredService(); + + stdio.RunAsync(lifetime.ApplicationStopping).GetAwaiter().GetResult(); + host.StopAsync().GetAwaiter().GetResult(); + + return true; + } + } +}