From d6d0f832dea4712d75de1d30c93ee5f642776048 Mon Sep 17 00:00:00 2001 From: Jerry Nixon <1749983+JerryNixon@users.noreply.github.com> Date: Sun, 26 Oct 2025 21:53:01 -0600 Subject: [PATCH 01/36] Updating the readme for clarity (pre-MCP) (#2918) This pull request significantly improves the `README.md` by restructuring and expanding the documentation. --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- README.md | 388 ++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 244 insertions(+), 144 deletions(-) diff --git a/README.md b/README.md index 1746a62482..d8edb22a50 100644 --- a/README.md +++ b/README.md @@ -7,9 +7,9 @@ [What's new?](https://learn.microsoft.com/azure/data-api-builder/whats-new) -## Community +## Join the community -Join the Data API builder community! This sign up will help us maintain a list of interested developers to be part of our roadmap and to help us better understand the different ways DAB is being used. Sign up [here](https://forms.office.com/pages/responsepage.aspx?id=v4j5cvGGr0GRqy180BHbR1S1JdzGAxhDrefV-tBYtwZUNE1RWVo0SUVMTkRESUZLMVVOS0wwUFNVRy4u). +Want to be part of our priorities and roadmap? Sign up [here](https://forms.office.com/pages/responsepage.aspx?id=v4j5cvGGr0GRqy180BHbR1S1JdzGAxhDrefV-tBYtwZUNE1RWVo0SUVMTkRESUZLMVVOS0wwUFNVRy4u). ![](docs/media/dab-logo.png) @@ -17,199 +17,299 @@ Join the Data API builder community! This sign up will help us maintain a list o Data API builder (DAB) is an open-source, no-code tool that creates secure, full-featured REST and GraphQL endpoints for your database. It’s a CRUD data API engine that runs in a container—on Azure, any other cloud, or on-premises. DAB is built for developers with integrated tooling, telemetry, and other productivity features. -```mermaid -erDiagram - DATA_API_BUILDER ||--|{ DATA_API : "Provides" - DATA_API_BUILDER { - container true "Microsoft Container Repository" - open-source true "MIT license / any cloud or on-prem." - objects true "Supports: Table / View / Stored Procedure" - developer true "Swagger / Nitro (fka Banana Cake Pop)" - otel true "Open Telemetry / Structured Logs / Health Endpoints" - security true "EntraId / EasyAuth / OAuth / JWT / Anonymous" - cache true "Level1 (in-memory) / Level2 (redis)" - policy true "Item policy / Database policy / Claims policy" - hot_reload true "Dynamically controllable log levels" - } - DATA_API ||--o{ DATASOURCE : "Queries" - DATA_API { - REST true "$select / $filter / $orderby" - GraphQL true "relationships / multiple mutations" - } - DATASOURCE { - MS_SQL Supported - PostgreSQL Supported - Cosmos_DB Supported - MySQL Supported - SQL_DW Supported - } - CLIENT ||--o{ DATA_API : "Consumes" - CLIENT { - Transport HTTP "HTTP / HTTPS" - Syntax JSON "Standard payloads" - Mobile Supported "No requirement" - Web Supported "No requirement" - Desktop Supported "No requirement" - Language Any "No requirement" - Framework None "Not required" - Library None "Not required" - ORM None "Not required" - Driver None "Not required" - } -``` +> [!IMPORTANT] +> Data API builder (DAB) is open source and always free. + +### Which databases does Data API builder support? + +| | Azure SQL | SQL Server | SQLDW | Cosmos DB | PostgreSQL | MySQL | +| :-----------: | :-------: | :--------: | :---: | :-------: | :--------: | :---: | +| **Supported** | Yes | Yes | Yes | Yes | Yes | Yes | + +### Which environments does Data API builder support? + +| | On-Prem | Azure | AWS | GCP | Other | +| :-----------: | :-----: | :---: | :--: | :--: | :---: | +| **Supported** | Yes | Yes | Yes | Yes | Yes | + +### Which endpoints does Data API builder support? + +| | REST | GraphQL | MCP | +| :-----------: | :--: | :-----: | :---------: | +| **Supported** | Yes | Yes | Coming soon | + +## Getting started + +Use the [Getting Started](https://learn.microsoft.com/azure/data-api-builder/get-started/get-started-with-data-api-builder) tutorial to quickly explore the core tools and concepts. -## Getting Started +### 1. Install the `dotnet` [command line](https://get.dot.net) -Use the [Getting Started](https://learn.microsoft.com/azure/data-api-builder/get-started/get-started-with-data-api-builder) tutorial to quickly explore the core tools and concepts. It gives you hands-on experience with how DAB makes you more efficient by removing boilerplate code. +https://get.dot.net -**1. Install the DAB CLI** +> [!NOTE] +> You may already have .NET installed! -The [DAB CLI](https://aka.ms/dab/docs) is a cross-platform .NET tool. Install the [.NET SDK](https://get.dot.net) before running: +The Data API builder (DAB) command line requires the .NET runtime version 8 or later. +#### Validate your installation + +```sh +dotnet --version ``` + +### 2. Install the `dab` command line + +The Data API builder (DAB) command line is cross-platform and intended for local developer use. + +```sh dotnet tool install microsoft.dataapibuilder -g ``` -**2. Create your initial configuration file** +#### Validate your installation + +```sh +dab --version +``` + +### 3. Create your database (example: Azure SQL database / T-SQL) + +This example uses a single table for simplicity. + +```sql +CREATE TABLE dbo.Todo +( + Id INT PRIMARY KEY IDENTITY, + Title NVARCHAR(500) NOT NULL, + IsCompleted BIT NOT NULL DEFAULT 0 +); +INSERT dbo.Todo (Title, IsCompleted) +VALUES + ('Walk the dog', 0), + ('Feed the fish', 0), + ('Clean the cat', 1); +``` + +### 4. Prepare your connection string -DAB requires a JSON configuration file. Edit manually or with the CLI. Use `dab --help` for syntax options. +Data API builder (DAB) supports `.env` files for testing process-level environment variables. +#### PowerShell (Windows) + +```ps +echo "my-connection-string=$env:database_connection_string" > .env ``` + +#### cmd.exe (Windows) + +```cmd +echo my-connection-string=%database_connection_string% > .env +``` + +#### bash (macOS/Linux) + +```bash +echo "my-connection-string=$database_connection_string" > .env +``` + +#### Resulting .env file + +The file `.env` is automatically created through this process. These are the resulting contents: + +``` +"my-connection-string=$env:database_connection_string" +``` +> [!NOTE] +> Be sure and replace `database_connection_string` with your actual database connection string. + +> [!IMPORTANT] +> Adding `.env` to your `.gitignore` file will help ensure your secrets are not added to source control. + +### 5. Create your initial configuration file + +Data API builder (DAB) requires a JSON configuration file. Use `dab --help` for syntax options. + +```sh dab init --database-type mssql --connection-string "@env('my-connection-string')" --host-mode development ``` -**3. Add your first table** +> [!NOTE] +> Including `--host-mode development` enables Swagger for REST and Nitro for GraphQL. -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. +#### Resulting configuration +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": "StaticWebApps" + }, + "mode": "development" + } + }, + "entities": { } +} ``` -dab add Actor - --source "dbo.Actor" +### 6. Add your table to the configuration + +```sh +dab add Todo + --source "dbo.Todo" --permissions "anonymous:*" ``` -**4. Run Data API builder** +> [!NOTE] +> DAB supports tables, views, and stored procedures. When the type is not specified, the default is `table`. + +#### Resulting configuration + +The `entities` section of the configuration is no longer empty: + +```json +{ + "entities": { + "Todo": { + "source": { + "object": "dbo.Todo", + "type": "table" + }, + "graphql": { + "enabled": true, + "type": { + "singular": "Todo", + "plural": "Todos" + } + }, + "rest": { + "enabled": true + }, + "permissions": [ + { + "role": "anonymous", + "actions": [ + { + "action": "*" + } + ] + } + ] + } + } +} +``` -In `production`, DAB runs in a container. In `development`, it’s self-hosted locally with hot reload, Swagger, and Nitro (fka Banana Cake Pop) support. +### 7. Run Data API builder -``` +In `production`, DAB runs in a container. In `development`, it’s locally self-hosted. + +```sh dab start ``` -> **Note**: Before you run `dab start`, make sure your connection string is stored in an environment variable called `my-connection-string`. This is required for `@env('my-connection-string')` in your config file to work. The easiest way is to create a `.env` file with `name=value` pairs—DAB will load these automatically at runtime. +> [!IMPORTANT] +> The DAB CLI assumes your configuration file is called `dab-config.json` and is in the local folder. -**5. Access your data source** +### 8. Access your data! -By default, DAB enables both REST and GraphQL. REST supports `$select`, `$filter`, and `$orderBy`. GraphQL uses config-defined relationships. +By default, DAB enables both REST and GraphQL. +```sh +GET http://localhost:5000/api/Todo ``` -GET http://localhost:5000/api/Actor -``` - -### Walk-through video - - Play Video - +> [!NOTE] +> Change the URL to match your port if it is different. -Demo source code: [startrek](https://aka.ms/dab/startrek) +#### Other things you should try -## Overview - -| Category | Features | -|----------------|----------| -| **Database Objects** | • NoSQL collections
• RDBMS tables, views, stored procedures | -| **Data Sources** | • SQL Server & Azure SQL
• Azure Cosmos DB
• PostgreSQL
• MySQL | -| **REST** | • `$select` for projection
• `$filter` for filtering
• `$orderBy` for sorting | -| **GraphQL** | • Relationship navigation
• Data aggregation
• Multiple mutations | -| **Telemetry** | • Structured logs
• OpenTelemetry
• Application Insights
• Health endpoints | -| **Advanced** | • Pagination
• Level 1 (in-memory) cache | -| **Authentication** | • OAuth2/JWT
• EasyAuth
• Entra ID | -| **Authorization** | • Role-based support
• Entity permissions
• Database policies | -| **Developer** | • Cross-platform CLI
• Swagger (REST)
• Nitro [previously Banana Cake Pop] (GraphQL)
• Open Source
• Configuration Hot Reload | +* DAB’s Health endpoint: `http://localhost:5000/health` +* DAB’s Swagger UI: `http://localhost:5000/swagger` +* DAB’s Nitro UI: `http://localhost:5000/graphql` ## How does it work? -This diagram shows how DAB works. DAB dynamically creates endpoints from your config file. It translates HTTP requests to SQL, returns JSON, and auto-pages results. +DAB dynamically creates endpoints and translates requests to SQL, returning JSON. ```mermaid sequenceDiagram - actor Client - - box Data API builder (DAB) - participant Endpoint - participant QueryBuilder - end - - participant Configuration as Configuration File - - box Data Source - participant DB - end - - Endpoint->>Endpoint: Start - activate Endpoint - Endpoint->>Configuration: Request - Configuration-->>Endpoint: Configuration - Endpoint->>DB: Request - DB-->>Endpoint: Metadata - Note over Endpoint, DB: Some configuration is validated against the metadata - Endpoint-->>Endpoint: Configure - deactivate Endpoint - Client-->>Endpoint: HTTP Request - activate Endpoint - critical - Endpoint-->>Endpoint: Authenticate - Endpoint-->>Endpoint: Authorize - end - Endpoint->>QueryBuilder: Request - QueryBuilder-->>Endpoint: SQL - alt Cache - Endpoint-->>Endpoint: Use Cache - else Query - Endpoint-->>DB: Request - Note over Endpoint, DB: Query is automatically throttled and results paginated - DB->>Endpoint: Results - Note over Endpoint, DB: Results are automatically cached for use in next request - end - Endpoint->>Client: HTTP 200 - deactivate Endpoint -``` - -Because DAB is stateless, it can scale up or out using any container size. It builds a feature-rich API like you would from scratch—but now you don’t have to. - -## Additional Resources - -- [Online Documentation](https://aka.ms/dab/docs) -- [Official Samples](https://aka.ms/dab/samples) -- [Known Issues](https://learn.microsoft.com/azure/data-api-builder/known-issues) -- [Feature Roadmap](https://github.com/Azure/data-api-builder/discussions/1377) + actor Client as Client + participant Endpoint as Endpoint + participant QueryBuilder as QueryBuilder + participant DB as Database + + %% Initialization / Warming up section (light grey) + rect rgba(120,120,120,0.10) + Endpoint -->>+ Endpoint: Read Config + Endpoint ->> DB: Query Metadata + DB -->> Endpoint: Metadata Response + Endpoint ->>- Endpoint: Start Engine + end + + %% Request/Response section (very light purple) + rect rgba(180,150,255,0.11) + Client ->>+ Endpoint: HTTP Request + Endpoint ->> Endpoint: Authorize + Endpoint ->> QueryBuilder: Invoke + QueryBuilder -->> Endpoint: SQL Query + Endpoint ->> DB: Submit Query + DB -->> Endpoint: Data Response + Endpoint -->>- Client: HTTP Response + end +``` -#### References +## Additional resources -- [Microsoft REST API Guidelines](https://github.com/microsoft/api-guidelines/blob/vNext/Guidelines.md) -- [Microsoft Azure REST API Guidelines](https://github.com/microsoft/api-guidelines/blob/vNext/azure/Guidelines.md) -- [GraphQL Specification](https://graphql.org/) +* [Online Documentation](https://aka.ms/dab/docs) +* [Official Samples](https://aka.ms/dab/samples) +* [Known Issues](https://learn.microsoft.com/azure/data-api-builder/known-issues) +* [Feature Roadmap](https://github.com/Azure/data-api-builder/discussions/1377) -### How to Contribute +#### References -To contribute, see these documents: +* [Microsoft REST API Guidelines](https://github.com/microsoft/api-guidelines/blob/vNext/Guidelines.md) +* [Microsoft Azure REST API Guidelines](https://github.com/microsoft/api-guidelines/blob/vNext/azure/Guidelines.md) +* [GraphQL Specification](https://graphql.org/) -- [Code of Conduct](./CODE_OF_CONDUCT.md) -- [Security](./SECURITY.md) -- [Contributing](./CONTRIBUTING.md) +### How to contribute -### License +To contribute, see these documents: -**Data API builder for Azure Databases** is licensed under the MIT License. See [LICENSE](./LICENSE.txt) for details. +* [Code of Conduct](./CODE_OF_CONDUCT.md) +* [Security](./SECURITY.md) +* [Contributing](./CONTRIBUTING.md) +* [MIT License](./LICENSE.txt) -### Third-Party Component Notice +### Third-party component notice -Nitro (fka Banana Cake Pop by ChilliCream, Inc.) may optionally store work in its cloud service via your ChilliCream account. Microsoft is not affiliated with or endorsing this service. Use at your discretion. +Nitro (formerly Banana Cake Pop by ChilliCream, Inc.) may optionally store work in its cloud service via your ChilliCream account. Microsoft is not affiliated with or endorsing this service. Use at your discretion. ### Trademarks -This project may use trademarks or logos. Use of Microsoft trademarks must follow Microsoft’s [Trademark & Brand Guidelines](https://www.microsoft.com/en-us/legal/intellectualproperty/trademarks). Use of third-party marks is subject to their policies. +This project may use trademarks or logos. Use of Microsoft trademarks must follow Microsoft’s [Trademark & Brand Guidelines](https://www.microsoft.com/en-us/legal/intellectualproperty/trademarks). Use of third-party marks is subject to their policies. \ No newline at end of file From 69b0b17392a8ce2c93aa6044f575946f5721527b Mon Sep 17 00:00:00 2001 From: Giovanna Ribeiro <44936262+gilemos@users.noreply.github.com> Date: Wed, 29 Oct 2025 19:17:55 -0700 Subject: [PATCH 02/36] Fix for stored procedure empty cell bug (#2887) ## Why make this change? We have recently noticed that, if we have a column of type NVARCHAR or VARCHAR and we try to run a stored procedure that reads a row in which that column has an empty string value, we had an internal server error. This error happens when we try to run the method GetChars passing in a buffer with length 0 This PR aims to fix this problem ## What is this change? We have added a small change to the method that was throwing the exception. If we find that resultFieldSize is 0 - which means that the data in the cell we are reading has a length of 0 - we will not call the method GetChars to read the data, but assume the data is empty and return the size of the data read in bytes as 0. As you can see in the example bellow, that fixes the issue. ## How was this tested? - [x] Integration Tests - [x] Unit Tests ## Sample Request(s) We have a table with a column of type NVARCHAR called "Description". In one of the rows, Description is an empty string image **Before the changes:** If we try to run a stored procedure that reads that empty cell, we get an error image **After changes** Stored procedure runs as expected image --------- Co-authored-by: Giovanna Ribeiro Co-authored-by: RubenCerna2079 <32799214+RubenCerna2079@users.noreply.github.com> --- src/Core/Resolvers/QueryExecutor.cs | 13 ++ src/Service.Tests/DatabaseSchema-DwSql.sql | 3 +- src/Service.Tests/DatabaseSchema-MsSql.sql | 3 +- src/Service.Tests/DatabaseSchema-MySql.sql | 3 +- .../DatabaseSchema-PostgreSql.sql | 3 +- .../GraphQLMutationTestBase.cs | 4 +- .../GraphQLPaginationTestBase.cs | 191 +++++++++--------- .../MsSqlGraphQLQueryTests.cs | 16 ++ .../RestApiTests/Delete/DwSqlDeleteApiTest.cs | 2 +- .../RestApiTests/Delete/MsSqlDeleteApiTest.cs | 2 +- .../UnitTests/SqlQueryExecutorUnitTests.cs | 45 +++++ 11 files changed, 187 insertions(+), 98 deletions(-) diff --git a/src/Core/Resolvers/QueryExecutor.cs b/src/Core/Resolvers/QueryExecutor.cs index 908c1bb1e8..97e2f7e8d4 100644 --- a/src/Core/Resolvers/QueryExecutor.cs +++ b/src/Core/Resolvers/QueryExecutor.cs @@ -740,6 +740,12 @@ internal int StreamCharData(DbDataReader dbDataReader, long availableSize, Strin // else we throw exception. ValidateSize(availableSize, resultFieldSize); + // If the cell is empty, don't append anything to the resultJsonString and return 0. + if (resultFieldSize == 0) + { + return 0; + } + char[] buffer = new char[resultFieldSize]; // read entire field into buffer and reduce available size. @@ -766,6 +772,13 @@ internal int StreamByteData(DbDataReader dbDataReader, long availableSize, int o // else we throw exception. ValidateSize(availableSize, resultFieldSize); + // If the cell is empty, set resultBytes to an empty array and return 0. + if (resultFieldSize == 0) + { + resultBytes = Array.Empty(); + return 0; + } + resultBytes = new byte[resultFieldSize]; dbDataReader.GetBytes(ordinal: ordinal, dataOffset: 0, buffer: resultBytes, bufferOffset: 0, length: resultBytes.Length); diff --git a/src/Service.Tests/DatabaseSchema-DwSql.sql b/src/Service.Tests/DatabaseSchema-DwSql.sql index 300ef7ff32..daed665949 100644 --- a/src/Service.Tests/DatabaseSchema-DwSql.sql +++ b/src/Service.Tests/DatabaseSchema-DwSql.sql @@ -336,7 +336,8 @@ VALUES (1, 'Awesome book', 1234), (17, 'CONN%_CONN', 1234), (18, '[Special Book]', 1234), (19, 'ME\YOU', 1234), -(20, 'C:\\LIFE', 1234); +(20, 'C:\\LIFE', 1234), +(21, '', 1234); INSERT INTO book_website_placements(id, book_id, price) VALUES (1, 1, 100), (2, 2, 50), (3, 3, 23), (4, 5, 33); diff --git a/src/Service.Tests/DatabaseSchema-MsSql.sql b/src/Service.Tests/DatabaseSchema-MsSql.sql index 3605b2628a..4e87394aee 100644 --- a/src/Service.Tests/DatabaseSchema-MsSql.sql +++ b/src/Service.Tests/DatabaseSchema-MsSql.sql @@ -531,7 +531,8 @@ VALUES (1, 'Awesome book', 1234), (17, 'CONN%_CONN', 1234), (18, '[Special Book]', 1234), (19, 'ME\YOU', 1234), -(20, 'C:\\LIFE', 1234); +(20, 'C:\\LIFE', 1234), +(21, '', 1234); SET IDENTITY_INSERT books OFF SET IDENTITY_INSERT books_mm ON diff --git a/src/Service.Tests/DatabaseSchema-MySql.sql b/src/Service.Tests/DatabaseSchema-MySql.sql index f746bc063a..dda93d86d1 100644 --- a/src/Service.Tests/DatabaseSchema-MySql.sql +++ b/src/Service.Tests/DatabaseSchema-MySql.sql @@ -388,7 +388,8 @@ INSERT INTO books(id, title, publisher_id) (17, 'CONN%_CONN', 1234), (18, '[Special Book]', 1234), (19, 'ME\\YOU', 1234), - (20, 'C:\\\\LIFE', 1234); + (20, 'C:\\\\LIFE', 1234), + (21, '', 1234); INSERT INTO book_website_placements(book_id, price) VALUES (1, 100), (2, 50), (3, 23), (5, 33); INSERT INTO website_users(id, username) VALUES (1, 'George'), (2, NULL), (3, ''), (4, 'book_lover_95'), (5, 'null'); INSERT INTO book_author_link(book_id, author_id) VALUES (1, 123), (2, 124), (3, 123), (3, 124), (4, 123), (4, 124), (5, 126); diff --git a/src/Service.Tests/DatabaseSchema-PostgreSql.sql b/src/Service.Tests/DatabaseSchema-PostgreSql.sql index 14615707b1..523e96c22f 100644 --- a/src/Service.Tests/DatabaseSchema-PostgreSql.sql +++ b/src/Service.Tests/DatabaseSchema-PostgreSql.sql @@ -391,7 +391,8 @@ INSERT INTO books(id, title, publisher_id) (17, 'CONN%_CONN', 1234), (18, '[Special Book]', 1234), (19, 'ME\YOU', 1234), - (20, 'C:\\LIFE', 1234); + (20, 'C:\\LIFE', 1234), + (21, '', 1234); INSERT INTO book_website_placements(book_id, price) VALUES (1, 100), (2, 50), (3, 23), (5, 33); INSERT INTO website_users(id, username) VALUES (1, 'George'), (2, NULL), (3, ''), (4, 'book_lover_95'), (5, 'null'); INSERT INTO book_author_link(book_id, author_id) VALUES (1, 123), (2, 124), (3, 123), (3, 124), (4, 123), (4, 124), (5, 126);; diff --git a/src/Service.Tests/SqlTests/GraphQLMutationTests/GraphQLMutationTestBase.cs b/src/Service.Tests/SqlTests/GraphQLMutationTests/GraphQLMutationTestBase.cs index c9207a6672..745d5eade3 100644 --- a/src/Service.Tests/SqlTests/GraphQLMutationTests/GraphQLMutationTestBase.cs +++ b/src/Service.Tests/SqlTests/GraphQLMutationTests/GraphQLMutationTestBase.cs @@ -257,7 +257,7 @@ public async Task TestStoredProcedureMutationForDeletion(string dbQueryToVerifyD string currentDbResponse = await GetDatabaseResultAsync(dbQueryToVerifyDeletion); JsonDocument currentResult = JsonDocument.Parse(currentDbResponse); - Assert.AreEqual(currentResult.RootElement.GetProperty("maxId").GetInt64(), 20); + Assert.AreEqual(currentResult.RootElement.GetProperty("maxId").GetInt64(), 21); JsonElement graphQLResponse = await ExecuteGraphQLRequestAsync(graphQLMutation, graphQLMutationName, isAuthenticated: true); // Stored Procedure didn't return anything @@ -266,7 +266,7 @@ public async Task TestStoredProcedureMutationForDeletion(string dbQueryToVerifyD // check to verify new element is inserted string updatedDbResponse = await GetDatabaseResultAsync(dbQueryToVerifyDeletion); JsonDocument updatedResult = JsonDocument.Parse(updatedDbResponse); - Assert.AreEqual(updatedResult.RootElement.GetProperty("maxId").GetInt64(), 19); + Assert.AreEqual(updatedResult.RootElement.GetProperty("maxId").GetInt64(), 20); } public async Task InsertMutationOnTableWithTriggerWithNonAutoGenPK(string dbQuery) diff --git a/src/Service.Tests/SqlTests/GraphQLPaginationTests/GraphQLPaginationTestBase.cs b/src/Service.Tests/SqlTests/GraphQLPaginationTests/GraphQLPaginationTestBase.cs index e7e18d6090..33db8f8b49 100644 --- a/src/Service.Tests/SqlTests/GraphQLPaginationTests/GraphQLPaginationTestBase.cs +++ b/src/Service.Tests/SqlTests/GraphQLPaginationTests/GraphQLPaginationTestBase.cs @@ -84,95 +84,101 @@ public async Task RequestMaxUsingNegativeOne() } }"; - // this resultset represents all books in the db. - JsonElement actual = await ExecuteGraphQLRequestAsync(graphQLQuery, graphQLQueryName, isAuthenticated: false); string expected = @"{ - ""items"": [ + ""items"": [ { - ""id"": 1, - ""title"": ""Awesome book"" + ""id"": 1, + ""title"": ""Awesome book"" }, { - ""id"": 2, - ""title"": ""Also Awesome book"" + ""id"": 2, + ""title"": ""Also Awesome book"" }, { - ""id"": 3, - ""title"": ""Great wall of china explained"" + ""id"": 3, + ""title"": ""Great wall of china explained"" }, { - ""id"": 4, - ""title"": ""US history in a nutshell"" + ""id"": 4, + ""title"": ""US history in a nutshell"" }, { - ""id"": 5, - ""title"": ""Chernobyl Diaries"" + ""id"": 5, + ""title"": ""Chernobyl Diaries"" }, { - ""id"": 6, - ""title"": ""The Palace Door"" + ""id"": 6, + ""title"": ""The Palace Door"" }, { - ""id"": 7, - ""title"": ""The Groovy Bar"" + ""id"": 7, + ""title"": ""The Groovy Bar"" }, { - ""id"": 8, - ""title"": ""Time to Eat"" + ""id"": 8, + ""title"": ""Time to Eat"" }, { - ""id"": 9, - ""title"": ""Policy-Test-01"" + ""id"": 9, + ""title"": ""Policy-Test-01"" }, { - ""id"": 10, - ""title"": ""Policy-Test-02"" + ""id"": 10, + ""title"": ""Policy-Test-02"" }, { - ""id"": 11, - ""title"": ""Policy-Test-04"" + ""id"": 11, + ""title"": ""Policy-Test-04"" }, { - ""id"": 12, - ""title"": ""Time to Eat 2"" + ""id"": 12, + ""title"": ""Time to Eat 2"" + }, + { + ""id"": 13, + ""title"": ""Before Sunrise"" }, { - ""id"": 13, - ""title"": ""Before Sunrise"" + ""id"": 14, + ""title"": ""Before Sunset"" }, { - ""id"": 14, - ""title"": ""Before Sunset"" + ""id"": 15, + ""title"": ""SQL_CONN"" }, { - ""id"": 15, - ""title"": ""SQL_CONN"" + ""id"": 16, + ""title"": ""SOME%CONN"" }, { - ""id"": 16, - ""title"": ""SOME%CONN"" + ""id"": 17, + ""title"": ""CONN%_CONN"" }, { - ""id"": 17, - ""title"": ""CONN%_CONN"" + ""id"": 18, + ""title"": ""[Special Book]"" }, { - ""id"": 18, - ""title"": ""[Special Book]"" + ""id"": 19, + ""title"": ""ME\\YOU"" }, { - ""id"": 19, - ""title"": ""ME\\YOU"" + ""id"": 20, + ""title"": ""C:\\\\LIFE"" }, { - ""id"": 20, - ""title"": ""C:\\\\LIFE"" + ""id"": 21, + ""title"": """" } - ], - ""endCursor"": null, - ""hasNextPage"": false + ], + ""endCursor"": null, + ""hasNextPage"": false }"; + // Note: The max page size is 21 for MsSql and 20 for all other data sources, so when using -1 + // this resultset represents all books in the db. + JsonElement actual = await ExecuteGraphQLRequestAsync(graphQLQuery, graphQLQueryName, isAuthenticated: false); + SqlTestHelper.PerformTestEqualJsonStrings(expected, actual.ToString()); } @@ -196,91 +202,96 @@ public async Task RequestNoParamFullConnection() }"; JsonElement actual = await ExecuteGraphQLRequestAsync(graphQLQuery, graphQLQueryName, isAuthenticated: false); + string expected = @"{ - ""items"": [ + ""items"": [ { - ""id"": 1, - ""title"": ""Awesome book"" + ""id"": 1, + ""title"": ""Awesome book"" }, { - ""id"": 2, - ""title"": ""Also Awesome book"" + ""id"": 2, + ""title"": ""Also Awesome book"" }, { - ""id"": 3, - ""title"": ""Great wall of china explained"" + ""id"": 3, + ""title"": ""Great wall of china explained"" }, { - ""id"": 4, - ""title"": ""US history in a nutshell"" + ""id"": 4, + ""title"": ""US history in a nutshell"" }, { - ""id"": 5, - ""title"": ""Chernobyl Diaries"" + ""id"": 5, + ""title"": ""Chernobyl Diaries"" }, { - ""id"": 6, - ""title"": ""The Palace Door"" + ""id"": 6, + ""title"": ""The Palace Door"" }, { - ""id"": 7, - ""title"": ""The Groovy Bar"" + ""id"": 7, + ""title"": ""The Groovy Bar"" }, { - ""id"": 8, - ""title"": ""Time to Eat"" + ""id"": 8, + ""title"": ""Time to Eat"" }, { - ""id"": 9, - ""title"": ""Policy-Test-01"" + ""id"": 9, + ""title"": ""Policy-Test-01"" }, { - ""id"": 10, - ""title"": ""Policy-Test-02"" + ""id"": 10, + ""title"": ""Policy-Test-02"" }, { - ""id"": 11, - ""title"": ""Policy-Test-04"" + ""id"": 11, + ""title"": ""Policy-Test-04"" }, { - ""id"": 12, - ""title"": ""Time to Eat 2"" + ""id"": 12, + ""title"": ""Time to Eat 2"" }, { - ""id"": 13, - ""title"": ""Before Sunrise"" + ""id"": 13, + ""title"": ""Before Sunrise"" }, { - ""id"": 14, - ""title"": ""Before Sunset"" + ""id"": 14, + ""title"": ""Before Sunset"" }, { - ""id"": 15, - ""title"": ""SQL_CONN"" + ""id"": 15, + ""title"": ""SQL_CONN"" }, { - ""id"": 16, - ""title"": ""SOME%CONN"" + ""id"": 16, + ""title"": ""SOME%CONN"" }, { - ""id"": 17, - ""title"": ""CONN%_CONN"" + ""id"": 17, + ""title"": ""CONN%_CONN"" }, { - ""id"": 18, - ""title"": ""[Special Book]"" + ""id"": 18, + ""title"": ""[Special Book]"" }, { - ""id"": 19, - ""title"": ""ME\\YOU"" + ""id"": 19, + ""title"": ""ME\\YOU"" }, { - ""id"": 20, - ""title"": ""C:\\\\LIFE"" + ""id"": 20, + ""title"": ""C:\\\\LIFE"" + }, + { + ""id"": 21, + ""title"": """" } - ], - ""endCursor"": null, - ""hasNextPage"": false + ], + ""endCursor"": null, + ""hasNextPage"": false }"; SqlTestHelper.PerformTestEqualJsonStrings(expected, actual.ToString()); diff --git a/src/Service.Tests/SqlTests/GraphQLQueryTests/MsSqlGraphQLQueryTests.cs b/src/Service.Tests/SqlTests/GraphQLQueryTests/MsSqlGraphQLQueryTests.cs index 00930103c7..1d90a4c6f1 100644 --- a/src/Service.Tests/SqlTests/GraphQLQueryTests/MsSqlGraphQLQueryTests.cs +++ b/src/Service.Tests/SqlTests/GraphQLQueryTests/MsSqlGraphQLQueryTests.cs @@ -268,6 +268,22 @@ SELECT title FROM books await QueryWithSingleColumnPrimaryKey(msSqlQuery); } + [TestMethod] + public virtual async Task QueryWithEmptyStringResult() + { + string graphQLQueryName = "book_by_pk"; + string graphQLQuery = @"{ + book_by_pk(id: 21) { + title + } + }"; + + JsonElement actual = await ExecuteGraphQLRequestAsync(graphQLQuery, graphQLQueryName, isAuthenticated: false); + + string title = actual.GetProperty("title").GetString(); + Assert.AreEqual("", title); + } + [TestMethod] public async Task QueryWithSingleColumnPrimaryKeyAndMappings() { diff --git a/src/Service.Tests/SqlTests/RestApiTests/Delete/DwSqlDeleteApiTest.cs b/src/Service.Tests/SqlTests/RestApiTests/Delete/DwSqlDeleteApiTest.cs index c574db540f..984b252727 100644 --- a/src/Service.Tests/SqlTests/RestApiTests/Delete/DwSqlDeleteApiTest.cs +++ b/src/Service.Tests/SqlTests/RestApiTests/Delete/DwSqlDeleteApiTest.cs @@ -20,7 +20,7 @@ public class DwSqlDeleteApiTests : DeleteApiTestBase { "DeleteOneWithStoredProcedureTest", $"SELECT [id] FROM { _integrationTableName } " + - $"WHERE id = 20" + $"WHERE id = 21" } }; #region Test Fixture Setup diff --git a/src/Service.Tests/SqlTests/RestApiTests/Delete/MsSqlDeleteApiTest.cs b/src/Service.Tests/SqlTests/RestApiTests/Delete/MsSqlDeleteApiTest.cs index 13f4d31cf2..cf8a1f6fc5 100644 --- a/src/Service.Tests/SqlTests/RestApiTests/Delete/MsSqlDeleteApiTest.cs +++ b/src/Service.Tests/SqlTests/RestApiTests/Delete/MsSqlDeleteApiTest.cs @@ -29,7 +29,7 @@ public class MsSqlDeleteApiTests : DeleteApiTestBase // This query is used to confirm that the item no longer exists, not the // actual delete query. $"SELECT [id] FROM { _integrationTableName } " + - $"WHERE id = 20 FOR JSON PATH, INCLUDE_NULL_VALUES, WITHOUT_ARRAY_WRAPPER" + $"WHERE id = 21 FOR JSON PATH, INCLUDE_NULL_VALUES, WITHOUT_ARRAY_WRAPPER" } }; #region Test Fixture Setup diff --git a/src/Service.Tests/UnitTests/SqlQueryExecutorUnitTests.cs b/src/Service.Tests/UnitTests/SqlQueryExecutorUnitTests.cs index 908b7019c4..2b62c6b444 100644 --- a/src/Service.Tests/UnitTests/SqlQueryExecutorUnitTests.cs +++ b/src/Service.Tests/UnitTests/SqlQueryExecutorUnitTests.cs @@ -623,6 +623,51 @@ public void ValidateStreamingLogicForStoredProcedures(int readDataLoops, bool ex } } + /// + /// Makes sure the stream logic handles cells with empty strings correctly. + /// + [DataTestMethod, TestCategory(TestCategory.MSSQL)] + public void ValidateStreamingLogicForEmptyCellsAsync() + { + TestHelper.SetupDatabaseEnvironment(TestCategory.MSSQL); + FileSystem fileSystem = new(); + FileSystemRuntimeConfigLoader loader = new(fileSystem); + RuntimeConfig runtimeConfig = new( + Schema: "UnitTestSchema", + DataSource: new DataSource(DatabaseType: DatabaseType.MSSQL, "", Options: null), + Runtime: new( + Rest: new(), + GraphQL: new(), + Mcp: new(), + Host: new(Cors: null, Authentication: null, MaxResponseSizeMB: 5) + ), + Entities: new(new Dictionary())); + + RuntimeConfigProvider runtimeConfigProvider = TestHelper.GenerateInMemoryRuntimeConfigProvider(runtimeConfig); + + Mock>> queryExecutorLogger = new(); + Mock httpContextAccessor = new(); + DbExceptionParser dbExceptionParser = new MsSqlDbExceptionParser(runtimeConfigProvider); + + // Instantiate the MsSqlQueryExecutor and Setup parameters for the query + MsSqlQueryExecutor msSqlQueryExecutor = new(runtimeConfigProvider, dbExceptionParser, queryExecutorLogger.Object, httpContextAccessor.Object); + + Mock dbDataReader = new(); + dbDataReader.Setup(d => d.HasRows).Returns(true); + + // Make sure GetChars returns 0 when buffer is null + dbDataReader.Setup(x => x.GetChars(It.IsAny(), It.IsAny(), null, It.IsAny(), It.IsAny())).Returns(0); + + // Make sure available size is set to > 0 + int availableSize = (int)runtimeConfig.MaxResponseSizeMB() * 1024 * 1024; + + // Stream char data should not return an exception + availableSize -= msSqlQueryExecutor.StreamCharData( + dbDataReader: dbDataReader.Object, availableSize: availableSize, resultJsonString: new(), ordinal: 0); + + Assert.AreEqual(availableSize, (int)runtimeConfig.MaxResponseSizeMB() * 1024 * 1024); + } + [TestCleanup] public void CleanupAfterEachTest() { From 21dde3189c99a34faf2378d038a4e3bdb2d1257d Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Fri, 31 Oct 2025 19:07:31 +0000 Subject: [PATCH 03/36] Improve MCP tool descriptions for ChatGPT compatibility (#2937) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MCP `list_tools` descriptions were insufficiently clear for ChatGPT to understand tool usage patterns and workflows, while Claude handled them adequately. ## Changes Updated descriptions and input schemas for all 6 MCP tools: - **describe_entities**: Added "ALWAYS CALL FIRST" directive and clarified permissions structure (`'ALL'` expands by type: data→CREATE, READ, UPDATE, DELETE). Expanded `nameOnly` and `entities` parameter descriptions to include detailed usage guidance: - `nameOnly`: Explains when to use it (for discovery with many entities), the two-call strategy (first with `nameOnly=true`, then with specific entities), and warns that it doesn't provide enough detail for CRUD/EXECUTE operations - `entities`: Clarifies its purpose for targeted metadata retrieval and explicitly warns against combining it with `nameOnly=true` - **CRUD tools** (create_record, read_records, update_record, delete_record): Added explicit STEP 1→STEP 2 workflow (describe_entities first, then call with matching permissions/fields) - **execute_entity**: Added workflow guidance and clarified use case (actions/computed results) - **All tools**: Condensed parameter descriptions (e.g., "Comma-separated field names" vs. "A comma-separated list of field names to include in the response. If omitted, all fields are returned. Optional.") ## Example Before: ```csharp Description = "Creates a new record in the specified entity." ``` After: ```csharp Description = "STEP 1: describe_entities -> find entities with CREATE permission and their fields. STEP 2: call this tool with matching field names and values." ``` All changes are metadata-only; no functional code modified.
Original prompt ---- *This section details on the original issue you should resolve* [BUG]: MCP `list_tools` need more comprehensive descriptions. ## What? Our tools have descriptions already. They need better to help models. > Claude works but ChatGPT struggles to understand with our current descriptions. ## New descriptions ```json { "tools": [ { "name": "describe_entities", "description": "Lists all entities and metadata. ALWAYS CALL FIRST. Each entity includes: name, type, fields, parameters, and permissions. The permissions array defines which tools are allowed. 'ALL' expands by type: data->CREATE, READ, UPDATE, DELETE.", "inputSchema": { "type": "object", "properties": { "nameOnly": { "type": "boolean", "description": "True: names and summaries only. False (default): full metadata." }, "entities": { "type": "array", "items": { "type": "string" }, "description": "Optional: specific entity names. Omit for all." } } } }, { "name": "create_record", "description": "STEP 1: describe_entities -> find entities with CREATE permission and their fields. STEP 2: call this tool with matching field names and values.", "inputSchema": { "type": "object", "properties": { "entity": { "type": "string", "description": "Entity name with CREATE permission." }, "data": { "type": "object", "description": "Required fields and values for the new record." } }, "required": ["entity", "data"] } }, { "name": "read_records", "description": "STEP 1: describe_entities -> find entities with READ permission and their fields. STEP 2: call this tool with select, filter, sort, or pagination options.", "inputSchema": { "type": "object", "properties": { "entity": { "type": "string", "description": "Entity name with READ permission." }, "select": { "type": "string", "description": "Comma-separated field names." }, "filter": { "type": "string", "description": "OData expression: eq, ne, gt, ge, lt, le, and, or, not." }, "orderby": { "type": "array", "items": { "type": "string" }, "description": "Sort fields and directions, e.g., ['name asc', 'year desc']." }, "first": { "type": "integer", "description": "Max number of records (page size)." }, "after": { "type": "string", "description": "Cursor token for next page." } }, "required": ["entity"] } }, { "name": "update_record", "description": "STEP 1: describe_entities -> find entities with UPDATE permission and their key fields. STEP 2: call this tool with keys and new field values.", "inputSchema": { "type": "object", "properties": { "entity": { "type": "string", "description": "Entity name with UPDATE permission." }, "keys": { "type": "object", "description": "Primary or composite keys identifying the record." }, "fields": { "type": "object", "description": "Fields and their new values." } }, "required": ["entity", "keys", "fields"] } }, { "name": "delete_record", "description": "STEP 1: describe_entities -> find entities with DELETE permission and their key fields. STEP 2: call this tool with full key values.", "inputSchema": { "type": "object", "properties": { "entity": { "type": "string", "description": "Entity name with DELETE permission." }, "keys": { "type": "object", "description": "All key fields identifying the record." } }, "required": ["entity", "keys"] } }, { "name": "execute_entity", "description": "STEP 1: describe_entities -> find entities with EXECUTE permission and their parameters. STEP 2: call this tool with matching parameter values. Used for entities that perform actions or return computed results.", "inputSchema": { "type": "object", "properties": { "entity": { "type": "string", "description": "Entity name with EXECUTE permission." }, "parameters": { "type": "object", "description": "Optional parameter names and values." } }, "required": ["entity"] } } ] } ``` - Fixes Azure/data-api-builder#2936 --- ✨ Let Copilot coding agent [set things up for you](https://github.com/Azure/data-api-builder/issues/new?title=✨+Set+up+Copilot+instructions&body=Configure%20instructions%20for%20this%20repository%20as%20documented%20in%20%5BBest%20practices%20for%20Copilot%20coding%20agent%20in%20your%20repository%5D%28https://gh.io/copilot-coding-agent-tips%29%2E%0A%0A%3COnboard%20this%20repo%3E&assignees=copilot) — coding agent works faster and does higher quality work when set up for your repo. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: JerryNixon <1749983+JerryNixon@users.noreply.github.com> Co-authored-by: Aniruddh Munde --- .../BuiltInTools/CreateRecordTool.cs | 6 ++--- .../BuiltInTools/DeleteRecordTool.cs | 6 ++--- .../BuiltInTools/DescribeEntitiesTool.cs | 8 +++---- .../BuiltInTools/ExecuteEntityTool.cs | 6 ++--- .../BuiltInTools/ReadRecordsTool.cs | 23 ++++++++++--------- .../BuiltInTools/UpdateRecordTool.cs | 8 +++---- 6 files changed, 29 insertions(+), 28 deletions(-) diff --git a/src/Azure.DataApiBuilder.Mcp/BuiltInTools/CreateRecordTool.cs b/src/Azure.DataApiBuilder.Mcp/BuiltInTools/CreateRecordTool.cs index 6fbe08879b..68447f16f4 100644 --- a/src/Azure.DataApiBuilder.Mcp/BuiltInTools/CreateRecordTool.cs +++ b/src/Azure.DataApiBuilder.Mcp/BuiltInTools/CreateRecordTool.cs @@ -31,18 +31,18 @@ public Tool GetToolMetadata() return new Tool { Name = "create_record", - Description = "Creates a new record in the specified entity.", + Description = "STEP 1: describe_entities -> find entities with CREATE permission and their fields. STEP 2: call this tool with matching field names and values.", InputSchema = JsonSerializer.Deserialize( @"{ ""type"": ""object"", ""properties"": { ""entity"": { ""type"": ""string"", - ""description"": ""The name of the entity"" + ""description"": ""Entity name with CREATE permission."" }, ""data"": { ""type"": ""object"", - ""description"": ""The data for the new record"" + ""description"": ""Required fields and values for the new record."" } }, ""required"": [""entity"", ""data""] diff --git a/src/Azure.DataApiBuilder.Mcp/BuiltInTools/DeleteRecordTool.cs b/src/Azure.DataApiBuilder.Mcp/BuiltInTools/DeleteRecordTool.cs index 86a5ce15ec..7abac888c5 100644 --- a/src/Azure.DataApiBuilder.Mcp/BuiltInTools/DeleteRecordTool.cs +++ b/src/Azure.DataApiBuilder.Mcp/BuiltInTools/DeleteRecordTool.cs @@ -44,18 +44,18 @@ public Tool GetToolMetadata() return new Tool { Name = "delete_record", - Description = "Deletes a record from a table based on primary key or composite key", + Description = "STEP 1: describe_entities -> find entities with DELETE permission and their key fields. STEP 2: call this tool with full key values.", InputSchema = JsonSerializer.Deserialize( @"{ ""type"": ""object"", ""properties"": { ""entity"": { ""type"": ""string"", - ""description"": ""The name of the entity (table) as configured in dab-config. Required."" + ""description"": ""Entity name with DELETE permission."" }, ""keys"": { ""type"": ""object"", - ""description"": ""Primary key values to identify the record to delete. For composite keys, provide all key columns as properties. Required."" + ""description"": ""All key fields identifying the record."" } }, ""required"": [""entity"", ""keys""] diff --git a/src/Azure.DataApiBuilder.Mcp/BuiltInTools/DescribeEntitiesTool.cs b/src/Azure.DataApiBuilder.Mcp/BuiltInTools/DescribeEntitiesTool.cs index 95c53d1d28..14e1e4db85 100644 --- a/src/Azure.DataApiBuilder.Mcp/BuiltInTools/DescribeEntitiesTool.cs +++ b/src/Azure.DataApiBuilder.Mcp/BuiltInTools/DescribeEntitiesTool.cs @@ -25,7 +25,7 @@ public class DescribeEntitiesTool : IMcpTool public ToolType ToolType { get; } = ToolType.BuiltIn; /// - /// Gets the metadata for the delete-record tool, including its name, description, and input schema. + /// Gets the metadata for the describe-entities tool, including its name, description, and input schema. /// /// public Tool GetToolMetadata() @@ -33,21 +33,21 @@ public Tool GetToolMetadata() return new Tool { Name = "describe_entities", - Description = "Lists and describes all entities in the database, including their types and available operations.", + Description = "Lists all entities and metadata. ALWAYS CALL FIRST. Each entity includes: name, type, fields, parameters, and permissions. The permissions array defines which tools are allowed. 'ALL' expands by type: data->CREATE, READ, UPDATE, DELETE.", InputSchema = JsonSerializer.Deserialize( @"{ ""type"": ""object"", ""properties"": { ""nameOnly"": { ""type"": ""boolean"", - ""description"": ""If true, only entity names and descriptions will be returned. If false, full metadata including fields, parameters etc. will be included. Default is false."" + ""description"": ""If true, the response includes only entity names and short summaries, omitting detailed metadata such as fields, parameters, and permissions. Use this when the database contains many entities and the full payload would be too large. The usual strategy is: first call describe_entities with nameOnly=true to get a lightweight list, then call describe_entities again with nameOnly=false for specific entities that require full metadata. This flag is meant for discovery, not execution planning. The model must not assume that nameOnly=true provides enough detail for CRUD or EXECUTE operations."" }, ""entities"": { ""type"": ""array"", ""items"": { ""type"": ""string"" }, - ""description"": ""Optional list of specific entity names to filter by. If empty, all entities will be described."" + ""description"": ""Optional list of entity names to describe in full detail. Use this to reduce payload size when only certain entities are relevant. Do NOT pass both entities[] and nameOnly=true together, as that combination is nonsensical: nameOnly=true ignores detailed metadata, while entities[] explicitly requests it. Choose one approach—broad discovery with nameOnly=true OR targeted metadata with entities[]."" } } }" diff --git a/src/Azure.DataApiBuilder.Mcp/BuiltInTools/ExecuteEntityTool.cs b/src/Azure.DataApiBuilder.Mcp/BuiltInTools/ExecuteEntityTool.cs index c7734eea22..be2fa7af36 100644 --- a/src/Azure.DataApiBuilder.Mcp/BuiltInTools/ExecuteEntityTool.cs +++ b/src/Azure.DataApiBuilder.Mcp/BuiltInTools/ExecuteEntityTool.cs @@ -44,18 +44,18 @@ public Tool GetToolMetadata() return new Tool { Name = "execute_entity", - Description = "Executes a stored procedure or function, returns the results (if any)", + Description = "STEP 1: describe_entities -> find entities with EXECUTE permission and their parameters. STEP 2: call this tool with matching parameter values. Used for entities that perform actions or return computed results.", InputSchema = JsonSerializer.Deserialize( @"{ ""type"": ""object"", ""properties"": { ""entity"": { ""type"": ""string"", - ""description"": ""The entity name of the procedure or function to execute. Must match a stored-procedure entity as configured in dab-config. Required."" + ""description"": ""Entity name with EXECUTE permission."" }, ""parameters"": { ""type"": ""object"", - ""description"": ""A dictionary of parameter names and values to pass to the procedure. Parameters must match those defined in dab-config. Optional if no parameters."" + ""description"": ""Optional parameter names and values."" } }, ""required"": [""entity""] diff --git a/src/Azure.DataApiBuilder.Mcp/BuiltInTools/ReadRecordsTool.cs b/src/Azure.DataApiBuilder.Mcp/BuiltInTools/ReadRecordsTool.cs index db1c761d2f..42b1f41ea0 100644 --- a/src/Azure.DataApiBuilder.Mcp/BuiltInTools/ReadRecordsTool.cs +++ b/src/Azure.DataApiBuilder.Mcp/BuiltInTools/ReadRecordsTool.cs @@ -35,37 +35,38 @@ public Tool GetToolMetadata() return new Tool { Name = "read_records", - Description = "Retrieves records from a given entity.", + Description = "STEP 1: describe_entities -> find entities with READ permission and their fields. STEP 2: call this tool with select, filter, sort, or pagination options.", InputSchema = JsonSerializer.Deserialize( @"{ ""type"": ""object"", ""properties"": { ""entity"": { ""type"": ""string"", - ""description"": ""The name of the entity to read, as provided by the describe_entities tool. Required."" + ""description"": ""Entity name with READ permission."" }, ""select"": { ""type"": ""string"", - ""description"": ""A comma-separated list of field names to include in the response. If omitted, all fields are returned. Optional."" + ""description"": ""Comma-separated field names."" }, ""filter"": { ""type"": ""string"", - ""description"": ""A case-insensitive OData-like expression that defines a query predicate. Supports logical grouping with parentheses and the operators eq, ne, gt, ge, lt, le, and, or, not. Examples: year ge 1990, date lt 2025-01-01T00:00:00Z, (title eq 'Foundation') and (available ne false). Optional."" - }, - ""first"": { - ""type"": ""integer"", - ""description"": ""The maximum number of records to return in the current page. Optional."" + ""description"": ""OData expression: eq, ne, gt, ge, lt, le, and, or, not."" }, ""orderby"": { ""type"": ""array"", ""items"": { ""type"": ""string"" }, - ""description"": ""A list of field names and directions for sorting, for example 'name asc' or 'year desc'. Optional."" + ""description"": ""Sort fields and directions, e.g., ['name asc', 'year desc']."" + }, + ""first"": { + ""type"": ""integer"", + ""description"": ""Max number of records (page size)."" }, ""after"": { ""type"": ""string"", - ""description"": ""A cursor token for retrieving the next page of results. Returned as 'after' in the previous response. Optional."" + ""description"": ""Cursor token for next page."" } - } + }, + ""required"": [""entity""] }" ) }; diff --git a/src/Azure.DataApiBuilder.Mcp/BuiltInTools/UpdateRecordTool.cs b/src/Azure.DataApiBuilder.Mcp/BuiltInTools/UpdateRecordTool.cs index 195a758454..9e7d101fe6 100644 --- a/src/Azure.DataApiBuilder.Mcp/BuiltInTools/UpdateRecordTool.cs +++ b/src/Azure.DataApiBuilder.Mcp/BuiltInTools/UpdateRecordTool.cs @@ -46,22 +46,22 @@ public Tool GetToolMetadata() return new Tool { Name = "update_record", - Description = "Updates an existing record in the specified entity. Requires 'keys' to locate the record and 'fields' to specify new values.", + Description = "STEP 1: describe_entities -> find entities with UPDATE permission and their key fields. STEP 2: call this tool with keys and new field values.", InputSchema = JsonSerializer.Deserialize( @"{ ""type"": ""object"", ""properties"": { ""entity"": { ""type"": ""string"", - ""description"": ""The name of the entity"" + ""description"": ""Entity name with UPDATE permission."" }, ""keys"": { ""type"": ""object"", - ""description"": ""Key fields and their values to identify the record"" + ""description"": ""Primary or composite keys identifying the record."" }, ""fields"": { ""type"": ""object"", - ""description"": ""Fields and their new values to update"" + ""description"": ""Fields and their new values."" } }, ""required"": [""entity"", ""keys"", ""fields""] From ccc4f2874077691e9537b075f72ff46a69773867 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 3 Nov 2025 21:14:50 +0000 Subject: [PATCH 04/36] Bump dotnet-sdk from 8.0.414 to 8.0.415 (#2908) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [dotnet-sdk](https://github.com/dotnet/sdk) from 8.0.414 to 8.0.415.
Release notes

Sourced from dotnet-sdk's releases.

.NET 8.0.21

Release

What's Changed

Full Changelog: https://github.com/dotnet/sdk/compare/v8.0.414...v8.0.415

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=dotnet-sdk&package-manager=dotnet_sdk&previous-version=8.0.414&new-version=8.0.415)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: RubenCerna2079 <32799214+RubenCerna2079@users.noreply.github.com> --- global.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/global.json b/global.json index 7e9f2f6bb4..3d952f846d 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "8.0.414", + "version": "8.0.415", "rollForward": "latestFeature" } } From 7228e608d8a3dcf8f3a77381ab69b15ed08ab078 Mon Sep 17 00:00:00 2001 From: Souvik Ghosh Date: Wed, 5 Nov 2025 07:31:45 +0530 Subject: [PATCH 05/36] Bug fix for setting dml tools based on bool value as set in config (#2927) ## Why make this change? Bug fix for setting dml tools correctly. Closes on - #2942 ## What is this change? This fix addresses the inconsistency in config where `dml-tools` can be either set as bool or a dictionary object containing individual tool names and their bool values. Without this fix, we need to individually set each `dml-tools` to true currently. With this change, we will have the following scenarios- - set `dml-tools` to `true` will enable all dml tools (default is true) Sample- ```"dml-tools": true``` - set `dml-tools` to `false` will disable all dml tools Sample-```"dml-tools": false``` - individual tool names can be specified as dictionary object for `dml-tools` to turn them on (default is true if a tool is not specified). ## How was this tested? - [ ] Integration Tests - [ ] Unit Tests - [x] Manual testing using various combinations and scenarios **Scenario 1:** Enable all tools using a single boolean value `"dml-tools": true` Note: default is true so even if `dml-tools` unspecified it will default to true. **Scenario 2:** Disable `execute-entities` only and keep other tools enabled as default. ``` "dml-tools": { "execute-entities": false } ``` **Scenario 3:** Use full list of tools and enable or disable them ``` "dml-tools": { "execute-entity": true, "delete-record": false, "update-record": false, "read-records": false, "describe-entities": true } ``` --------- Co-authored-by: Aniruddh Munde --- src/Cli.Tests/ConfigGeneratorTests.cs | 12 +++- ...stMethodsAndGraphQLOperations.verified.txt | 18 +++++- ...tyWithSourceAsStoredProcedure.verified.txt | 18 +++++- ...tityWithSourceWithDefaultType.verified.txt | 18 +++++- ...dingEntityWithoutIEnumerables.verified.txt | 18 +++++- ...ests.TestInitForCosmosDBNoSql.verified.txt | 18 +++++- ...toredProcedureWithRestMethods.verified.txt | 18 +++++- ...stMethodsAndGraphQLOperations.verified.txt | 18 +++++- ...itTests.CosmosDbNoSqlDatabase.verified.txt | 18 +++++- ...ts.CosmosDbPostgreSqlDatabase.verified.txt | 18 +++++- ...ionProviders_171ea8114ff71814.verified.txt | 18 +++++- ...ionProviders_2df7a1794712f154.verified.txt | 18 +++++- ...ionProviders_59fe1a10aa78899d.verified.txt | 18 +++++- ...ionProviders_b95b637ea87f16a7.verified.txt | 18 +++++- ...ionProviders_daacbd948b7ef72f.verified.txt | 18 +++++- ...tStartingSlashWillHaveItAdded.verified.txt | 18 +++++- .../InitTests.MsSQLDatabase.verified.txt | 18 +++++- ...tStartingSlashWillHaveItAdded.verified.txt | 18 +++++- ...ConfigWithoutConnectionString.verified.txt | 18 +++++- ...lCharactersInConnectionString.verified.txt | 18 +++++- ...ationOptions_0546bef37027a950.verified.txt | 18 +++++- ...ationOptions_0ac567dd32a2e8f5.verified.txt | 18 +++++- ...ationOptions_0c06949221514e77.verified.txt | 18 +++++- ...ationOptions_18667ab7db033e9d.verified.txt | 18 +++++- ...ationOptions_2f42f44c328eb020.verified.txt | 18 +++++- ...ationOptions_3243d3f3441fdcc1.verified.txt | 18 +++++- ...ationOptions_53350b8b47df2112.verified.txt | 18 +++++- ...ationOptions_6584e0ec46b8a11d.verified.txt | 18 +++++- ...ationOptions_81cc88db3d4eecfb.verified.txt | 18 +++++- ...ationOptions_8ea187616dbb5577.verified.txt | 18 +++++- ...ationOptions_905845c29560a3ef.verified.txt | 18 +++++- ...ationOptions_b2fd24fab5b80917.verified.txt | 18 +++++- ...ationOptions_bd7cd088755287c9.verified.txt | 18 +++++- ...ationOptions_d2eccba2f836b380.verified.txt | 18 +++++- ...ationOptions_d463eed7fe5e4bbe.verified.txt | 18 +++++- ...ationOptions_d5520dd5c33f7b8d.verified.txt | 18 +++++- ...ationOptions_eab4a6010e602b59.verified.txt | 18 +++++- ...ationOptions_ecaa688829b4030e.verified.txt | 18 +++++- .../Converters/DmlToolsConfigConverter.cs | 5 +- src/Config/ObjectModel/DmlToolsConfig.cs | 62 +++++++------------ src/Config/ObjectModel/McpRuntimeOptions.cs | 10 ++- ...ReadingRuntimeConfigForCosmos.verified.txt | 18 +++++- ...tReadingRuntimeConfigForMsSql.verified.txt | 18 +++++- ...tReadingRuntimeConfigForMySql.verified.txt | 18 +++++- ...ingRuntimeConfigForPostgreSql.verified.txt | 18 +++++- 45 files changed, 740 insertions(+), 87 deletions(-) diff --git a/src/Cli.Tests/ConfigGeneratorTests.cs b/src/Cli.Tests/ConfigGeneratorTests.cs index 58e006b75d..9f56d5964f 100644 --- a/src/Cli.Tests/ConfigGeneratorTests.cs +++ b/src/Cli.Tests/ConfigGeneratorTests.cs @@ -165,8 +165,16 @@ public void TestSpecialCharactersInConnectionString() }, ""mcp"": { ""enabled"": true, - ""path"": ""/mcp"" - }, + ""path"": ""/mcp"", + ""dml-tools"":{ + ""describe-entities"": true, + ""create-record"": true, + ""read-records"": true, + ""update-record"": true, + ""delete-record"": true, + ""execute-entity"":true + } + }, ""host"": { ""cors"": { ""origins"": [], diff --git a/src/Cli.Tests/Snapshots/EndToEndTests.TestAddingStoredProcedureWithRestMethodsAndGraphQLOperations.verified.txt b/src/Cli.Tests/Snapshots/EndToEndTests.TestAddingStoredProcedureWithRestMethodsAndGraphQLOperations.verified.txt index 4411b47348..12316672ab 100644 --- a/src/Cli.Tests/Snapshots/EndToEndTests.TestAddingStoredProcedureWithRestMethodsAndGraphQLOperations.verified.txt +++ b/src/Cli.Tests/Snapshots/EndToEndTests.TestAddingStoredProcedureWithRestMethodsAndGraphQLOperations.verified.txt @@ -18,7 +18,23 @@ }, Mcp: { Enabled: true, - Path: /mcp + Path: /mcp, + DmlTools: { + AllToolsEnabled: true, + DescribeEntities: true, + CreateRecord: true, + ReadRecords: true, + UpdateRecord: true, + DeleteRecord: true, + ExecuteEntity: true, + UserProvidedAllToolsEnabled: false, + UserProvidedDescribeEntities: true, + UserProvidedCreateRecord: true, + UserProvidedReadRecords: true, + UserProvidedUpdateRecord: true, + UserProvidedDeleteRecord: true, + UserProvidedExecuteEntity: true + } }, Host: { Cors: { diff --git a/src/Cli.Tests/Snapshots/EndToEndTests.TestConfigGeneratedAfterAddingEntityWithSourceAsStoredProcedure.verified.txt b/src/Cli.Tests/Snapshots/EndToEndTests.TestConfigGeneratedAfterAddingEntityWithSourceAsStoredProcedure.verified.txt index 636d44805e..218d4b46e8 100644 --- a/src/Cli.Tests/Snapshots/EndToEndTests.TestConfigGeneratedAfterAddingEntityWithSourceAsStoredProcedure.verified.txt +++ b/src/Cli.Tests/Snapshots/EndToEndTests.TestConfigGeneratedAfterAddingEntityWithSourceAsStoredProcedure.verified.txt @@ -18,7 +18,23 @@ }, Mcp: { Enabled: true, - Path: /mcp + Path: /mcp, + DmlTools: { + AllToolsEnabled: true, + DescribeEntities: true, + CreateRecord: true, + ReadRecords: true, + UpdateRecord: true, + DeleteRecord: true, + ExecuteEntity: true, + UserProvidedAllToolsEnabled: false, + UserProvidedDescribeEntities: true, + UserProvidedCreateRecord: true, + UserProvidedReadRecords: true, + UserProvidedUpdateRecord: true, + UserProvidedDeleteRecord: true, + UserProvidedExecuteEntity: true + } }, Host: { Cors: { diff --git a/src/Cli.Tests/Snapshots/EndToEndTests.TestConfigGeneratedAfterAddingEntityWithSourceWithDefaultType.verified.txt b/src/Cli.Tests/Snapshots/EndToEndTests.TestConfigGeneratedAfterAddingEntityWithSourceWithDefaultType.verified.txt index a77ecc134b..9ea6e5e143 100644 --- a/src/Cli.Tests/Snapshots/EndToEndTests.TestConfigGeneratedAfterAddingEntityWithSourceWithDefaultType.verified.txt +++ b/src/Cli.Tests/Snapshots/EndToEndTests.TestConfigGeneratedAfterAddingEntityWithSourceWithDefaultType.verified.txt @@ -18,7 +18,23 @@ }, Mcp: { Enabled: true, - Path: /mcp + Path: /mcp, + DmlTools: { + AllToolsEnabled: true, + DescribeEntities: true, + CreateRecord: true, + ReadRecords: true, + UpdateRecord: true, + DeleteRecord: true, + ExecuteEntity: true, + UserProvidedAllToolsEnabled: false, + UserProvidedDescribeEntities: true, + UserProvidedCreateRecord: true, + UserProvidedReadRecords: true, + UserProvidedUpdateRecord: true, + UserProvidedDeleteRecord: true, + UserProvidedExecuteEntity: true + } }, Host: { Cors: { diff --git a/src/Cli.Tests/Snapshots/EndToEndTests.TestConfigGeneratedAfterAddingEntityWithoutIEnumerables.verified.txt b/src/Cli.Tests/Snapshots/EndToEndTests.TestConfigGeneratedAfterAddingEntityWithoutIEnumerables.verified.txt index a19694b688..2685db11db 100644 --- a/src/Cli.Tests/Snapshots/EndToEndTests.TestConfigGeneratedAfterAddingEntityWithoutIEnumerables.verified.txt +++ b/src/Cli.Tests/Snapshots/EndToEndTests.TestConfigGeneratedAfterAddingEntityWithoutIEnumerables.verified.txt @@ -18,7 +18,23 @@ }, Mcp: { Enabled: true, - Path: /mcp + Path: /mcp, + DmlTools: { + AllToolsEnabled: true, + DescribeEntities: true, + CreateRecord: true, + ReadRecords: true, + UpdateRecord: true, + DeleteRecord: true, + ExecuteEntity: true, + UserProvidedAllToolsEnabled: false, + UserProvidedDescribeEntities: true, + UserProvidedCreateRecord: true, + UserProvidedReadRecords: true, + UserProvidedUpdateRecord: true, + UserProvidedDeleteRecord: true, + UserProvidedExecuteEntity: true + } }, Host: { Cors: { diff --git a/src/Cli.Tests/Snapshots/EndToEndTests.TestInitForCosmosDBNoSql.verified.txt b/src/Cli.Tests/Snapshots/EndToEndTests.TestInitForCosmosDBNoSql.verified.txt index 081c5f8e55..d13807441c 100644 --- a/src/Cli.Tests/Snapshots/EndToEndTests.TestInitForCosmosDBNoSql.verified.txt +++ b/src/Cli.Tests/Snapshots/EndToEndTests.TestInitForCosmosDBNoSql.verified.txt @@ -19,7 +19,23 @@ }, Mcp: { Enabled: true, - Path: /mcp + Path: /mcp, + DmlTools: { + AllToolsEnabled: true, + DescribeEntities: true, + CreateRecord: true, + ReadRecords: true, + UpdateRecord: true, + DeleteRecord: true, + ExecuteEntity: true, + UserProvidedAllToolsEnabled: false, + UserProvidedDescribeEntities: true, + UserProvidedCreateRecord: true, + UserProvidedReadRecords: true, + UserProvidedUpdateRecord: true, + UserProvidedDeleteRecord: true, + UserProvidedExecuteEntity: true + } }, Host: { Cors: { diff --git a/src/Cli.Tests/Snapshots/EndToEndTests.TestUpdatingStoredProcedureWithRestMethods.verified.txt b/src/Cli.Tests/Snapshots/EndToEndTests.TestUpdatingStoredProcedureWithRestMethods.verified.txt index fef1d83bf2..c1f5280277 100644 --- a/src/Cli.Tests/Snapshots/EndToEndTests.TestUpdatingStoredProcedureWithRestMethods.verified.txt +++ b/src/Cli.Tests/Snapshots/EndToEndTests.TestUpdatingStoredProcedureWithRestMethods.verified.txt @@ -18,7 +18,23 @@ }, Mcp: { Enabled: true, - Path: /mcp + Path: /mcp, + DmlTools: { + AllToolsEnabled: true, + DescribeEntities: true, + CreateRecord: true, + ReadRecords: true, + UpdateRecord: true, + DeleteRecord: true, + ExecuteEntity: true, + UserProvidedAllToolsEnabled: false, + UserProvidedDescribeEntities: true, + UserProvidedCreateRecord: true, + UserProvidedReadRecords: true, + UserProvidedUpdateRecord: true, + UserProvidedDeleteRecord: true, + UserProvidedExecuteEntity: true + } }, Host: { Cors: { diff --git a/src/Cli.Tests/Snapshots/EndToEndTests.TestUpdatingStoredProcedureWithRestMethodsAndGraphQLOperations.verified.txt b/src/Cli.Tests/Snapshots/EndToEndTests.TestUpdatingStoredProcedureWithRestMethodsAndGraphQLOperations.verified.txt index 09007e27f8..92d6369214 100644 --- a/src/Cli.Tests/Snapshots/EndToEndTests.TestUpdatingStoredProcedureWithRestMethodsAndGraphQLOperations.verified.txt +++ b/src/Cli.Tests/Snapshots/EndToEndTests.TestUpdatingStoredProcedureWithRestMethodsAndGraphQLOperations.verified.txt @@ -18,7 +18,23 @@ }, Mcp: { Enabled: true, - Path: /mcp + Path: /mcp, + DmlTools: { + AllToolsEnabled: true, + DescribeEntities: true, + CreateRecord: true, + ReadRecords: true, + UpdateRecord: true, + DeleteRecord: true, + ExecuteEntity: true, + UserProvidedAllToolsEnabled: false, + UserProvidedDescribeEntities: true, + UserProvidedCreateRecord: true, + UserProvidedReadRecords: true, + UserProvidedUpdateRecord: true, + UserProvidedDeleteRecord: true, + UserProvidedExecuteEntity: true + } }, Host: { Cors: { diff --git a/src/Cli.Tests/Snapshots/InitTests.CosmosDbNoSqlDatabase.verified.txt b/src/Cli.Tests/Snapshots/InitTests.CosmosDbNoSqlDatabase.verified.txt index b3f63dd336..50e3aa35b8 100644 --- a/src/Cli.Tests/Snapshots/InitTests.CosmosDbNoSqlDatabase.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.CosmosDbNoSqlDatabase.verified.txt @@ -19,7 +19,23 @@ }, Mcp: { Enabled: true, - Path: /mcp + Path: /mcp, + DmlTools: { + AllToolsEnabled: true, + DescribeEntities: true, + CreateRecord: true, + ReadRecords: true, + UpdateRecord: true, + DeleteRecord: true, + ExecuteEntity: true, + UserProvidedAllToolsEnabled: true, + UserProvidedDescribeEntities: true, + UserProvidedCreateRecord: true, + UserProvidedReadRecords: true, + UserProvidedUpdateRecord: true, + UserProvidedDeleteRecord: true, + UserProvidedExecuteEntity: true + } }, Host: { Cors: { diff --git a/src/Cli.Tests/Snapshots/InitTests.CosmosDbPostgreSqlDatabase.verified.txt b/src/Cli.Tests/Snapshots/InitTests.CosmosDbPostgreSqlDatabase.verified.txt index 42e0ff5e2f..496bf5f97c 100644 --- a/src/Cli.Tests/Snapshots/InitTests.CosmosDbPostgreSqlDatabase.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.CosmosDbPostgreSqlDatabase.verified.txt @@ -15,7 +15,23 @@ }, Mcp: { Enabled: true, - Path: /mcp + Path: /mcp, + DmlTools: { + AllToolsEnabled: true, + DescribeEntities: true, + CreateRecord: true, + ReadRecords: true, + UpdateRecord: true, + DeleteRecord: true, + ExecuteEntity: true, + UserProvidedAllToolsEnabled: true, + UserProvidedDescribeEntities: true, + UserProvidedCreateRecord: true, + UserProvidedReadRecords: true, + UserProvidedUpdateRecord: true, + UserProvidedDeleteRecord: true, + UserProvidedExecuteEntity: true + } }, Host: { Cors: { diff --git a/src/Cli.Tests/Snapshots/InitTests.EnsureCorrectConfigGenerationWithDifferentAuthenticationProviders_171ea8114ff71814.verified.txt b/src/Cli.Tests/Snapshots/InitTests.EnsureCorrectConfigGenerationWithDifferentAuthenticationProviders_171ea8114ff71814.verified.txt index 0af93023dc..db26433ea0 100644 --- a/src/Cli.Tests/Snapshots/InitTests.EnsureCorrectConfigGenerationWithDifferentAuthenticationProviders_171ea8114ff71814.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.EnsureCorrectConfigGenerationWithDifferentAuthenticationProviders_171ea8114ff71814.verified.txt @@ -18,7 +18,23 @@ }, Mcp: { Enabled: true, - Path: /mcp + Path: /mcp, + DmlTools: { + AllToolsEnabled: true, + DescribeEntities: true, + CreateRecord: true, + ReadRecords: true, + UpdateRecord: true, + DeleteRecord: true, + ExecuteEntity: true, + UserProvidedAllToolsEnabled: true, + UserProvidedDescribeEntities: true, + UserProvidedCreateRecord: true, + UserProvidedReadRecords: true, + UserProvidedUpdateRecord: true, + UserProvidedDeleteRecord: true, + UserProvidedExecuteEntity: true + } }, Host: { Cors: { diff --git a/src/Cli.Tests/Snapshots/InitTests.EnsureCorrectConfigGenerationWithDifferentAuthenticationProviders_2df7a1794712f154.verified.txt b/src/Cli.Tests/Snapshots/InitTests.EnsureCorrectConfigGenerationWithDifferentAuthenticationProviders_2df7a1794712f154.verified.txt index 9e77b24d74..1471284e3a 100644 --- a/src/Cli.Tests/Snapshots/InitTests.EnsureCorrectConfigGenerationWithDifferentAuthenticationProviders_2df7a1794712f154.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.EnsureCorrectConfigGenerationWithDifferentAuthenticationProviders_2df7a1794712f154.verified.txt @@ -18,7 +18,23 @@ }, Mcp: { Enabled: true, - Path: /mcp + Path: /mcp, + DmlTools: { + AllToolsEnabled: true, + DescribeEntities: true, + CreateRecord: true, + ReadRecords: true, + UpdateRecord: true, + DeleteRecord: true, + ExecuteEntity: true, + UserProvidedAllToolsEnabled: true, + UserProvidedDescribeEntities: true, + UserProvidedCreateRecord: true, + UserProvidedReadRecords: true, + UserProvidedUpdateRecord: true, + UserProvidedDeleteRecord: true, + UserProvidedExecuteEntity: true + } }, Host: { Cors: { diff --git a/src/Cli.Tests/Snapshots/InitTests.EnsureCorrectConfigGenerationWithDifferentAuthenticationProviders_59fe1a10aa78899d.verified.txt b/src/Cli.Tests/Snapshots/InitTests.EnsureCorrectConfigGenerationWithDifferentAuthenticationProviders_59fe1a10aa78899d.verified.txt index 32f72a7a54..6c6b6fa055 100644 --- a/src/Cli.Tests/Snapshots/InitTests.EnsureCorrectConfigGenerationWithDifferentAuthenticationProviders_59fe1a10aa78899d.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.EnsureCorrectConfigGenerationWithDifferentAuthenticationProviders_59fe1a10aa78899d.verified.txt @@ -18,7 +18,23 @@ }, Mcp: { Enabled: true, - Path: /mcp + Path: /mcp, + DmlTools: { + AllToolsEnabled: true, + DescribeEntities: true, + CreateRecord: true, + ReadRecords: true, + UpdateRecord: true, + DeleteRecord: true, + ExecuteEntity: true, + UserProvidedAllToolsEnabled: true, + UserProvidedDescribeEntities: true, + UserProvidedCreateRecord: true, + UserProvidedReadRecords: true, + UserProvidedUpdateRecord: true, + UserProvidedDeleteRecord: true, + UserProvidedExecuteEntity: true + } }, Host: { Cors: { diff --git a/src/Cli.Tests/Snapshots/InitTests.EnsureCorrectConfigGenerationWithDifferentAuthenticationProviders_b95b637ea87f16a7.verified.txt b/src/Cli.Tests/Snapshots/InitTests.EnsureCorrectConfigGenerationWithDifferentAuthenticationProviders_b95b637ea87f16a7.verified.txt index 24416a0d02..11f92aa0bb 100644 --- a/src/Cli.Tests/Snapshots/InitTests.EnsureCorrectConfigGenerationWithDifferentAuthenticationProviders_b95b637ea87f16a7.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.EnsureCorrectConfigGenerationWithDifferentAuthenticationProviders_b95b637ea87f16a7.verified.txt @@ -18,7 +18,23 @@ }, Mcp: { Enabled: true, - Path: /mcp + Path: /mcp, + DmlTools: { + AllToolsEnabled: true, + DescribeEntities: true, + CreateRecord: true, + ReadRecords: true, + UpdateRecord: true, + DeleteRecord: true, + ExecuteEntity: true, + UserProvidedAllToolsEnabled: true, + UserProvidedDescribeEntities: true, + UserProvidedCreateRecord: true, + UserProvidedReadRecords: true, + UserProvidedUpdateRecord: true, + UserProvidedDeleteRecord: true, + UserProvidedExecuteEntity: true + } }, Host: { Cors: { diff --git a/src/Cli.Tests/Snapshots/InitTests.EnsureCorrectConfigGenerationWithDifferentAuthenticationProviders_daacbd948b7ef72f.verified.txt b/src/Cli.Tests/Snapshots/InitTests.EnsureCorrectConfigGenerationWithDifferentAuthenticationProviders_daacbd948b7ef72f.verified.txt index 6c674a4772..5d24ae9d2f 100644 --- a/src/Cli.Tests/Snapshots/InitTests.EnsureCorrectConfigGenerationWithDifferentAuthenticationProviders_daacbd948b7ef72f.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.EnsureCorrectConfigGenerationWithDifferentAuthenticationProviders_daacbd948b7ef72f.verified.txt @@ -18,7 +18,23 @@ }, Mcp: { Enabled: true, - Path: /mcp + Path: /mcp, + DmlTools: { + AllToolsEnabled: true, + DescribeEntities: true, + CreateRecord: true, + ReadRecords: true, + UpdateRecord: true, + DeleteRecord: true, + ExecuteEntity: true, + UserProvidedAllToolsEnabled: true, + UserProvidedDescribeEntities: true, + UserProvidedCreateRecord: true, + UserProvidedReadRecords: true, + UserProvidedUpdateRecord: true, + UserProvidedDeleteRecord: true, + UserProvidedExecuteEntity: true + } }, Host: { Cors: { diff --git a/src/Cli.Tests/Snapshots/InitTests.GraphQLPathWithoutStartingSlashWillHaveItAdded.verified.txt b/src/Cli.Tests/Snapshots/InitTests.GraphQLPathWithoutStartingSlashWillHaveItAdded.verified.txt index b6aac13236..cd721f65d2 100644 --- a/src/Cli.Tests/Snapshots/InitTests.GraphQLPathWithoutStartingSlashWillHaveItAdded.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.GraphQLPathWithoutStartingSlashWillHaveItAdded.verified.txt @@ -18,7 +18,23 @@ }, Mcp: { Enabled: true, - Path: /mcp + Path: /mcp, + DmlTools: { + AllToolsEnabled: true, + DescribeEntities: true, + CreateRecord: true, + ReadRecords: true, + UpdateRecord: true, + DeleteRecord: true, + ExecuteEntity: true, + UserProvidedAllToolsEnabled: true, + UserProvidedDescribeEntities: true, + UserProvidedCreateRecord: true, + UserProvidedReadRecords: true, + UserProvidedUpdateRecord: true, + UserProvidedDeleteRecord: true, + UserProvidedExecuteEntity: true + } }, Host: { Cors: { diff --git a/src/Cli.Tests/Snapshots/InitTests.MsSQLDatabase.verified.txt b/src/Cli.Tests/Snapshots/InitTests.MsSQLDatabase.verified.txt index 8841c0f326..d304df7085 100644 --- a/src/Cli.Tests/Snapshots/InitTests.MsSQLDatabase.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.MsSQLDatabase.verified.txt @@ -18,7 +18,23 @@ }, Mcp: { Enabled: true, - Path: /mcp + Path: /mcp, + DmlTools: { + AllToolsEnabled: true, + DescribeEntities: true, + CreateRecord: true, + ReadRecords: true, + UpdateRecord: true, + DeleteRecord: true, + ExecuteEntity: true, + UserProvidedAllToolsEnabled: true, + UserProvidedDescribeEntities: true, + UserProvidedCreateRecord: true, + UserProvidedReadRecords: true, + UserProvidedUpdateRecord: true, + UserProvidedDeleteRecord: true, + UserProvidedExecuteEntity: true + } }, Host: { Cors: { diff --git a/src/Cli.Tests/Snapshots/InitTests.RestPathWithoutStartingSlashWillHaveItAdded.verified.txt b/src/Cli.Tests/Snapshots/InitTests.RestPathWithoutStartingSlashWillHaveItAdded.verified.txt index 68e4d231fd..ff7b282c32 100644 --- a/src/Cli.Tests/Snapshots/InitTests.RestPathWithoutStartingSlashWillHaveItAdded.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.RestPathWithoutStartingSlashWillHaveItAdded.verified.txt @@ -18,7 +18,23 @@ }, Mcp: { Enabled: true, - Path: /mcp + Path: /mcp, + DmlTools: { + AllToolsEnabled: true, + DescribeEntities: true, + CreateRecord: true, + ReadRecords: true, + UpdateRecord: true, + DeleteRecord: true, + ExecuteEntity: true, + UserProvidedAllToolsEnabled: true, + UserProvidedDescribeEntities: true, + UserProvidedCreateRecord: true, + UserProvidedReadRecords: true, + UserProvidedUpdateRecord: true, + UserProvidedDeleteRecord: true, + UserProvidedExecuteEntity: true + } }, Host: { Cors: { diff --git a/src/Cli.Tests/Snapshots/InitTests.TestInitializingConfigWithoutConnectionString.verified.txt b/src/Cli.Tests/Snapshots/InitTests.TestInitializingConfigWithoutConnectionString.verified.txt index 3c281ad6aa..6714b2a50d 100644 --- a/src/Cli.Tests/Snapshots/InitTests.TestInitializingConfigWithoutConnectionString.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.TestInitializingConfigWithoutConnectionString.verified.txt @@ -18,7 +18,23 @@ }, Mcp: { Enabled: true, - Path: /mcp + Path: /mcp, + DmlTools: { + AllToolsEnabled: true, + DescribeEntities: true, + CreateRecord: true, + ReadRecords: true, + UpdateRecord: true, + DeleteRecord: true, + ExecuteEntity: true, + UserProvidedAllToolsEnabled: true, + UserProvidedDescribeEntities: true, + UserProvidedCreateRecord: true, + UserProvidedReadRecords: true, + UserProvidedUpdateRecord: true, + UserProvidedDeleteRecord: true, + UserProvidedExecuteEntity: true + } }, Host: { Cors: { diff --git a/src/Cli.Tests/Snapshots/InitTests.TestSpecialCharactersInConnectionString.verified.txt b/src/Cli.Tests/Snapshots/InitTests.TestSpecialCharactersInConnectionString.verified.txt index 32f72a7a54..6c6b6fa055 100644 --- a/src/Cli.Tests/Snapshots/InitTests.TestSpecialCharactersInConnectionString.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.TestSpecialCharactersInConnectionString.verified.txt @@ -18,7 +18,23 @@ }, Mcp: { Enabled: true, - Path: /mcp + Path: /mcp, + DmlTools: { + AllToolsEnabled: true, + DescribeEntities: true, + CreateRecord: true, + ReadRecords: true, + UpdateRecord: true, + DeleteRecord: true, + ExecuteEntity: true, + UserProvidedAllToolsEnabled: true, + UserProvidedDescribeEntities: true, + UserProvidedCreateRecord: true, + UserProvidedReadRecords: true, + UserProvidedUpdateRecord: true, + UserProvidedDeleteRecord: true, + UserProvidedExecuteEntity: true + } }, Host: { Cors: { diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_0546bef37027a950.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_0546bef37027a950.verified.txt index 888466ab4a..397a36867b 100644 --- a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_0546bef37027a950.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_0546bef37027a950.verified.txt @@ -18,7 +18,23 @@ }, Mcp: { Enabled: true, - Path: /mcp + Path: /mcp, + DmlTools: { + AllToolsEnabled: true, + DescribeEntities: true, + CreateRecord: true, + ReadRecords: true, + UpdateRecord: true, + DeleteRecord: true, + ExecuteEntity: true, + UserProvidedAllToolsEnabled: true, + UserProvidedDescribeEntities: true, + UserProvidedCreateRecord: true, + UserProvidedReadRecords: true, + UserProvidedUpdateRecord: true, + UserProvidedDeleteRecord: true, + UserProvidedExecuteEntity: true + } }, Host: { Cors: { diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_0ac567dd32a2e8f5.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_0ac567dd32a2e8f5.verified.txt index 8841c0f326..d304df7085 100644 --- a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_0ac567dd32a2e8f5.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_0ac567dd32a2e8f5.verified.txt @@ -18,7 +18,23 @@ }, Mcp: { Enabled: true, - Path: /mcp + Path: /mcp, + DmlTools: { + AllToolsEnabled: true, + DescribeEntities: true, + CreateRecord: true, + ReadRecords: true, + UpdateRecord: true, + DeleteRecord: true, + ExecuteEntity: true, + UserProvidedAllToolsEnabled: true, + UserProvidedDescribeEntities: true, + UserProvidedCreateRecord: true, + UserProvidedReadRecords: true, + UserProvidedUpdateRecord: true, + UserProvidedDeleteRecord: true, + UserProvidedExecuteEntity: true + } }, Host: { Cors: { diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_0c06949221514e77.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_0c06949221514e77.verified.txt index d56e05c483..ff833e5198 100644 --- a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_0c06949221514e77.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_0c06949221514e77.verified.txt @@ -23,7 +23,23 @@ }, Mcp: { Enabled: true, - Path: /mcp + Path: /mcp, + DmlTools: { + AllToolsEnabled: true, + DescribeEntities: true, + CreateRecord: true, + ReadRecords: true, + UpdateRecord: true, + DeleteRecord: true, + ExecuteEntity: true, + UserProvidedAllToolsEnabled: true, + UserProvidedDescribeEntities: true, + UserProvidedCreateRecord: true, + UserProvidedReadRecords: true, + UserProvidedUpdateRecord: true, + UserProvidedDeleteRecord: true, + UserProvidedExecuteEntity: true + } }, Host: { Cors: { diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_18667ab7db033e9d.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_18667ab7db033e9d.verified.txt index bc31484242..faf59ef911 100644 --- a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_18667ab7db033e9d.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_18667ab7db033e9d.verified.txt @@ -15,7 +15,23 @@ }, Mcp: { Enabled: true, - Path: /mcp + Path: /mcp, + DmlTools: { + AllToolsEnabled: true, + DescribeEntities: true, + CreateRecord: true, + ReadRecords: true, + UpdateRecord: true, + DeleteRecord: true, + ExecuteEntity: true, + UserProvidedAllToolsEnabled: true, + UserProvidedDescribeEntities: true, + UserProvidedCreateRecord: true, + UserProvidedReadRecords: true, + UserProvidedUpdateRecord: true, + UserProvidedDeleteRecord: true, + UserProvidedExecuteEntity: true + } }, Host: { Cors: { diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_2f42f44c328eb020.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_2f42f44c328eb020.verified.txt index 888466ab4a..397a36867b 100644 --- a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_2f42f44c328eb020.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_2f42f44c328eb020.verified.txt @@ -18,7 +18,23 @@ }, Mcp: { Enabled: true, - Path: /mcp + Path: /mcp, + DmlTools: { + AllToolsEnabled: true, + DescribeEntities: true, + CreateRecord: true, + ReadRecords: true, + UpdateRecord: true, + DeleteRecord: true, + ExecuteEntity: true, + UserProvidedAllToolsEnabled: true, + UserProvidedDescribeEntities: true, + UserProvidedCreateRecord: true, + UserProvidedReadRecords: true, + UserProvidedUpdateRecord: true, + UserProvidedDeleteRecord: true, + UserProvidedExecuteEntity: true + } }, Host: { Cors: { diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_3243d3f3441fdcc1.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_3243d3f3441fdcc1.verified.txt index bc31484242..faf59ef911 100644 --- a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_3243d3f3441fdcc1.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_3243d3f3441fdcc1.verified.txt @@ -15,7 +15,23 @@ }, Mcp: { Enabled: true, - Path: /mcp + Path: /mcp, + DmlTools: { + AllToolsEnabled: true, + DescribeEntities: true, + CreateRecord: true, + ReadRecords: true, + UpdateRecord: true, + DeleteRecord: true, + ExecuteEntity: true, + UserProvidedAllToolsEnabled: true, + UserProvidedDescribeEntities: true, + UserProvidedCreateRecord: true, + UserProvidedReadRecords: true, + UserProvidedUpdateRecord: true, + UserProvidedDeleteRecord: true, + UserProvidedExecuteEntity: true + } }, Host: { Cors: { diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_53350b8b47df2112.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_53350b8b47df2112.verified.txt index 48f5e7a7c9..bc08f75e48 100644 --- a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_53350b8b47df2112.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_53350b8b47df2112.verified.txt @@ -15,7 +15,23 @@ }, Mcp: { Enabled: true, - Path: /mcp + Path: /mcp, + DmlTools: { + AllToolsEnabled: true, + DescribeEntities: true, + CreateRecord: true, + ReadRecords: true, + UpdateRecord: true, + DeleteRecord: true, + ExecuteEntity: true, + UserProvidedAllToolsEnabled: true, + UserProvidedDescribeEntities: true, + UserProvidedCreateRecord: true, + UserProvidedReadRecords: true, + UserProvidedUpdateRecord: true, + UserProvidedDeleteRecord: true, + UserProvidedExecuteEntity: true + } }, Host: { Cors: { diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_6584e0ec46b8a11d.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_6584e0ec46b8a11d.verified.txt index 8fa9677f1d..85e52e4e7f 100644 --- a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_6584e0ec46b8a11d.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_6584e0ec46b8a11d.verified.txt @@ -19,7 +19,23 @@ }, Mcp: { Enabled: true, - Path: /mcp + Path: /mcp, + DmlTools: { + AllToolsEnabled: true, + DescribeEntities: true, + CreateRecord: true, + ReadRecords: true, + UpdateRecord: true, + DeleteRecord: true, + ExecuteEntity: true, + UserProvidedAllToolsEnabled: true, + UserProvidedDescribeEntities: true, + UserProvidedCreateRecord: true, + UserProvidedReadRecords: true, + UserProvidedUpdateRecord: true, + UserProvidedDeleteRecord: true, + UserProvidedExecuteEntity: true + } }, Host: { Cors: { diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_81cc88db3d4eecfb.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_81cc88db3d4eecfb.verified.txt index e3108801f5..1bcf97c8d7 100644 --- a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_81cc88db3d4eecfb.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_81cc88db3d4eecfb.verified.txt @@ -23,7 +23,23 @@ }, Mcp: { Enabled: true, - Path: /mcp + Path: /mcp, + DmlTools: { + AllToolsEnabled: true, + DescribeEntities: true, + CreateRecord: true, + ReadRecords: true, + UpdateRecord: true, + DeleteRecord: true, + ExecuteEntity: true, + UserProvidedAllToolsEnabled: true, + UserProvidedDescribeEntities: true, + UserProvidedCreateRecord: true, + UserProvidedReadRecords: true, + UserProvidedUpdateRecord: true, + UserProvidedDeleteRecord: true, + UserProvidedExecuteEntity: true + } }, Host: { Cors: { diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_8ea187616dbb5577.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_8ea187616dbb5577.verified.txt index 59f6636fb2..9d14eb779f 100644 --- a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_8ea187616dbb5577.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_8ea187616dbb5577.verified.txt @@ -15,7 +15,23 @@ }, Mcp: { Enabled: true, - Path: /mcp + Path: /mcp, + DmlTools: { + AllToolsEnabled: true, + DescribeEntities: true, + CreateRecord: true, + ReadRecords: true, + UpdateRecord: true, + DeleteRecord: true, + ExecuteEntity: true, + UserProvidedAllToolsEnabled: true, + UserProvidedDescribeEntities: true, + UserProvidedCreateRecord: true, + UserProvidedReadRecords: true, + UserProvidedUpdateRecord: true, + UserProvidedDeleteRecord: true, + UserProvidedExecuteEntity: true + } }, Host: { Cors: { diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_905845c29560a3ef.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_905845c29560a3ef.verified.txt index 888466ab4a..397a36867b 100644 --- a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_905845c29560a3ef.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_905845c29560a3ef.verified.txt @@ -18,7 +18,23 @@ }, Mcp: { Enabled: true, - Path: /mcp + Path: /mcp, + DmlTools: { + AllToolsEnabled: true, + DescribeEntities: true, + CreateRecord: true, + ReadRecords: true, + UpdateRecord: true, + DeleteRecord: true, + ExecuteEntity: true, + UserProvidedAllToolsEnabled: true, + UserProvidedDescribeEntities: true, + UserProvidedCreateRecord: true, + UserProvidedReadRecords: true, + UserProvidedUpdateRecord: true, + UserProvidedDeleteRecord: true, + UserProvidedExecuteEntity: true + } }, Host: { Cors: { diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_b2fd24fab5b80917.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_b2fd24fab5b80917.verified.txt index 8fa9677f1d..85e52e4e7f 100644 --- a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_b2fd24fab5b80917.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_b2fd24fab5b80917.verified.txt @@ -19,7 +19,23 @@ }, Mcp: { Enabled: true, - Path: /mcp + Path: /mcp, + DmlTools: { + AllToolsEnabled: true, + DescribeEntities: true, + CreateRecord: true, + ReadRecords: true, + UpdateRecord: true, + DeleteRecord: true, + ExecuteEntity: true, + UserProvidedAllToolsEnabled: true, + UserProvidedDescribeEntities: true, + UserProvidedCreateRecord: true, + UserProvidedReadRecords: true, + UserProvidedUpdateRecord: true, + UserProvidedDeleteRecord: true, + UserProvidedExecuteEntity: true + } }, Host: { Cors: { diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_bd7cd088755287c9.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_bd7cd088755287c9.verified.txt index 8fa9677f1d..85e52e4e7f 100644 --- a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_bd7cd088755287c9.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_bd7cd088755287c9.verified.txt @@ -19,7 +19,23 @@ }, Mcp: { Enabled: true, - Path: /mcp + Path: /mcp, + DmlTools: { + AllToolsEnabled: true, + DescribeEntities: true, + CreateRecord: true, + ReadRecords: true, + UpdateRecord: true, + DeleteRecord: true, + ExecuteEntity: true, + UserProvidedAllToolsEnabled: true, + UserProvidedDescribeEntities: true, + UserProvidedCreateRecord: true, + UserProvidedReadRecords: true, + UserProvidedUpdateRecord: true, + UserProvidedDeleteRecord: true, + UserProvidedExecuteEntity: true + } }, Host: { Cors: { diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_d2eccba2f836b380.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_d2eccba2f836b380.verified.txt index 48f5e7a7c9..bc08f75e48 100644 --- a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_d2eccba2f836b380.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_d2eccba2f836b380.verified.txt @@ -15,7 +15,23 @@ }, Mcp: { Enabled: true, - Path: /mcp + Path: /mcp, + DmlTools: { + AllToolsEnabled: true, + DescribeEntities: true, + CreateRecord: true, + ReadRecords: true, + UpdateRecord: true, + DeleteRecord: true, + ExecuteEntity: true, + UserProvidedAllToolsEnabled: true, + UserProvidedDescribeEntities: true, + UserProvidedCreateRecord: true, + UserProvidedReadRecords: true, + UserProvidedUpdateRecord: true, + UserProvidedDeleteRecord: true, + UserProvidedExecuteEntity: true + } }, Host: { Cors: { diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_d463eed7fe5e4bbe.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_d463eed7fe5e4bbe.verified.txt index 59f6636fb2..9d14eb779f 100644 --- a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_d463eed7fe5e4bbe.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_d463eed7fe5e4bbe.verified.txt @@ -15,7 +15,23 @@ }, Mcp: { Enabled: true, - Path: /mcp + Path: /mcp, + DmlTools: { + AllToolsEnabled: true, + DescribeEntities: true, + CreateRecord: true, + ReadRecords: true, + UpdateRecord: true, + DeleteRecord: true, + ExecuteEntity: true, + UserProvidedAllToolsEnabled: true, + UserProvidedDescribeEntities: true, + UserProvidedCreateRecord: true, + UserProvidedReadRecords: true, + UserProvidedUpdateRecord: true, + UserProvidedDeleteRecord: true, + UserProvidedExecuteEntity: true + } }, Host: { Cors: { diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_d5520dd5c33f7b8d.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_d5520dd5c33f7b8d.verified.txt index 48f5e7a7c9..bc08f75e48 100644 --- a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_d5520dd5c33f7b8d.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_d5520dd5c33f7b8d.verified.txt @@ -15,7 +15,23 @@ }, Mcp: { Enabled: true, - Path: /mcp + Path: /mcp, + DmlTools: { + AllToolsEnabled: true, + DescribeEntities: true, + CreateRecord: true, + ReadRecords: true, + UpdateRecord: true, + DeleteRecord: true, + ExecuteEntity: true, + UserProvidedAllToolsEnabled: true, + UserProvidedDescribeEntities: true, + UserProvidedCreateRecord: true, + UserProvidedReadRecords: true, + UserProvidedUpdateRecord: true, + UserProvidedDeleteRecord: true, + UserProvidedExecuteEntity: true + } }, Host: { Cors: { diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_eab4a6010e602b59.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_eab4a6010e602b59.verified.txt index bc31484242..faf59ef911 100644 --- a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_eab4a6010e602b59.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_eab4a6010e602b59.verified.txt @@ -15,7 +15,23 @@ }, Mcp: { Enabled: true, - Path: /mcp + Path: /mcp, + DmlTools: { + AllToolsEnabled: true, + DescribeEntities: true, + CreateRecord: true, + ReadRecords: true, + UpdateRecord: true, + DeleteRecord: true, + ExecuteEntity: true, + UserProvidedAllToolsEnabled: true, + UserProvidedDescribeEntities: true, + UserProvidedCreateRecord: true, + UserProvidedReadRecords: true, + UserProvidedUpdateRecord: true, + UserProvidedDeleteRecord: true, + UserProvidedExecuteEntity: true + } }, Host: { Cors: { diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_ecaa688829b4030e.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_ecaa688829b4030e.verified.txt index 59f6636fb2..9d14eb779f 100644 --- a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_ecaa688829b4030e.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_ecaa688829b4030e.verified.txt @@ -15,7 +15,23 @@ }, Mcp: { Enabled: true, - Path: /mcp + Path: /mcp, + DmlTools: { + AllToolsEnabled: true, + DescribeEntities: true, + CreateRecord: true, + ReadRecords: true, + UpdateRecord: true, + DeleteRecord: true, + ExecuteEntity: true, + UserProvidedAllToolsEnabled: true, + UserProvidedDescribeEntities: true, + UserProvidedCreateRecord: true, + UserProvidedReadRecords: true, + UserProvidedUpdateRecord: true, + UserProvidedDeleteRecord: true, + UserProvidedExecuteEntity: true + } }, Host: { Cors: { diff --git a/src/Config/Converters/DmlToolsConfigConverter.cs b/src/Config/Converters/DmlToolsConfigConverter.cs index 9acef0f9b2..098bc64bc3 100644 --- a/src/Config/Converters/DmlToolsConfigConverter.cs +++ b/src/Config/Converters/DmlToolsConfigConverter.cs @@ -37,7 +37,7 @@ internal class DmlToolsConfigConverter : JsonConverter // Handle object format if (reader.TokenType is JsonTokenType.StartObject) { - // When using object format, unspecified tools default to true + // Start with null values - only set when explicitly provided in JSON bool? describeEntities = null; bool? createRecord = null; bool? readRecords = null; @@ -102,8 +102,7 @@ internal class DmlToolsConfigConverter : JsonConverter } } - // Create the config with specified values - // Unspecified values (null) will default to true in the DmlToolsConfig constructor + // Pass null for unspecified values - the constructor will handle defaults return new DmlToolsConfig( allToolsEnabled: null, describeEntities: describeEntities, diff --git a/src/Config/ObjectModel/DmlToolsConfig.cs b/src/Config/ObjectModel/DmlToolsConfig.cs index c14f8e49ed..a08fd8d176 100644 --- a/src/Config/ObjectModel/DmlToolsConfig.cs +++ b/src/Config/ObjectModel/DmlToolsConfig.cs @@ -71,41 +71,24 @@ public DmlToolsConfig( AllToolsEnabled = DEFAULT_ENABLED; } - if (describeEntities is not null) - { - DescribeEntities = describeEntities; - UserProvidedDescribeEntities = true; - } + // Set values with defaults and track user-provided status + DescribeEntities = describeEntities ?? DEFAULT_ENABLED; + UserProvidedDescribeEntities = describeEntities is not null; - if (createRecord is not null) - { - CreateRecord = createRecord; - UserProvidedCreateRecord = true; - } + CreateRecord = createRecord ?? DEFAULT_ENABLED; + UserProvidedCreateRecord = createRecord is not null; - if (readRecords is not null) - { - ReadRecords = readRecords; - UserProvidedReadRecords = true; - } + ReadRecords = readRecords ?? DEFAULT_ENABLED; + UserProvidedReadRecords = readRecords is not null; - if (updateRecord is not null) - { - UpdateRecord = updateRecord; - UserProvidedUpdateRecord = true; - } + UpdateRecord = updateRecord ?? DEFAULT_ENABLED; + UserProvidedUpdateRecord = updateRecord is not null; - if (deleteRecord is not null) - { - DeleteRecord = deleteRecord; - UserProvidedDeleteRecord = true; - } + DeleteRecord = deleteRecord ?? DEFAULT_ENABLED; + UserProvidedDeleteRecord = deleteRecord is not null; - if (executeEntity is not null) - { - ExecuteEntity = executeEntity; - UserProvidedExecuteEntity = true; - } + ExecuteEntity = executeEntity ?? DEFAULT_ENABLED; + UserProvidedExecuteEntity = executeEntity is not null; } /// @@ -113,16 +96,15 @@ public DmlToolsConfig( /// public static DmlToolsConfig FromBoolean(bool enabled) { - return new DmlToolsConfig - { - AllToolsEnabled = enabled, - DescribeEntities = null, - CreateRecord = null, - ReadRecords = null, - UpdateRecord = null, - DeleteRecord = null, - ExecuteEntity = null - }; + return new DmlToolsConfig( + allToolsEnabled: enabled, + describeEntities: enabled, + createRecord: enabled, + readRecords: enabled, + updateRecord: enabled, + deleteRecord: enabled, + executeEntity: enabled + ); } /// diff --git a/src/Config/ObjectModel/McpRuntimeOptions.cs b/src/Config/ObjectModel/McpRuntimeOptions.cs index 73d695ee4a..3934c091bb 100644 --- a/src/Config/ObjectModel/McpRuntimeOptions.cs +++ b/src/Config/ObjectModel/McpRuntimeOptions.cs @@ -48,7 +48,15 @@ public McpRuntimeOptions( this.Path = DEFAULT_PATH; } - this.DmlTools = DmlTools; + // if DmlTools is null, set All tools enabled by default + if (DmlTools is null) + { + this.DmlTools = DmlToolsConfig.FromBoolean(DmlToolsConfig.DEFAULT_ENABLED); + } + else + { + this.DmlTools = DmlTools; + } } /// diff --git a/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForCosmos.verified.txt b/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForCosmos.verified.txt index 420977ed26..c135dffdb6 100644 --- a/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForCosmos.verified.txt +++ b/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForCosmos.verified.txt @@ -19,7 +19,23 @@ }, Mcp: { Enabled: true, - Path: /mcp + Path: /mcp, + DmlTools: { + AllToolsEnabled: true, + DescribeEntities: true, + CreateRecord: true, + ReadRecords: true, + UpdateRecord: true, + DeleteRecord: true, + ExecuteEntity: true, + UserProvidedAllToolsEnabled: false, + UserProvidedDescribeEntities: true, + UserProvidedCreateRecord: true, + UserProvidedReadRecords: true, + UserProvidedUpdateRecord: true, + UserProvidedDeleteRecord: true, + UserProvidedExecuteEntity: true + } }, Host: { Cors: { diff --git a/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMsSql.verified.txt b/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMsSql.verified.txt index b622552ef5..5b2a607817 100644 --- a/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMsSql.verified.txt +++ b/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMsSql.verified.txt @@ -23,7 +23,23 @@ }, Mcp: { Enabled: true, - Path: /mcp + Path: /mcp, + DmlTools: { + AllToolsEnabled: true, + DescribeEntities: true, + CreateRecord: true, + ReadRecords: true, + UpdateRecord: true, + DeleteRecord: true, + ExecuteEntity: true, + UserProvidedAllToolsEnabled: false, + UserProvidedDescribeEntities: true, + UserProvidedCreateRecord: true, + UserProvidedReadRecords: true, + UserProvidedUpdateRecord: true, + UserProvidedDeleteRecord: true, + UserProvidedExecuteEntity: true + } }, Host: { Cors: { diff --git a/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMySql.verified.txt b/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMySql.verified.txt index 6c81c138ce..0f30634609 100644 --- a/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMySql.verified.txt +++ b/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMySql.verified.txt @@ -15,7 +15,23 @@ }, Mcp: { Enabled: true, - Path: /mcp + Path: /mcp, + DmlTools: { + AllToolsEnabled: true, + DescribeEntities: true, + CreateRecord: true, + ReadRecords: true, + UpdateRecord: true, + DeleteRecord: true, + ExecuteEntity: true, + UserProvidedAllToolsEnabled: false, + UserProvidedDescribeEntities: true, + UserProvidedCreateRecord: true, + UserProvidedReadRecords: true, + UserProvidedUpdateRecord: true, + UserProvidedDeleteRecord: true, + UserProvidedExecuteEntity: true + } }, Host: { Cors: { diff --git a/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForPostgreSql.verified.txt b/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForPostgreSql.verified.txt index 5e8631d46f..6a4f569a2a 100644 --- a/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForPostgreSql.verified.txt +++ b/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForPostgreSql.verified.txt @@ -15,7 +15,23 @@ }, Mcp: { Enabled: true, - Path: /mcp + Path: /mcp, + DmlTools: { + AllToolsEnabled: true, + DescribeEntities: true, + CreateRecord: true, + ReadRecords: true, + UpdateRecord: true, + DeleteRecord: true, + ExecuteEntity: true, + UserProvidedAllToolsEnabled: false, + UserProvidedDescribeEntities: true, + UserProvidedCreateRecord: true, + UserProvidedReadRecords: true, + UserProvidedUpdateRecord: true, + UserProvidedDeleteRecord: true, + UserProvidedExecuteEntity: true + } }, Host: { Cors: { From 3b48c4900b2ef7121e61b6bc1757fae9bc7b9f17 Mon Sep 17 00:00:00 2001 From: Souvik Ghosh Date: Thu, 6 Nov 2025 00:15:27 +0530 Subject: [PATCH 06/36] Expand ALL permissions in MCP describe_entities to explicit operations (#2951) The MCP `describe_entities` tool returns `"ALL"` for wildcard permissions, which confuses LLM consumers that need explicit operation lists. ### Changes Modified `DescribeEntitiesTool.BuildPermissionsInfo()` to expand `EntityActionOperation.All`: - **Tables/Views**: Expands to `["CREATE", "DELETE", "READ", "UPDATE"]` via `EntityAction.ValidPermissionOperations` - **Stored Procedures**: Expands to `["EXECUTE"]` via `EntityAction.ValidStoredProcedurePermissionOperations` ### Example **Before:** ```json { "name": "Todo", "permissions": ["ALL"] } ``` **After:** ```json { "name": "Todo", "permissions": ["CREATE", "DELETE", "READ", "UPDATE"] } ```
Original prompt > > ---- > > *This section details on the original issue you should resolve* > > [Bug]: MCP `describe_entities` permissions value `ALL` needs to be expanded. > ## What? > > Models are confused by `ALL`. > > ```json > { > "entities": [ > { > "name": "Todo", > "description": "This table contains the list of todo items.", > "fields": [ ], > "permissions": [ > "ALL" // this is the problem > ] > } > ], > "count": 1, > "mode": "full", > "status": "success" > } > ``` > > ## Solution > > When table/view. > > ```json > { > "permissions: [ > "CREATE", > "DELETE", > "READ", > "UPDATE" > ] > } > ``` > > When stored procedure. > > ```json > { > "permissions: [ > "EXECUTE" > ] > } > ``` > > > ## Comments on the Issue (you are @copilot in this section) > > > >
- Fixes Azure/data-api-builder#2935 --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: JerryNixon <1749983+JerryNixon@users.noreply.github.com> --- .../BuiltInTools/DescribeEntitiesTool.cs | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/src/Azure.DataApiBuilder.Mcp/BuiltInTools/DescribeEntitiesTool.cs b/src/Azure.DataApiBuilder.Mcp/BuiltInTools/DescribeEntitiesTool.cs index 14e1e4db85..4215705572 100644 --- a/src/Azure.DataApiBuilder.Mcp/BuiltInTools/DescribeEntitiesTool.cs +++ b/src/Azure.DataApiBuilder.Mcp/BuiltInTools/DescribeEntitiesTool.cs @@ -368,13 +368,30 @@ private static List BuildParameterMetadataInfo(List? /// A list of permissions available to the entity private static string[] BuildPermissionsInfo(Entity entity) { - HashSet permissions = new(); + if (entity.Permissions == null) + { + return Array.Empty(); + } + + bool isStoredProcedure = entity.Source.Type == EntitySourceType.StoredProcedure; + HashSet validOperations = isStoredProcedure + ? EntityAction.ValidStoredProcedurePermissionOperations + : EntityAction.ValidPermissionOperations; - if (entity.Permissions != null) + HashSet permissions = new(StringComparer.OrdinalIgnoreCase); + + foreach (EntityPermission permission in entity.Permissions) { - foreach (EntityPermission permission in entity.Permissions) + foreach (EntityAction action in permission.Actions) { - foreach (EntityAction action in permission.Actions) + if (action.Action == EntityActionOperation.All) + { + foreach (EntityActionOperation op in validOperations) + { + permissions.Add(op.ToString().ToUpperInvariant()); + } + } + else { permissions.Add(action.Action.ToString().ToUpperInvariant()); } From 3adf04f1cc3454688f98667326a8406068aa98f0 Mon Sep 17 00:00:00 2001 From: Souvik Ghosh Date: Sat, 8 Nov 2025 00:52:21 +0530 Subject: [PATCH 07/36] User provided dml-tools property serialization (#2952) ## Why make this change? This PR addresses the issue where with serialization of `dml-tools` property for user-provided (non-default) values. This is a follow up to the fix made for the functionality of `dml-tools` property- https://github.com/Azure/data-api-builder/pull/2927 ## What is this change? If `dml-tools` is not configured, this property should not be serialized. Serialization should only be done for user provided configuration. So, any value explicitly set or configured by the user for `dml-tools` will be serialized. `dml-tools` can either have either of the below values- - A boolean value- true or false. This is a global value to enable or disable all DML tools and `UserProvidedAllTools` is written in JSON - A dictionary object of individual tool name and boolean values. This contains individual tool specific values and only the specified tools will be taken for JSON writing and unspecified tool names will be ignored. ## How was this tested? - [x] Integration Tests - [x] Unit Tests - [x] Manual Tests Sample scenarios for testing- **Scenario 1:** Enable all tools using a single boolean value `"dml-tools": true` Note: default is true so even if `dml-tools` unspecified it will default to true. **Scenario 2:** Disable `execute-entities` only and keep other tools enabled as default. ``` "dml-tools": { "execute-entities": false } ``` **Scenario 3:** Use full list of tools and enable or disable them ``` "dml-tools": { "execute-entity": true, "delete-record": false, "update-record": false, "read-records": false, "describe-entities": true } ``` --------- Co-authored-by: Aniruddh Munde --- src/Cli.Tests/ConfigGeneratorTests.cs | 10 +--- ...stMethodsAndGraphQLOperations.verified.txt | 14 ++--- ...tyWithSourceAsStoredProcedure.verified.txt | 14 ++--- ...tityWithSourceWithDefaultType.verified.txt | 14 ++--- ...dingEntityWithoutIEnumerables.verified.txt | 14 ++--- ...ests.TestInitForCosmosDBNoSql.verified.txt | 14 ++--- ...toredProcedureWithRestMethods.verified.txt | 14 ++--- ...stMethodsAndGraphQLOperations.verified.txt | 14 ++--- ...itTests.CosmosDbNoSqlDatabase.verified.txt | 14 ++--- ...ts.CosmosDbPostgreSqlDatabase.verified.txt | 14 ++--- ...ionProviders_171ea8114ff71814.verified.txt | 14 ++--- ...ionProviders_2df7a1794712f154.verified.txt | 14 ++--- ...ionProviders_59fe1a10aa78899d.verified.txt | 14 ++--- ...ionProviders_b95b637ea87f16a7.verified.txt | 14 ++--- ...ionProviders_daacbd948b7ef72f.verified.txt | 14 ++--- ...tStartingSlashWillHaveItAdded.verified.txt | 14 ++--- .../InitTests.MsSQLDatabase.verified.txt | 14 ++--- ...tStartingSlashWillHaveItAdded.verified.txt | 14 ++--- ...ConfigWithoutConnectionString.verified.txt | 14 ++--- ...lCharactersInConnectionString.verified.txt | 14 ++--- ...ationOptions_0546bef37027a950.verified.txt | 14 ++--- ...ationOptions_0ac567dd32a2e8f5.verified.txt | 14 ++--- ...ationOptions_0c06949221514e77.verified.txt | 14 ++--- ...ationOptions_18667ab7db033e9d.verified.txt | 14 ++--- ...ationOptions_2f42f44c328eb020.verified.txt | 14 ++--- ...ationOptions_3243d3f3441fdcc1.verified.txt | 14 ++--- ...ationOptions_53350b8b47df2112.verified.txt | 14 ++--- ...ationOptions_6584e0ec46b8a11d.verified.txt | 14 ++--- ...ationOptions_81cc88db3d4eecfb.verified.txt | 14 ++--- ...ationOptions_8ea187616dbb5577.verified.txt | 14 ++--- ...ationOptions_905845c29560a3ef.verified.txt | 14 ++--- ...ationOptions_b2fd24fab5b80917.verified.txt | 14 ++--- ...ationOptions_bd7cd088755287c9.verified.txt | 14 ++--- ...ationOptions_d2eccba2f836b380.verified.txt | 14 ++--- ...ationOptions_d463eed7fe5e4bbe.verified.txt | 14 ++--- ...ationOptions_d5520dd5c33f7b8d.verified.txt | 14 ++--- ...ationOptions_eab4a6010e602b59.verified.txt | 14 ++--- ...ationOptions_ecaa688829b4030e.verified.txt | 14 ++--- .../Converters/DmlToolsConfigConverter.cs | 4 +- src/Config/ObjectModel/DmlToolsConfig.cs | 60 ++++++++++++------- src/Config/ObjectModel/McpRuntimeOptions.cs | 7 ++- ...ReadingRuntimeConfigForCosmos.verified.txt | 14 ++--- ...tReadingRuntimeConfigForMsSql.verified.txt | 14 ++--- ...tReadingRuntimeConfigForMySql.verified.txt | 14 ++--- ...ingRuntimeConfigForPostgreSql.verified.txt | 14 ++--- 45 files changed, 333 insertions(+), 322 deletions(-) diff --git a/src/Cli.Tests/ConfigGeneratorTests.cs b/src/Cli.Tests/ConfigGeneratorTests.cs index 9f56d5964f..604860eb69 100644 --- a/src/Cli.Tests/ConfigGeneratorTests.cs +++ b/src/Cli.Tests/ConfigGeneratorTests.cs @@ -165,15 +165,7 @@ public void TestSpecialCharactersInConnectionString() }, ""mcp"": { ""enabled"": true, - ""path"": ""/mcp"", - ""dml-tools"":{ - ""describe-entities"": true, - ""create-record"": true, - ""read-records"": true, - ""update-record"": true, - ""delete-record"": true, - ""execute-entity"":true - } + ""path"": ""/mcp"" }, ""host"": { ""cors"": { diff --git a/src/Cli.Tests/Snapshots/EndToEndTests.TestAddingStoredProcedureWithRestMethodsAndGraphQLOperations.verified.txt b/src/Cli.Tests/Snapshots/EndToEndTests.TestAddingStoredProcedureWithRestMethodsAndGraphQLOperations.verified.txt index 12316672ab..b072d5e5a0 100644 --- a/src/Cli.Tests/Snapshots/EndToEndTests.TestAddingStoredProcedureWithRestMethodsAndGraphQLOperations.verified.txt +++ b/src/Cli.Tests/Snapshots/EndToEndTests.TestAddingStoredProcedureWithRestMethodsAndGraphQLOperations.verified.txt @@ -27,13 +27,13 @@ UpdateRecord: true, DeleteRecord: true, ExecuteEntity: true, - UserProvidedAllToolsEnabled: false, - UserProvidedDescribeEntities: true, - UserProvidedCreateRecord: true, - UserProvidedReadRecords: true, - UserProvidedUpdateRecord: true, - UserProvidedDeleteRecord: true, - UserProvidedExecuteEntity: true + UserProvidedAllTools: false, + UserProvidedDescribeEntities: false, + UserProvidedCreateRecord: false, + UserProvidedReadRecords: false, + UserProvidedUpdateRecord: false, + UserProvidedDeleteRecord: false, + UserProvidedExecuteEntity: false } }, Host: { diff --git a/src/Cli.Tests/Snapshots/EndToEndTests.TestConfigGeneratedAfterAddingEntityWithSourceAsStoredProcedure.verified.txt b/src/Cli.Tests/Snapshots/EndToEndTests.TestConfigGeneratedAfterAddingEntityWithSourceAsStoredProcedure.verified.txt index 218d4b46e8..ca7211c485 100644 --- a/src/Cli.Tests/Snapshots/EndToEndTests.TestConfigGeneratedAfterAddingEntityWithSourceAsStoredProcedure.verified.txt +++ b/src/Cli.Tests/Snapshots/EndToEndTests.TestConfigGeneratedAfterAddingEntityWithSourceAsStoredProcedure.verified.txt @@ -27,13 +27,13 @@ UpdateRecord: true, DeleteRecord: true, ExecuteEntity: true, - UserProvidedAllToolsEnabled: false, - UserProvidedDescribeEntities: true, - UserProvidedCreateRecord: true, - UserProvidedReadRecords: true, - UserProvidedUpdateRecord: true, - UserProvidedDeleteRecord: true, - UserProvidedExecuteEntity: true + UserProvidedAllTools: false, + UserProvidedDescribeEntities: false, + UserProvidedCreateRecord: false, + UserProvidedReadRecords: false, + UserProvidedUpdateRecord: false, + UserProvidedDeleteRecord: false, + UserProvidedExecuteEntity: false } }, Host: { diff --git a/src/Cli.Tests/Snapshots/EndToEndTests.TestConfigGeneratedAfterAddingEntityWithSourceWithDefaultType.verified.txt b/src/Cli.Tests/Snapshots/EndToEndTests.TestConfigGeneratedAfterAddingEntityWithSourceWithDefaultType.verified.txt index 9ea6e5e143..d7aadee93c 100644 --- a/src/Cli.Tests/Snapshots/EndToEndTests.TestConfigGeneratedAfterAddingEntityWithSourceWithDefaultType.verified.txt +++ b/src/Cli.Tests/Snapshots/EndToEndTests.TestConfigGeneratedAfterAddingEntityWithSourceWithDefaultType.verified.txt @@ -27,13 +27,13 @@ UpdateRecord: true, DeleteRecord: true, ExecuteEntity: true, - UserProvidedAllToolsEnabled: false, - UserProvidedDescribeEntities: true, - UserProvidedCreateRecord: true, - UserProvidedReadRecords: true, - UserProvidedUpdateRecord: true, - UserProvidedDeleteRecord: true, - UserProvidedExecuteEntity: true + UserProvidedAllTools: false, + UserProvidedDescribeEntities: false, + UserProvidedCreateRecord: false, + UserProvidedReadRecords: false, + UserProvidedUpdateRecord: false, + UserProvidedDeleteRecord: false, + UserProvidedExecuteEntity: false } }, Host: { diff --git a/src/Cli.Tests/Snapshots/EndToEndTests.TestConfigGeneratedAfterAddingEntityWithoutIEnumerables.verified.txt b/src/Cli.Tests/Snapshots/EndToEndTests.TestConfigGeneratedAfterAddingEntityWithoutIEnumerables.verified.txt index 2685db11db..331a040a45 100644 --- a/src/Cli.Tests/Snapshots/EndToEndTests.TestConfigGeneratedAfterAddingEntityWithoutIEnumerables.verified.txt +++ b/src/Cli.Tests/Snapshots/EndToEndTests.TestConfigGeneratedAfterAddingEntityWithoutIEnumerables.verified.txt @@ -27,13 +27,13 @@ UpdateRecord: true, DeleteRecord: true, ExecuteEntity: true, - UserProvidedAllToolsEnabled: false, - UserProvidedDescribeEntities: true, - UserProvidedCreateRecord: true, - UserProvidedReadRecords: true, - UserProvidedUpdateRecord: true, - UserProvidedDeleteRecord: true, - UserProvidedExecuteEntity: true + UserProvidedAllTools: false, + UserProvidedDescribeEntities: false, + UserProvidedCreateRecord: false, + UserProvidedReadRecords: false, + UserProvidedUpdateRecord: false, + UserProvidedDeleteRecord: false, + UserProvidedExecuteEntity: false } }, Host: { diff --git a/src/Cli.Tests/Snapshots/EndToEndTests.TestInitForCosmosDBNoSql.verified.txt b/src/Cli.Tests/Snapshots/EndToEndTests.TestInitForCosmosDBNoSql.verified.txt index d13807441c..ff3f25d357 100644 --- a/src/Cli.Tests/Snapshots/EndToEndTests.TestInitForCosmosDBNoSql.verified.txt +++ b/src/Cli.Tests/Snapshots/EndToEndTests.TestInitForCosmosDBNoSql.verified.txt @@ -28,13 +28,13 @@ UpdateRecord: true, DeleteRecord: true, ExecuteEntity: true, - UserProvidedAllToolsEnabled: false, - UserProvidedDescribeEntities: true, - UserProvidedCreateRecord: true, - UserProvidedReadRecords: true, - UserProvidedUpdateRecord: true, - UserProvidedDeleteRecord: true, - UserProvidedExecuteEntity: true + UserProvidedAllTools: false, + UserProvidedDescribeEntities: false, + UserProvidedCreateRecord: false, + UserProvidedReadRecords: false, + UserProvidedUpdateRecord: false, + UserProvidedDeleteRecord: false, + UserProvidedExecuteEntity: false } }, Host: { diff --git a/src/Cli.Tests/Snapshots/EndToEndTests.TestUpdatingStoredProcedureWithRestMethods.verified.txt b/src/Cli.Tests/Snapshots/EndToEndTests.TestUpdatingStoredProcedureWithRestMethods.verified.txt index c1f5280277..a04dc2fe36 100644 --- a/src/Cli.Tests/Snapshots/EndToEndTests.TestUpdatingStoredProcedureWithRestMethods.verified.txt +++ b/src/Cli.Tests/Snapshots/EndToEndTests.TestUpdatingStoredProcedureWithRestMethods.verified.txt @@ -27,13 +27,13 @@ UpdateRecord: true, DeleteRecord: true, ExecuteEntity: true, - UserProvidedAllToolsEnabled: false, - UserProvidedDescribeEntities: true, - UserProvidedCreateRecord: true, - UserProvidedReadRecords: true, - UserProvidedUpdateRecord: true, - UserProvidedDeleteRecord: true, - UserProvidedExecuteEntity: true + UserProvidedAllTools: false, + UserProvidedDescribeEntities: false, + UserProvidedCreateRecord: false, + UserProvidedReadRecords: false, + UserProvidedUpdateRecord: false, + UserProvidedDeleteRecord: false, + UserProvidedExecuteEntity: false } }, Host: { diff --git a/src/Cli.Tests/Snapshots/EndToEndTests.TestUpdatingStoredProcedureWithRestMethodsAndGraphQLOperations.verified.txt b/src/Cli.Tests/Snapshots/EndToEndTests.TestUpdatingStoredProcedureWithRestMethodsAndGraphQLOperations.verified.txt index 92d6369214..cfa928a025 100644 --- a/src/Cli.Tests/Snapshots/EndToEndTests.TestUpdatingStoredProcedureWithRestMethodsAndGraphQLOperations.verified.txt +++ b/src/Cli.Tests/Snapshots/EndToEndTests.TestUpdatingStoredProcedureWithRestMethodsAndGraphQLOperations.verified.txt @@ -27,13 +27,13 @@ UpdateRecord: true, DeleteRecord: true, ExecuteEntity: true, - UserProvidedAllToolsEnabled: false, - UserProvidedDescribeEntities: true, - UserProvidedCreateRecord: true, - UserProvidedReadRecords: true, - UserProvidedUpdateRecord: true, - UserProvidedDeleteRecord: true, - UserProvidedExecuteEntity: true + UserProvidedAllTools: false, + UserProvidedDescribeEntities: false, + UserProvidedCreateRecord: false, + UserProvidedReadRecords: false, + UserProvidedUpdateRecord: false, + UserProvidedDeleteRecord: false, + UserProvidedExecuteEntity: false } }, Host: { diff --git a/src/Cli.Tests/Snapshots/InitTests.CosmosDbNoSqlDatabase.verified.txt b/src/Cli.Tests/Snapshots/InitTests.CosmosDbNoSqlDatabase.verified.txt index 50e3aa35b8..b9b040aa2f 100644 --- a/src/Cli.Tests/Snapshots/InitTests.CosmosDbNoSqlDatabase.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.CosmosDbNoSqlDatabase.verified.txt @@ -28,13 +28,13 @@ UpdateRecord: true, DeleteRecord: true, ExecuteEntity: true, - UserProvidedAllToolsEnabled: true, - UserProvidedDescribeEntities: true, - UserProvidedCreateRecord: true, - UserProvidedReadRecords: true, - UserProvidedUpdateRecord: true, - UserProvidedDeleteRecord: true, - UserProvidedExecuteEntity: true + UserProvidedAllTools: false, + UserProvidedDescribeEntities: false, + UserProvidedCreateRecord: false, + UserProvidedReadRecords: false, + UserProvidedUpdateRecord: false, + UserProvidedDeleteRecord: false, + UserProvidedExecuteEntity: false } }, Host: { diff --git a/src/Cli.Tests/Snapshots/InitTests.CosmosDbPostgreSqlDatabase.verified.txt b/src/Cli.Tests/Snapshots/InitTests.CosmosDbPostgreSqlDatabase.verified.txt index 496bf5f97c..65b03f6293 100644 --- a/src/Cli.Tests/Snapshots/InitTests.CosmosDbPostgreSqlDatabase.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.CosmosDbPostgreSqlDatabase.verified.txt @@ -24,13 +24,13 @@ UpdateRecord: true, DeleteRecord: true, ExecuteEntity: true, - UserProvidedAllToolsEnabled: true, - UserProvidedDescribeEntities: true, - UserProvidedCreateRecord: true, - UserProvidedReadRecords: true, - UserProvidedUpdateRecord: true, - UserProvidedDeleteRecord: true, - UserProvidedExecuteEntity: true + UserProvidedAllTools: false, + UserProvidedDescribeEntities: false, + UserProvidedCreateRecord: false, + UserProvidedReadRecords: false, + UserProvidedUpdateRecord: false, + UserProvidedDeleteRecord: false, + UserProvidedExecuteEntity: false } }, Host: { diff --git a/src/Cli.Tests/Snapshots/InitTests.EnsureCorrectConfigGenerationWithDifferentAuthenticationProviders_171ea8114ff71814.verified.txt b/src/Cli.Tests/Snapshots/InitTests.EnsureCorrectConfigGenerationWithDifferentAuthenticationProviders_171ea8114ff71814.verified.txt index db26433ea0..978d1a253b 100644 --- a/src/Cli.Tests/Snapshots/InitTests.EnsureCorrectConfigGenerationWithDifferentAuthenticationProviders_171ea8114ff71814.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.EnsureCorrectConfigGenerationWithDifferentAuthenticationProviders_171ea8114ff71814.verified.txt @@ -27,13 +27,13 @@ UpdateRecord: true, DeleteRecord: true, ExecuteEntity: true, - UserProvidedAllToolsEnabled: true, - UserProvidedDescribeEntities: true, - UserProvidedCreateRecord: true, - UserProvidedReadRecords: true, - UserProvidedUpdateRecord: true, - UserProvidedDeleteRecord: true, - UserProvidedExecuteEntity: true + UserProvidedAllTools: false, + UserProvidedDescribeEntities: false, + UserProvidedCreateRecord: false, + UserProvidedReadRecords: false, + UserProvidedUpdateRecord: false, + UserProvidedDeleteRecord: false, + UserProvidedExecuteEntity: false } }, Host: { diff --git a/src/Cli.Tests/Snapshots/InitTests.EnsureCorrectConfigGenerationWithDifferentAuthenticationProviders_2df7a1794712f154.verified.txt b/src/Cli.Tests/Snapshots/InitTests.EnsureCorrectConfigGenerationWithDifferentAuthenticationProviders_2df7a1794712f154.verified.txt index 1471284e3a..402bf4d2bc 100644 --- a/src/Cli.Tests/Snapshots/InitTests.EnsureCorrectConfigGenerationWithDifferentAuthenticationProviders_2df7a1794712f154.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.EnsureCorrectConfigGenerationWithDifferentAuthenticationProviders_2df7a1794712f154.verified.txt @@ -27,13 +27,13 @@ UpdateRecord: true, DeleteRecord: true, ExecuteEntity: true, - UserProvidedAllToolsEnabled: true, - UserProvidedDescribeEntities: true, - UserProvidedCreateRecord: true, - UserProvidedReadRecords: true, - UserProvidedUpdateRecord: true, - UserProvidedDeleteRecord: true, - UserProvidedExecuteEntity: true + UserProvidedAllTools: false, + UserProvidedDescribeEntities: false, + UserProvidedCreateRecord: false, + UserProvidedReadRecords: false, + UserProvidedUpdateRecord: false, + UserProvidedDeleteRecord: false, + UserProvidedExecuteEntity: false } }, Host: { diff --git a/src/Cli.Tests/Snapshots/InitTests.EnsureCorrectConfigGenerationWithDifferentAuthenticationProviders_59fe1a10aa78899d.verified.txt b/src/Cli.Tests/Snapshots/InitTests.EnsureCorrectConfigGenerationWithDifferentAuthenticationProviders_59fe1a10aa78899d.verified.txt index 6c6b6fa055..ab71a40f03 100644 --- a/src/Cli.Tests/Snapshots/InitTests.EnsureCorrectConfigGenerationWithDifferentAuthenticationProviders_59fe1a10aa78899d.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.EnsureCorrectConfigGenerationWithDifferentAuthenticationProviders_59fe1a10aa78899d.verified.txt @@ -27,13 +27,13 @@ UpdateRecord: true, DeleteRecord: true, ExecuteEntity: true, - UserProvidedAllToolsEnabled: true, - UserProvidedDescribeEntities: true, - UserProvidedCreateRecord: true, - UserProvidedReadRecords: true, - UserProvidedUpdateRecord: true, - UserProvidedDeleteRecord: true, - UserProvidedExecuteEntity: true + UserProvidedAllTools: false, + UserProvidedDescribeEntities: false, + UserProvidedCreateRecord: false, + UserProvidedReadRecords: false, + UserProvidedUpdateRecord: false, + UserProvidedDeleteRecord: false, + UserProvidedExecuteEntity: false } }, Host: { diff --git a/src/Cli.Tests/Snapshots/InitTests.EnsureCorrectConfigGenerationWithDifferentAuthenticationProviders_b95b637ea87f16a7.verified.txt b/src/Cli.Tests/Snapshots/InitTests.EnsureCorrectConfigGenerationWithDifferentAuthenticationProviders_b95b637ea87f16a7.verified.txt index 11f92aa0bb..25e3976685 100644 --- a/src/Cli.Tests/Snapshots/InitTests.EnsureCorrectConfigGenerationWithDifferentAuthenticationProviders_b95b637ea87f16a7.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.EnsureCorrectConfigGenerationWithDifferentAuthenticationProviders_b95b637ea87f16a7.verified.txt @@ -27,13 +27,13 @@ UpdateRecord: true, DeleteRecord: true, ExecuteEntity: true, - UserProvidedAllToolsEnabled: true, - UserProvidedDescribeEntities: true, - UserProvidedCreateRecord: true, - UserProvidedReadRecords: true, - UserProvidedUpdateRecord: true, - UserProvidedDeleteRecord: true, - UserProvidedExecuteEntity: true + UserProvidedAllTools: false, + UserProvidedDescribeEntities: false, + UserProvidedCreateRecord: false, + UserProvidedReadRecords: false, + UserProvidedUpdateRecord: false, + UserProvidedDeleteRecord: false, + UserProvidedExecuteEntity: false } }, Host: { diff --git a/src/Cli.Tests/Snapshots/InitTests.EnsureCorrectConfigGenerationWithDifferentAuthenticationProviders_daacbd948b7ef72f.verified.txt b/src/Cli.Tests/Snapshots/InitTests.EnsureCorrectConfigGenerationWithDifferentAuthenticationProviders_daacbd948b7ef72f.verified.txt index 5d24ae9d2f..140f017b78 100644 --- a/src/Cli.Tests/Snapshots/InitTests.EnsureCorrectConfigGenerationWithDifferentAuthenticationProviders_daacbd948b7ef72f.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.EnsureCorrectConfigGenerationWithDifferentAuthenticationProviders_daacbd948b7ef72f.verified.txt @@ -27,13 +27,13 @@ UpdateRecord: true, DeleteRecord: true, ExecuteEntity: true, - UserProvidedAllToolsEnabled: true, - UserProvidedDescribeEntities: true, - UserProvidedCreateRecord: true, - UserProvidedReadRecords: true, - UserProvidedUpdateRecord: true, - UserProvidedDeleteRecord: true, - UserProvidedExecuteEntity: true + UserProvidedAllTools: false, + UserProvidedDescribeEntities: false, + UserProvidedCreateRecord: false, + UserProvidedReadRecords: false, + UserProvidedUpdateRecord: false, + UserProvidedDeleteRecord: false, + UserProvidedExecuteEntity: false } }, Host: { diff --git a/src/Cli.Tests/Snapshots/InitTests.GraphQLPathWithoutStartingSlashWillHaveItAdded.verified.txt b/src/Cli.Tests/Snapshots/InitTests.GraphQLPathWithoutStartingSlashWillHaveItAdded.verified.txt index cd721f65d2..bc6b6cfecb 100644 --- a/src/Cli.Tests/Snapshots/InitTests.GraphQLPathWithoutStartingSlashWillHaveItAdded.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.GraphQLPathWithoutStartingSlashWillHaveItAdded.verified.txt @@ -27,13 +27,13 @@ UpdateRecord: true, DeleteRecord: true, ExecuteEntity: true, - UserProvidedAllToolsEnabled: true, - UserProvidedDescribeEntities: true, - UserProvidedCreateRecord: true, - UserProvidedReadRecords: true, - UserProvidedUpdateRecord: true, - UserProvidedDeleteRecord: true, - UserProvidedExecuteEntity: true + UserProvidedAllTools: false, + UserProvidedDescribeEntities: false, + UserProvidedCreateRecord: false, + UserProvidedReadRecords: false, + UserProvidedUpdateRecord: false, + UserProvidedDeleteRecord: false, + UserProvidedExecuteEntity: false } }, Host: { diff --git a/src/Cli.Tests/Snapshots/InitTests.MsSQLDatabase.verified.txt b/src/Cli.Tests/Snapshots/InitTests.MsSQLDatabase.verified.txt index d304df7085..3078fb644f 100644 --- a/src/Cli.Tests/Snapshots/InitTests.MsSQLDatabase.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.MsSQLDatabase.verified.txt @@ -27,13 +27,13 @@ UpdateRecord: true, DeleteRecord: true, ExecuteEntity: true, - UserProvidedAllToolsEnabled: true, - UserProvidedDescribeEntities: true, - UserProvidedCreateRecord: true, - UserProvidedReadRecords: true, - UserProvidedUpdateRecord: true, - UserProvidedDeleteRecord: true, - UserProvidedExecuteEntity: true + UserProvidedAllTools: false, + UserProvidedDescribeEntities: false, + UserProvidedCreateRecord: false, + UserProvidedReadRecords: false, + UserProvidedUpdateRecord: false, + UserProvidedDeleteRecord: false, + UserProvidedExecuteEntity: false } }, Host: { diff --git a/src/Cli.Tests/Snapshots/InitTests.RestPathWithoutStartingSlashWillHaveItAdded.verified.txt b/src/Cli.Tests/Snapshots/InitTests.RestPathWithoutStartingSlashWillHaveItAdded.verified.txt index ff7b282c32..c888431526 100644 --- a/src/Cli.Tests/Snapshots/InitTests.RestPathWithoutStartingSlashWillHaveItAdded.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.RestPathWithoutStartingSlashWillHaveItAdded.verified.txt @@ -27,13 +27,13 @@ UpdateRecord: true, DeleteRecord: true, ExecuteEntity: true, - UserProvidedAllToolsEnabled: true, - UserProvidedDescribeEntities: true, - UserProvidedCreateRecord: true, - UserProvidedReadRecords: true, - UserProvidedUpdateRecord: true, - UserProvidedDeleteRecord: true, - UserProvidedExecuteEntity: true + UserProvidedAllTools: false, + UserProvidedDescribeEntities: false, + UserProvidedCreateRecord: false, + UserProvidedReadRecords: false, + UserProvidedUpdateRecord: false, + UserProvidedDeleteRecord: false, + UserProvidedExecuteEntity: false } }, Host: { diff --git a/src/Cli.Tests/Snapshots/InitTests.TestInitializingConfigWithoutConnectionString.verified.txt b/src/Cli.Tests/Snapshots/InitTests.TestInitializingConfigWithoutConnectionString.verified.txt index 6714b2a50d..0273dcc976 100644 --- a/src/Cli.Tests/Snapshots/InitTests.TestInitializingConfigWithoutConnectionString.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.TestInitializingConfigWithoutConnectionString.verified.txt @@ -27,13 +27,13 @@ UpdateRecord: true, DeleteRecord: true, ExecuteEntity: true, - UserProvidedAllToolsEnabled: true, - UserProvidedDescribeEntities: true, - UserProvidedCreateRecord: true, - UserProvidedReadRecords: true, - UserProvidedUpdateRecord: true, - UserProvidedDeleteRecord: true, - UserProvidedExecuteEntity: true + UserProvidedAllTools: false, + UserProvidedDescribeEntities: false, + UserProvidedCreateRecord: false, + UserProvidedReadRecords: false, + UserProvidedUpdateRecord: false, + UserProvidedDeleteRecord: false, + UserProvidedExecuteEntity: false } }, Host: { diff --git a/src/Cli.Tests/Snapshots/InitTests.TestSpecialCharactersInConnectionString.verified.txt b/src/Cli.Tests/Snapshots/InitTests.TestSpecialCharactersInConnectionString.verified.txt index 6c6b6fa055..ab71a40f03 100644 --- a/src/Cli.Tests/Snapshots/InitTests.TestSpecialCharactersInConnectionString.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.TestSpecialCharactersInConnectionString.verified.txt @@ -27,13 +27,13 @@ UpdateRecord: true, DeleteRecord: true, ExecuteEntity: true, - UserProvidedAllToolsEnabled: true, - UserProvidedDescribeEntities: true, - UserProvidedCreateRecord: true, - UserProvidedReadRecords: true, - UserProvidedUpdateRecord: true, - UserProvidedDeleteRecord: true, - UserProvidedExecuteEntity: true + UserProvidedAllTools: false, + UserProvidedDescribeEntities: false, + UserProvidedCreateRecord: false, + UserProvidedReadRecords: false, + UserProvidedUpdateRecord: false, + UserProvidedDeleteRecord: false, + UserProvidedExecuteEntity: false } }, Host: { diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_0546bef37027a950.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_0546bef37027a950.verified.txt index 397a36867b..86a0716003 100644 --- a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_0546bef37027a950.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_0546bef37027a950.verified.txt @@ -27,13 +27,13 @@ UpdateRecord: true, DeleteRecord: true, ExecuteEntity: true, - UserProvidedAllToolsEnabled: true, - UserProvidedDescribeEntities: true, - UserProvidedCreateRecord: true, - UserProvidedReadRecords: true, - UserProvidedUpdateRecord: true, - UserProvidedDeleteRecord: true, - UserProvidedExecuteEntity: true + UserProvidedAllTools: false, + UserProvidedDescribeEntities: false, + UserProvidedCreateRecord: false, + UserProvidedReadRecords: false, + UserProvidedUpdateRecord: false, + UserProvidedDeleteRecord: false, + UserProvidedExecuteEntity: false } }, Host: { diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_0ac567dd32a2e8f5.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_0ac567dd32a2e8f5.verified.txt index d304df7085..3078fb644f 100644 --- a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_0ac567dd32a2e8f5.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_0ac567dd32a2e8f5.verified.txt @@ -27,13 +27,13 @@ UpdateRecord: true, DeleteRecord: true, ExecuteEntity: true, - UserProvidedAllToolsEnabled: true, - UserProvidedDescribeEntities: true, - UserProvidedCreateRecord: true, - UserProvidedReadRecords: true, - UserProvidedUpdateRecord: true, - UserProvidedDeleteRecord: true, - UserProvidedExecuteEntity: true + UserProvidedAllTools: false, + UserProvidedDescribeEntities: false, + UserProvidedCreateRecord: false, + UserProvidedReadRecords: false, + UserProvidedUpdateRecord: false, + UserProvidedDeleteRecord: false, + UserProvidedExecuteEntity: false } }, Host: { diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_0c06949221514e77.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_0c06949221514e77.verified.txt index ff833e5198..aac85044f9 100644 --- a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_0c06949221514e77.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_0c06949221514e77.verified.txt @@ -32,13 +32,13 @@ UpdateRecord: true, DeleteRecord: true, ExecuteEntity: true, - UserProvidedAllToolsEnabled: true, - UserProvidedDescribeEntities: true, - UserProvidedCreateRecord: true, - UserProvidedReadRecords: true, - UserProvidedUpdateRecord: true, - UserProvidedDeleteRecord: true, - UserProvidedExecuteEntity: true + UserProvidedAllTools: false, + UserProvidedDescribeEntities: false, + UserProvidedCreateRecord: false, + UserProvidedReadRecords: false, + UserProvidedUpdateRecord: false, + UserProvidedDeleteRecord: false, + UserProvidedExecuteEntity: false } }, Host: { diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_18667ab7db033e9d.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_18667ab7db033e9d.verified.txt index faf59ef911..c7904175e0 100644 --- a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_18667ab7db033e9d.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_18667ab7db033e9d.verified.txt @@ -24,13 +24,13 @@ UpdateRecord: true, DeleteRecord: true, ExecuteEntity: true, - UserProvidedAllToolsEnabled: true, - UserProvidedDescribeEntities: true, - UserProvidedCreateRecord: true, - UserProvidedReadRecords: true, - UserProvidedUpdateRecord: true, - UserProvidedDeleteRecord: true, - UserProvidedExecuteEntity: true + UserProvidedAllTools: false, + UserProvidedDescribeEntities: false, + UserProvidedCreateRecord: false, + UserProvidedReadRecords: false, + UserProvidedUpdateRecord: false, + UserProvidedDeleteRecord: false, + UserProvidedExecuteEntity: false } }, Host: { diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_2f42f44c328eb020.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_2f42f44c328eb020.verified.txt index 397a36867b..86a0716003 100644 --- a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_2f42f44c328eb020.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_2f42f44c328eb020.verified.txt @@ -27,13 +27,13 @@ UpdateRecord: true, DeleteRecord: true, ExecuteEntity: true, - UserProvidedAllToolsEnabled: true, - UserProvidedDescribeEntities: true, - UserProvidedCreateRecord: true, - UserProvidedReadRecords: true, - UserProvidedUpdateRecord: true, - UserProvidedDeleteRecord: true, - UserProvidedExecuteEntity: true + UserProvidedAllTools: false, + UserProvidedDescribeEntities: false, + UserProvidedCreateRecord: false, + UserProvidedReadRecords: false, + UserProvidedUpdateRecord: false, + UserProvidedDeleteRecord: false, + UserProvidedExecuteEntity: false } }, Host: { diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_3243d3f3441fdcc1.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_3243d3f3441fdcc1.verified.txt index faf59ef911..c7904175e0 100644 --- a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_3243d3f3441fdcc1.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_3243d3f3441fdcc1.verified.txt @@ -24,13 +24,13 @@ UpdateRecord: true, DeleteRecord: true, ExecuteEntity: true, - UserProvidedAllToolsEnabled: true, - UserProvidedDescribeEntities: true, - UserProvidedCreateRecord: true, - UserProvidedReadRecords: true, - UserProvidedUpdateRecord: true, - UserProvidedDeleteRecord: true, - UserProvidedExecuteEntity: true + UserProvidedAllTools: false, + UserProvidedDescribeEntities: false, + UserProvidedCreateRecord: false, + UserProvidedReadRecords: false, + UserProvidedUpdateRecord: false, + UserProvidedDeleteRecord: false, + UserProvidedExecuteEntity: false } }, Host: { diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_53350b8b47df2112.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_53350b8b47df2112.verified.txt index bc08f75e48..d70704315e 100644 --- a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_53350b8b47df2112.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_53350b8b47df2112.verified.txt @@ -24,13 +24,13 @@ UpdateRecord: true, DeleteRecord: true, ExecuteEntity: true, - UserProvidedAllToolsEnabled: true, - UserProvidedDescribeEntities: true, - UserProvidedCreateRecord: true, - UserProvidedReadRecords: true, - UserProvidedUpdateRecord: true, - UserProvidedDeleteRecord: true, - UserProvidedExecuteEntity: true + UserProvidedAllTools: false, + UserProvidedDescribeEntities: false, + UserProvidedCreateRecord: false, + UserProvidedReadRecords: false, + UserProvidedUpdateRecord: false, + UserProvidedDeleteRecord: false, + UserProvidedExecuteEntity: false } }, Host: { diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_6584e0ec46b8a11d.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_6584e0ec46b8a11d.verified.txt index 85e52e4e7f..ac3815f949 100644 --- a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_6584e0ec46b8a11d.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_6584e0ec46b8a11d.verified.txt @@ -28,13 +28,13 @@ UpdateRecord: true, DeleteRecord: true, ExecuteEntity: true, - UserProvidedAllToolsEnabled: true, - UserProvidedDescribeEntities: true, - UserProvidedCreateRecord: true, - UserProvidedReadRecords: true, - UserProvidedUpdateRecord: true, - UserProvidedDeleteRecord: true, - UserProvidedExecuteEntity: true + UserProvidedAllTools: false, + UserProvidedDescribeEntities: false, + UserProvidedCreateRecord: false, + UserProvidedReadRecords: false, + UserProvidedUpdateRecord: false, + UserProvidedDeleteRecord: false, + UserProvidedExecuteEntity: false } }, Host: { diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_81cc88db3d4eecfb.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_81cc88db3d4eecfb.verified.txt index 1bcf97c8d7..fd6a494ba3 100644 --- a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_81cc88db3d4eecfb.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_81cc88db3d4eecfb.verified.txt @@ -32,13 +32,13 @@ UpdateRecord: true, DeleteRecord: true, ExecuteEntity: true, - UserProvidedAllToolsEnabled: true, - UserProvidedDescribeEntities: true, - UserProvidedCreateRecord: true, - UserProvidedReadRecords: true, - UserProvidedUpdateRecord: true, - UserProvidedDeleteRecord: true, - UserProvidedExecuteEntity: true + UserProvidedAllTools: false, + UserProvidedDescribeEntities: false, + UserProvidedCreateRecord: false, + UserProvidedReadRecords: false, + UserProvidedUpdateRecord: false, + UserProvidedDeleteRecord: false, + UserProvidedExecuteEntity: false } }, Host: { diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_8ea187616dbb5577.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_8ea187616dbb5577.verified.txt index 9d14eb779f..8044f643ac 100644 --- a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_8ea187616dbb5577.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_8ea187616dbb5577.verified.txt @@ -24,13 +24,13 @@ UpdateRecord: true, DeleteRecord: true, ExecuteEntity: true, - UserProvidedAllToolsEnabled: true, - UserProvidedDescribeEntities: true, - UserProvidedCreateRecord: true, - UserProvidedReadRecords: true, - UserProvidedUpdateRecord: true, - UserProvidedDeleteRecord: true, - UserProvidedExecuteEntity: true + UserProvidedAllTools: false, + UserProvidedDescribeEntities: false, + UserProvidedCreateRecord: false, + UserProvidedReadRecords: false, + UserProvidedUpdateRecord: false, + UserProvidedDeleteRecord: false, + UserProvidedExecuteEntity: false } }, Host: { diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_905845c29560a3ef.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_905845c29560a3ef.verified.txt index 397a36867b..86a0716003 100644 --- a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_905845c29560a3ef.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_905845c29560a3ef.verified.txt @@ -27,13 +27,13 @@ UpdateRecord: true, DeleteRecord: true, ExecuteEntity: true, - UserProvidedAllToolsEnabled: true, - UserProvidedDescribeEntities: true, - UserProvidedCreateRecord: true, - UserProvidedReadRecords: true, - UserProvidedUpdateRecord: true, - UserProvidedDeleteRecord: true, - UserProvidedExecuteEntity: true + UserProvidedAllTools: false, + UserProvidedDescribeEntities: false, + UserProvidedCreateRecord: false, + UserProvidedReadRecords: false, + UserProvidedUpdateRecord: false, + UserProvidedDeleteRecord: false, + UserProvidedExecuteEntity: false } }, Host: { diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_b2fd24fab5b80917.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_b2fd24fab5b80917.verified.txt index 85e52e4e7f..ac3815f949 100644 --- a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_b2fd24fab5b80917.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_b2fd24fab5b80917.verified.txt @@ -28,13 +28,13 @@ UpdateRecord: true, DeleteRecord: true, ExecuteEntity: true, - UserProvidedAllToolsEnabled: true, - UserProvidedDescribeEntities: true, - UserProvidedCreateRecord: true, - UserProvidedReadRecords: true, - UserProvidedUpdateRecord: true, - UserProvidedDeleteRecord: true, - UserProvidedExecuteEntity: true + UserProvidedAllTools: false, + UserProvidedDescribeEntities: false, + UserProvidedCreateRecord: false, + UserProvidedReadRecords: false, + UserProvidedUpdateRecord: false, + UserProvidedDeleteRecord: false, + UserProvidedExecuteEntity: false } }, Host: { diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_bd7cd088755287c9.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_bd7cd088755287c9.verified.txt index 85e52e4e7f..ac3815f949 100644 --- a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_bd7cd088755287c9.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_bd7cd088755287c9.verified.txt @@ -28,13 +28,13 @@ UpdateRecord: true, DeleteRecord: true, ExecuteEntity: true, - UserProvidedAllToolsEnabled: true, - UserProvidedDescribeEntities: true, - UserProvidedCreateRecord: true, - UserProvidedReadRecords: true, - UserProvidedUpdateRecord: true, - UserProvidedDeleteRecord: true, - UserProvidedExecuteEntity: true + UserProvidedAllTools: false, + UserProvidedDescribeEntities: false, + UserProvidedCreateRecord: false, + UserProvidedReadRecords: false, + UserProvidedUpdateRecord: false, + UserProvidedDeleteRecord: false, + UserProvidedExecuteEntity: false } }, Host: { diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_d2eccba2f836b380.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_d2eccba2f836b380.verified.txt index bc08f75e48..d70704315e 100644 --- a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_d2eccba2f836b380.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_d2eccba2f836b380.verified.txt @@ -24,13 +24,13 @@ UpdateRecord: true, DeleteRecord: true, ExecuteEntity: true, - UserProvidedAllToolsEnabled: true, - UserProvidedDescribeEntities: true, - UserProvidedCreateRecord: true, - UserProvidedReadRecords: true, - UserProvidedUpdateRecord: true, - UserProvidedDeleteRecord: true, - UserProvidedExecuteEntity: true + UserProvidedAllTools: false, + UserProvidedDescribeEntities: false, + UserProvidedCreateRecord: false, + UserProvidedReadRecords: false, + UserProvidedUpdateRecord: false, + UserProvidedDeleteRecord: false, + UserProvidedExecuteEntity: false } }, Host: { diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_d463eed7fe5e4bbe.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_d463eed7fe5e4bbe.verified.txt index 9d14eb779f..8044f643ac 100644 --- a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_d463eed7fe5e4bbe.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_d463eed7fe5e4bbe.verified.txt @@ -24,13 +24,13 @@ UpdateRecord: true, DeleteRecord: true, ExecuteEntity: true, - UserProvidedAllToolsEnabled: true, - UserProvidedDescribeEntities: true, - UserProvidedCreateRecord: true, - UserProvidedReadRecords: true, - UserProvidedUpdateRecord: true, - UserProvidedDeleteRecord: true, - UserProvidedExecuteEntity: true + UserProvidedAllTools: false, + UserProvidedDescribeEntities: false, + UserProvidedCreateRecord: false, + UserProvidedReadRecords: false, + UserProvidedUpdateRecord: false, + UserProvidedDeleteRecord: false, + UserProvidedExecuteEntity: false } }, Host: { diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_d5520dd5c33f7b8d.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_d5520dd5c33f7b8d.verified.txt index bc08f75e48..d70704315e 100644 --- a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_d5520dd5c33f7b8d.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_d5520dd5c33f7b8d.verified.txt @@ -24,13 +24,13 @@ UpdateRecord: true, DeleteRecord: true, ExecuteEntity: true, - UserProvidedAllToolsEnabled: true, - UserProvidedDescribeEntities: true, - UserProvidedCreateRecord: true, - UserProvidedReadRecords: true, - UserProvidedUpdateRecord: true, - UserProvidedDeleteRecord: true, - UserProvidedExecuteEntity: true + UserProvidedAllTools: false, + UserProvidedDescribeEntities: false, + UserProvidedCreateRecord: false, + UserProvidedReadRecords: false, + UserProvidedUpdateRecord: false, + UserProvidedDeleteRecord: false, + UserProvidedExecuteEntity: false } }, Host: { diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_eab4a6010e602b59.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_eab4a6010e602b59.verified.txt index faf59ef911..c7904175e0 100644 --- a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_eab4a6010e602b59.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_eab4a6010e602b59.verified.txt @@ -24,13 +24,13 @@ UpdateRecord: true, DeleteRecord: true, ExecuteEntity: true, - UserProvidedAllToolsEnabled: true, - UserProvidedDescribeEntities: true, - UserProvidedCreateRecord: true, - UserProvidedReadRecords: true, - UserProvidedUpdateRecord: true, - UserProvidedDeleteRecord: true, - UserProvidedExecuteEntity: true + UserProvidedAllTools: false, + UserProvidedDescribeEntities: false, + UserProvidedCreateRecord: false, + UserProvidedReadRecords: false, + UserProvidedUpdateRecord: false, + UserProvidedDeleteRecord: false, + UserProvidedExecuteEntity: false } }, Host: { diff --git a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_ecaa688829b4030e.verified.txt b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_ecaa688829b4030e.verified.txt index 9d14eb779f..8044f643ac 100644 --- a/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_ecaa688829b4030e.verified.txt +++ b/src/Cli.Tests/Snapshots/InitTests.VerifyCorrectConfigGenerationWithMultipleMutationOptions_ecaa688829b4030e.verified.txt @@ -24,13 +24,13 @@ UpdateRecord: true, DeleteRecord: true, ExecuteEntity: true, - UserProvidedAllToolsEnabled: true, - UserProvidedDescribeEntities: true, - UserProvidedCreateRecord: true, - UserProvidedReadRecords: true, - UserProvidedUpdateRecord: true, - UserProvidedDeleteRecord: true, - UserProvidedExecuteEntity: true + UserProvidedAllTools: false, + UserProvidedDescribeEntities: false, + UserProvidedCreateRecord: false, + UserProvidedReadRecords: false, + UserProvidedUpdateRecord: false, + UserProvidedDeleteRecord: false, + UserProvidedExecuteEntity: false } }, Host: { diff --git a/src/Config/Converters/DmlToolsConfigConverter.cs b/src/Config/Converters/DmlToolsConfigConverter.cs index 098bc64bc3..82ac3f6069 100644 --- a/src/Config/Converters/DmlToolsConfigConverter.cs +++ b/src/Config/Converters/DmlToolsConfigConverter.cs @@ -139,12 +139,12 @@ public override void Write(Utf8JsonWriter writer, DmlToolsConfig? value, JsonSer // Only write the boolean value if it's provided by user // This prevents writing "dml-tools": true when it's the default - if (!hasIndividualSettings && value.UserProvidedAllToolsEnabled) + if (!hasIndividualSettings && value.UserProvidedAllTools) { writer.WritePropertyName("dml-tools"); writer.WriteBooleanValue(value.AllToolsEnabled); } - else + else if (hasIndividualSettings) { writer.WritePropertyName("dml-tools"); diff --git a/src/Config/ObjectModel/DmlToolsConfig.cs b/src/Config/ObjectModel/DmlToolsConfig.cs index a08fd8d176..2a09e9d53c 100644 --- a/src/Config/ObjectModel/DmlToolsConfig.cs +++ b/src/Config/ObjectModel/DmlToolsConfig.cs @@ -64,53 +64,71 @@ public DmlToolsConfig( if (allToolsEnabled is not null) { AllToolsEnabled = allToolsEnabled.Value; - UserProvidedAllToolsEnabled = true; + UserProvidedAllTools = true; + + // When allToolsEnabled is set, use it as the default for all tools + bool toolDefault = allToolsEnabled.Value; + + DescribeEntities = describeEntities ?? toolDefault; + CreateRecord = createRecord ?? toolDefault; + ReadRecords = readRecords ?? toolDefault; + UpdateRecord = updateRecord ?? toolDefault; + DeleteRecord = deleteRecord ?? toolDefault; + ExecuteEntity = executeEntity ?? toolDefault; } else { AllToolsEnabled = DEFAULT_ENABLED; + + // Set values with defaults + DescribeEntities = describeEntities ?? DEFAULT_ENABLED; + CreateRecord = createRecord ?? DEFAULT_ENABLED; + ReadRecords = readRecords ?? DEFAULT_ENABLED; + UpdateRecord = updateRecord ?? DEFAULT_ENABLED; + DeleteRecord = deleteRecord ?? DEFAULT_ENABLED; + ExecuteEntity = executeEntity ?? DEFAULT_ENABLED; } - // Set values with defaults and track user-provided status - DescribeEntities = describeEntities ?? DEFAULT_ENABLED; + // Track user-provided status - only true if the parameter was not null UserProvidedDescribeEntities = describeEntities is not null; - - CreateRecord = createRecord ?? DEFAULT_ENABLED; UserProvidedCreateRecord = createRecord is not null; - - ReadRecords = readRecords ?? DEFAULT_ENABLED; UserProvidedReadRecords = readRecords is not null; - - UpdateRecord = updateRecord ?? DEFAULT_ENABLED; UserProvidedUpdateRecord = updateRecord is not null; - - DeleteRecord = deleteRecord ?? DEFAULT_ENABLED; UserProvidedDeleteRecord = deleteRecord is not null; - - ExecuteEntity = executeEntity ?? DEFAULT_ENABLED; UserProvidedExecuteEntity = executeEntity is not null; } /// /// Creates a DmlToolsConfig with all tools set to the same state + /// Used when user explicitly sets "dml-tools": true/false /// public static DmlToolsConfig FromBoolean(bool enabled) { + // Only pass allToolsEnabled, leave individual tools as null return new DmlToolsConfig( allToolsEnabled: enabled, - describeEntities: enabled, - createRecord: enabled, - readRecords: enabled, - updateRecord: enabled, - deleteRecord: enabled, - executeEntity: enabled + describeEntities: null, + createRecord: null, + readRecords: null, + updateRecord: null, + deleteRecord: null, + executeEntity: null ); } /// /// Creates a default DmlToolsConfig with all tools enabled + /// Used when dml-tools is not specified in config at all /// - public static DmlToolsConfig Default => FromBoolean(DEFAULT_ENABLED); + public static DmlToolsConfig Default => new( + allToolsEnabled: null, + describeEntities: null, + createRecord: null, + readRecords: null, + updateRecord: null, + deleteRecord: null, + executeEntity: null + ); /// /// Flag which informs CLI and JSON serializer whether to write all-tools-enabled @@ -118,7 +136,7 @@ public static DmlToolsConfig FromBoolean(bool enabled) /// [JsonIgnore(Condition = JsonIgnoreCondition.Always)] [MemberNotNullWhen(true, nameof(AllToolsEnabled))] - public bool UserProvidedAllToolsEnabled { get; init; } = false; + public bool UserProvidedAllTools { get; init; } = false; /// /// Flag which informs CLI and JSON serializer whether to write describe-entities diff --git a/src/Config/ObjectModel/McpRuntimeOptions.cs b/src/Config/ObjectModel/McpRuntimeOptions.cs index 3934c091bb..cd1e24f5fd 100644 --- a/src/Config/ObjectModel/McpRuntimeOptions.cs +++ b/src/Config/ObjectModel/McpRuntimeOptions.cs @@ -32,11 +32,11 @@ public record McpRuntimeOptions [JsonConstructor] public McpRuntimeOptions( - bool Enabled = true, + bool? Enabled = null, string? Path = null, DmlToolsConfig? DmlTools = null) { - this.Enabled = Enabled; + this.Enabled = Enabled ?? true; if (Path is not null) { @@ -51,7 +51,8 @@ public McpRuntimeOptions( // if DmlTools is null, set All tools enabled by default if (DmlTools is null) { - this.DmlTools = DmlToolsConfig.FromBoolean(DmlToolsConfig.DEFAULT_ENABLED); + // Use Default instead of FromBoolean to avoid setting UserProvided flags + this.DmlTools = DmlToolsConfig.Default; } else { diff --git a/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForCosmos.verified.txt b/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForCosmos.verified.txt index c135dffdb6..c75d645d13 100644 --- a/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForCosmos.verified.txt +++ b/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForCosmos.verified.txt @@ -28,13 +28,13 @@ UpdateRecord: true, DeleteRecord: true, ExecuteEntity: true, - UserProvidedAllToolsEnabled: false, - UserProvidedDescribeEntities: true, - UserProvidedCreateRecord: true, - UserProvidedReadRecords: true, - UserProvidedUpdateRecord: true, - UserProvidedDeleteRecord: true, - UserProvidedExecuteEntity: true + UserProvidedAllTools: false, + UserProvidedDescribeEntities: false, + UserProvidedCreateRecord: false, + UserProvidedReadRecords: false, + UserProvidedUpdateRecord: false, + UserProvidedDeleteRecord: false, + UserProvidedExecuteEntity: false } }, Host: { diff --git a/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMsSql.verified.txt b/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMsSql.verified.txt index 5b2a607817..52f4035868 100644 --- a/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMsSql.verified.txt +++ b/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMsSql.verified.txt @@ -32,13 +32,13 @@ UpdateRecord: true, DeleteRecord: true, ExecuteEntity: true, - UserProvidedAllToolsEnabled: false, - UserProvidedDescribeEntities: true, - UserProvidedCreateRecord: true, - UserProvidedReadRecords: true, - UserProvidedUpdateRecord: true, - UserProvidedDeleteRecord: true, - UserProvidedExecuteEntity: true + UserProvidedAllTools: false, + UserProvidedDescribeEntities: false, + UserProvidedCreateRecord: false, + UserProvidedReadRecords: false, + UserProvidedUpdateRecord: false, + UserProvidedDeleteRecord: false, + UserProvidedExecuteEntity: false } }, Host: { diff --git a/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMySql.verified.txt b/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMySql.verified.txt index 0f30634609..6a3a4c226c 100644 --- a/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMySql.verified.txt +++ b/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForMySql.verified.txt @@ -24,13 +24,13 @@ UpdateRecord: true, DeleteRecord: true, ExecuteEntity: true, - UserProvidedAllToolsEnabled: false, - UserProvidedDescribeEntities: true, - UserProvidedCreateRecord: true, - UserProvidedReadRecords: true, - UserProvidedUpdateRecord: true, - UserProvidedDeleteRecord: true, - UserProvidedExecuteEntity: true + UserProvidedAllTools: false, + UserProvidedDescribeEntities: false, + UserProvidedCreateRecord: false, + UserProvidedReadRecords: false, + UserProvidedUpdateRecord: false, + UserProvidedDeleteRecord: false, + UserProvidedExecuteEntity: false } }, Host: { diff --git a/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForPostgreSql.verified.txt b/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForPostgreSql.verified.txt index 6a4f569a2a..4373b266f4 100644 --- a/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForPostgreSql.verified.txt +++ b/src/Service.Tests/Snapshots/ConfigurationTests.TestReadingRuntimeConfigForPostgreSql.verified.txt @@ -24,13 +24,13 @@ UpdateRecord: true, DeleteRecord: true, ExecuteEntity: true, - UserProvidedAllToolsEnabled: false, - UserProvidedDescribeEntities: true, - UserProvidedCreateRecord: true, - UserProvidedReadRecords: true, - UserProvidedUpdateRecord: true, - UserProvidedDeleteRecord: true, - UserProvidedExecuteEntity: true + UserProvidedAllTools: false, + UserProvidedDescribeEntities: false, + UserProvidedCreateRecord: false, + UserProvidedReadRecords: false, + UserProvidedUpdateRecord: false, + UserProvidedDeleteRecord: false, + UserProvidedExecuteEntity: false } }, Host: { From 7b31e9a6f6d0100089b090bd1e7be047fe4dee07 Mon Sep 17 00:00:00 2001 From: Souvik Ghosh Date: Sat, 8 Nov 2025 06:35:35 +0530 Subject: [PATCH 08/36] Honoring incoming request role in determining allowed permissions for describe-entities MCP tool (#2956) ## Why make this change? - Addresses follow ups to PR #2900 The `describe_entities` tool response format needed improvements to better align with MCP specifications and provide more accurate, user-scoped information. Key issues included non-specification compliant response fields, overly broad permission reporting across all roles, and inconsistent entity/field naming conventions that didn't prioritize user-friendly aliases. ## What is this change? - **Removed non-spec fields from response**: Eliminated `mode` and `filter` fields that were not part of the MCP specification - **Scoped permissions to current user's role**: Modified permissions logic to only return permissions available to the requesting user's role instead of all permissions across all roles - **Implemented entity alias support**: Updated entity name resolution to prefer GraphQL singular names (aliases) over configuration names, falling back to entity name only when alias is absent - **Fixed parameter metadata format**: Changed parameter default value key from `@default` to `default` in JSON response - **Enhanced field name resolution**: Updated field metadata to use field aliases when available, falling back to field names when aliases are absent - **Added proper authorization context**: Integrated HTTP context and authorization resolver to determine current user's role for permission filtering ## How was this tested? - [x] Manual Tests ## Sample Request(s) ``` POST http://localhost:5000/mcp { "jsonrpc": "2.0", "method": "tools/call", "params": { "name": "describe_entities" }, "id": 1 } ``` ``` POST http://localhost:5000/mcp { "jsonrpc": "2.0", "method": "tools/call", "params": { "name": "describe_entities", "arguments": { "nameOnly": true } }, "id": 2 } ``` ``` POST http://localhost:5000/mcp { "jsonrpc": "2.0", "method": "tools/call", "params": { "name": "describe_entities", "arguments": { "entities": ["Book", "Publisher"] } }, "id": 1 } ``` --- .../BuiltInTools/DescribeEntitiesTool.cs | 114 +++++++++++++----- 1 file changed, 87 insertions(+), 27 deletions(-) diff --git a/src/Azure.DataApiBuilder.Mcp/BuiltInTools/DescribeEntitiesTool.cs b/src/Azure.DataApiBuilder.Mcp/BuiltInTools/DescribeEntitiesTool.cs index 4215705572..154b37ee80 100644 --- a/src/Azure.DataApiBuilder.Mcp/BuiltInTools/DescribeEntitiesTool.cs +++ b/src/Azure.DataApiBuilder.Mcp/BuiltInTools/DescribeEntitiesTool.cs @@ -2,11 +2,14 @@ // Licensed under the MIT License. using System.Text.Json; +using Azure.DataApiBuilder.Auth; using Azure.DataApiBuilder.Config.ObjectModel; +using Azure.DataApiBuilder.Core.Authorization; using Azure.DataApiBuilder.Core.Configurations; using Azure.DataApiBuilder.Mcp.Model; using Azure.DataApiBuilder.Mcp.Utils; using Azure.DataApiBuilder.Service.Exceptions; +using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using ModelContextProtocol.Protocol; @@ -80,8 +83,45 @@ public Task ExecuteAsync( logger)); } + // Get authorization services to determine current user's role + IAuthorizationResolver authResolver = serviceProvider.GetRequiredService(); + IHttpContextAccessor httpContextAccessor = serviceProvider.GetRequiredService(); + HttpContext? httpContext = httpContextAccessor.HttpContext; + + // 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 + string? currentUserRole = null; + 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) + { + logger?.LogWarning("Current user role could not be determined from HTTP context or role header. " + + "Entity permissions will be empty (no permissions shown) rather than using anonymous permissions. " + + "Ensure the '{RoleHeader}' header is properly set.", AuthorizationResolver.CLIENT_ROLE_HEADER); + } + List> entityList = new(); if (runtimeConfig.Entities != null) @@ -102,7 +142,7 @@ public Task ExecuteAsync( { Dictionary entityInfo = nameOnly ? BuildBasicEntityInfo(entityName, entity) - : BuildFullEntityInfo(entityName, entity); + : BuildFullEntityInfo(entityName, entity, currentUserRole); entityList.Add(entityInfo); } @@ -140,19 +180,14 @@ public Task ExecuteAsync( Dictionary responseData = new() { ["entities"] = finalEntityList, - ["count"] = finalEntityList.Count, - ["mode"] = nameOnly ? "basic" : "full" + ["count"] = finalEntityList.Count }; - if (entityFilter != null && entityFilter.Count > 0) - { - responseData["filter"] = entityFilter.ToArray(); - } - logger?.LogInformation( - "DescribeEntitiesTool returned {EntityCount} entities in {Mode} mode.", + "DescribeEntitiesTool returned {EntityCount} entities. Response type: {ResponseType} (nameOnly={NameOnly}).", finalEntityList.Count, - nameOnly ? "basic" : "full"); + nameOnly ? "lightweight summary (names and descriptions only)" : "full metadata with fields, parameters, and permissions", + nameOnly); return Task.FromResult(McpResponseBuilder.BuildSuccessResult( responseData, @@ -276,13 +311,18 @@ private static bool ShouldIncludeEntity(string entityName, HashSet? enti /// /// The name of the entity to include in the dictionary. /// The entity object from which to extract additional information. - /// A dictionary with two keys: "name", containing the entity name, and "description", containing the entity's + /// A dictionary with two keys: "name", containing the entity alias (or name if no alias), and "description", containing the entity's /// description or an empty string if the description is null. private static Dictionary BuildBasicEntityInfo(string entityName, Entity entity) { + // Use GraphQL singular name as alias if available, otherwise use entity name + string displayName = !string.IsNullOrWhiteSpace(entity.GraphQL?.Singular) + ? entity.GraphQL.Singular + : entityName; + return new Dictionary { - ["name"] = entityName, + ["name"] = displayName, ["description"] = entity.Description ?? string.Empty }; } @@ -290,11 +330,22 @@ private static bool ShouldIncludeEntity(string entityName, HashSet? enti /// /// Builds full entity info: name, description, fields, parameters (for stored procs), permissions. /// - private static Dictionary BuildFullEntityInfo(string entityName, Entity entity) + /// The name of the entity to include in the dictionary. + /// The entity object from which to extract additional information. + /// The role of the current user, used to determine permissions. + /// + /// A dictionary containing the entity's name, description, fields, parameters (if applicable), and permissions. + /// + private static Dictionary BuildFullEntityInfo(string entityName, Entity entity, string? currentUserRole) { + // Use GraphQL singular name as alias if available, otherwise use entity name + string displayName = !string.IsNullOrWhiteSpace(entity.GraphQL?.Singular) + ? entity.GraphQL.Singular + : entityName; + Dictionary info = new() { - ["name"] = entityName, + ["name"] = displayName, ["description"] = entity.Description ?? string.Empty, ["fields"] = BuildFieldMetadataInfo(entity.Fields), }; @@ -304,7 +355,7 @@ private static bool ShouldIncludeEntity(string entityName, HashSet? enti info["parameters"] = BuildParameterMetadataInfo(entity.Source.Parameters); } - info["permissions"] = BuildPermissionsInfo(entity); + info["permissions"] = BuildPermissionsInfo(entity, currentUserRole); return info; } @@ -325,7 +376,7 @@ private static List BuildFieldMetadataInfo(List? fields) { result.Add(new { - name = field.Name, + name = field.Alias ?? field.Name, description = field.Description ?? string.Empty }); } @@ -338,7 +389,7 @@ private static List BuildFieldMetadataInfo(List? fields) /// Builds a list of parameter metadata objects containing information about each parameter. /// /// A list of objects representing the parameters to process. Can be null. - /// A list of anonymous objects, each containing the parameter's name, whether it is required, its default + /// A list of dictionaries, each containing the parameter's name, whether it is required, its default /// value, and its description. Returns an empty list if is null. private static List BuildParameterMetadataInfo(List? parameters) { @@ -348,13 +399,14 @@ private static List BuildParameterMetadataInfo(List? { foreach (ParameterMetadata param in parameters) { - result.Add(new + Dictionary paramInfo = new() { - name = param.Name, - required = param.Default == null, // required if no default - @default = param.Default, - description = param.Description ?? string.Empty - }); + ["name"] = param.Name, + ["required"] = param.Required, + ["default"] = param.Default, + ["description"] = param.Description ?? string.Empty + }; + result.Add(paramInfo); } } @@ -362,13 +414,14 @@ private static List BuildParameterMetadataInfo(List? } /// - /// Build a list of permission metadata info + /// Build a list of permission metadata info for the current user's role /// /// The entity object - /// A list of permissions available to the entity - private static string[] BuildPermissionsInfo(Entity entity) + /// The current user's role - if null, returns empty permissions + /// A list of permissions available to the current user's role for this entity + private static string[] BuildPermissionsInfo(Entity entity, string? currentUserRole) { - if (entity.Permissions == null) + if (entity.Permissions == null || string.IsNullOrWhiteSpace(currentUserRole)) { return Array.Empty(); } @@ -380,8 +433,15 @@ private static string[] BuildPermissionsInfo(Entity entity) HashSet permissions = new(StringComparer.OrdinalIgnoreCase); + // Only include permissions for the current user's role foreach (EntityPermission permission in entity.Permissions) { + // Check if this permission applies to the current user's role + if (!string.Equals(permission.Role, currentUserRole, StringComparison.OrdinalIgnoreCase)) + { + continue; + } + foreach (EntityAction action in permission.Actions) { if (action.Action == EntityActionOperation.All) From 2d12abca87985c68223cb9d69bea9321392f9e5f Mon Sep 17 00:00:00 2001 From: "Maddy Montaquila (Leger)" Date: Mon, 10 Nov 2025 11:02:53 -0500 Subject: [PATCH 09/36] Adding Aspire!!!!!!!!!!!!!!!!!!!!!!!!!! (#2797) ## Why make this change? BECAUSE I AM COOL ## What is this change? Aspirification ## How was this tested? Blood, sweat, and tears ## Sample Request(s) `aspire run` baby --------- Co-authored-by: Tommaso Stocchi Co-authored-by: Damian Edwards Co-authored-by: Safia Abdalla Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Aniruddh Munde --- .aspire/settings.json | 3 + src/.editorconfig | 6 +- src/Aspire.AppHost/AppHost.cs | 95 +++++++ src/Aspire.AppHost/Aspire.AppHost.csproj | 24 ++ src/Aspire.AppHost/DockerStatus.cs | 26 ++ .../Properties/launchSettings.json | 57 +++++ src/Aspire.AppHost/README.md | 17 ++ src/Aspire.AppHost/appsettings.json | 9 + .../init-scripts/pg/create-database-pg.sql | 238 +++++++++++++++++ .../init-scripts/sql/create-database.sql | 239 ++++++++++++++++++ src/Azure.DataApiBuilder.sln | 7 + src/Directory.Packages.props | 9 +- .../Azure.DataApiBuilder.Service.csproj | 2 +- src/Service/HealthCheck/HealthCheckHelper.cs | 7 +- src/Service/HealthCheck/HttpUtilities.cs | 31 ++- src/Service/HealthCheck/Utilities.cs | 18 ++ src/Service/Properties/launchSettings.json | 26 +- src/Service/dab-config.json | 231 +++++++++++++++++ 18 files changed, 1007 insertions(+), 38 deletions(-) create mode 100644 .aspire/settings.json create mode 100644 src/Aspire.AppHost/AppHost.cs create mode 100644 src/Aspire.AppHost/Aspire.AppHost.csproj create mode 100644 src/Aspire.AppHost/DockerStatus.cs create mode 100644 src/Aspire.AppHost/Properties/launchSettings.json create mode 100644 src/Aspire.AppHost/README.md create mode 100644 src/Aspire.AppHost/appsettings.json create mode 100644 src/Aspire.AppHost/init-scripts/pg/create-database-pg.sql create mode 100644 src/Aspire.AppHost/init-scripts/sql/create-database.sql create mode 100644 src/Service/dab-config.json diff --git a/.aspire/settings.json b/.aspire/settings.json new file mode 100644 index 0000000000..61dd69a516 --- /dev/null +++ b/.aspire/settings.json @@ -0,0 +1,3 @@ +{ + "appHostPath": "../src/Aspire.AppHost/Aspire.AppHost.csproj" +} \ No newline at end of file diff --git a/src/.editorconfig b/src/.editorconfig index 5639a063ae..6367bdcb3c 100644 --- a/src/.editorconfig +++ b/src/.editorconfig @@ -75,9 +75,9 @@ dotnet_style_allow_statement_immediately_after_block_experimental = false:error #### C# Coding Conventions #### # var preferences -csharp_style_var_elsewhere = false:error -csharp_style_var_for_built_in_types = false:error -csharp_style_var_when_type_is_apparent = false:error +# csharp_style_var_elsewhere = false:error +# csharp_style_var_for_built_in_types = false:error +# csharp_style_var_when_type_is_apparent = false:error # Modifier preferences csharp_prefer_static_local_function = true:suggestion diff --git a/src/Aspire.AppHost/AppHost.cs b/src/Aspire.AppHost/AppHost.cs new file mode 100644 index 0000000000..5477c5c81b --- /dev/null +++ b/src/Aspire.AppHost/AppHost.cs @@ -0,0 +1,95 @@ +using Microsoft.Extensions.DependencyInjection; + +var builder = DistributedApplication.CreateBuilder(args); + +var aspireDB = Environment.GetEnvironmentVariable("ASPIRE_DATABASE_TYPE"); + +var databaseConnectionString = Environment.GetEnvironmentVariable("ASPIRE_DATABASE_CONNECTION_STRING") ?? ""; + +switch (aspireDB) +{ + case "mssql": + var sqlScript = File.ReadAllText("./init-scripts/sql/create-database.sql"); + + IResourceBuilder? sqlDbContainer = null; + + if (string.IsNullOrEmpty(databaseConnectionString)) + { + Console.WriteLine("No connection string provided, starting a local SQL Server container."); + + sqlDbContainer = builder.AddSqlServer("sqlserver") + .WithDataVolume() + .WithLifetime(ContainerLifetime.Persistent) + .AddDatabase("msSqlDb", "Trek") + .WithCreationScript(sqlScript); + } + + var mssqlService = builder.AddProject("mssql-service", "Development") + .WithArgs("-f", "net8.0") + .WithEndpoint(endpointName: "https", (e) => e.Port = 1234) + .WithEndpoint(endpointName: "http", (e) => e.Port = 2345) + .WithEnvironment("db-type", "mssql") + .WithUrls((e) => + { + e.Urls.Clear(); + e.Urls.Add(new() { Url = "/swagger", DisplayText = "🔒Swagger", Endpoint = e.GetEndpoint("https") }); + e.Urls.Add(new() { Url = "/graphql", DisplayText = "🔒GraphQL", Endpoint = e.GetEndpoint("https") }); + }) + .WithHttpHealthCheck("/health"); + + if (sqlDbContainer is null) + { + mssqlService.WithEnvironment("ConnectionStrings__Database", databaseConnectionString); + } + else + { + mssqlService.WithEnvironment("ConnectionStrings__Database", sqlDbContainer) + .WaitFor(sqlDbContainer); + } + + break; + case "postgresql": + var pgScript = File.ReadAllText("./init-scripts/pg/create-database-pg.sql"); + + IResourceBuilder? postgresDB = null; + + if (!string.IsNullOrEmpty(databaseConnectionString)) + { + Console.WriteLine("No connection string provided, starting a local PostgreSQL container."); + + postgresDB = builder.AddPostgres("postgres") + .WithPgAdmin() + .WithLifetime(ContainerLifetime.Persistent) + .AddDatabase("pgDb", "postgres") + .WithCreationScript(pgScript); + } + + var pgService = builder.AddProject("pg-service", "Development") + .WithArgs("-f", "net8.0") + .WithEndpoint(endpointName: "https", (e) => e.Port = 1234) + .WithEndpoint(endpointName: "http", (e) => e.Port = 2345) + .WithEnvironment("db-type", "postgresql") + .WithUrls((e) => + { + e.Urls.Clear(); + e.Urls.Add(new() { Url = "/swagger", DisplayText = "🔒Swagger", Endpoint = e.GetEndpoint("https") }); + e.Urls.Add(new() { Url = "/graphql", DisplayText = "🔒GraphQL", Endpoint = e.GetEndpoint("https") }); + }) + .WithHttpHealthCheck("/health"); + + if (postgresDB is null) + { + pgService.WithEnvironment("ConnectionStrings__Database", databaseConnectionString); + } + else + { + pgService.WithEnvironment("ConnectionStrings__Database", postgresDB) + .WaitFor(postgresDB); + } + + break; + default: + throw new Exception("Please set the ASPIRE_DATABASE environment variable to either 'mssql' or 'postgresql'."); +} + +builder.Build().Run(); diff --git a/src/Aspire.AppHost/Aspire.AppHost.csproj b/src/Aspire.AppHost/Aspire.AppHost.csproj new file mode 100644 index 0000000000..79d44e51a1 --- /dev/null +++ b/src/Aspire.AppHost/Aspire.AppHost.csproj @@ -0,0 +1,24 @@ + + + + + + Exe + net8.0 + enable + NU1603;NU1605 + enable + f08719fd-267f-459e-9980-77b1c52c8755 + + + + + + + + + + + + + diff --git a/src/Aspire.AppHost/DockerStatus.cs b/src/Aspire.AppHost/DockerStatus.cs new file mode 100644 index 0000000000..ad4237357e --- /dev/null +++ b/src/Aspire.AppHost/DockerStatus.cs @@ -0,0 +1,26 @@ +using System.Diagnostics; + +public static class DockerStatus +{ + public static async Task IsDockerRunningAsync() + { + var psi = new ProcessStartInfo + { + FileName = "docker", + Arguments = "info", + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + }; + try + { + using var process = Process.Start(psi)!; + await process.WaitForExitAsync(); + return process.ExitCode == 0; + } + catch + { + return false; + } + } +} diff --git a/src/Aspire.AppHost/Properties/launchSettings.json b/src/Aspire.AppHost/Properties/launchSettings.json new file mode 100644 index 0000000000..43a2673a34 --- /dev/null +++ b/src/Aspire.AppHost/Properties/launchSettings.json @@ -0,0 +1,57 @@ +{ + "profiles": { + "https": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21213", + "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22145" + }, + "dotnetRunMessages": true, + "applicationUrl": "https://localhost:17047;http://localhost:15161" + }, + "http": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "http://localhost:19015", + "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "http://localhost:20166" + }, + "dotnetRunMessages": true, + "applicationUrl": "http://localhost:15161" + }, + "aspire-mssql": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21213", + "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22145", + "ASPIRE_DATABASE_TYPE": "mssql", + "ASPIRE_DATABASE_CONNECTION_STRING": "" + }, + "dotnetRunMessages": true, + "applicationUrl": "https://localhost:17047;http://localhost:15161" + }, + "aspire-postgresql": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21213", + "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22145", + "ASPIRE_DATABASE_TYPE": "postgresql", + "ASPIRE_DATABASE_CONNECTION_STRING": "" + }, + "dotnetRunMessages": true, + "applicationUrl": "https://localhost:17047;http://localhost:15161" + } + }, + "$schema": "https://json.schemastore.org/launchsettings.json" +} diff --git a/src/Aspire.AppHost/README.md b/src/Aspire.AppHost/README.md new file mode 100644 index 0000000000..3437c367bb --- /dev/null +++ b/src/Aspire.AppHost/README.md @@ -0,0 +1,17 @@ +# Aspire Instructions + +This project allows you to run DAB in debug mode using [Aspire](https://learn.microsoft.com/en-us/dotnet/aspire/get-started/aspire-overview). + +## Prerequisites +- [.NET SDK](https://dotnet.microsoft.com/download) (8.0 or later) +- [Docker](https://www.docker.com/products/docker-desktop) (optional, for containerized development) + +## Database Configuration + +In the `launchProfile.json` file, you can configure the database connection string. If you don't, Aspire will start for you a local instance in a Docker container. + +Simply provide a value for the `ASPIRE_DATABASE_CONNECTION_STRING` environment variable. + +You can select to run Aspire with different databases selecting the appropriate launch profile: +- `aspire-sql` +- `aspire-postgres` diff --git a/src/Aspire.AppHost/appsettings.json b/src/Aspire.AppHost/appsettings.json new file mode 100644 index 0000000000..31c092aa45 --- /dev/null +++ b/src/Aspire.AppHost/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Aspire.Hosting.Dcp": "Warning" + } + } +} diff --git a/src/Aspire.AppHost/init-scripts/pg/create-database-pg.sql b/src/Aspire.AppHost/init-scripts/pg/create-database-pg.sql new file mode 100644 index 0000000000..73adccc008 --- /dev/null +++ b/src/Aspire.AppHost/init-scripts/pg/create-database-pg.sql @@ -0,0 +1,238 @@ +-- create-database-pg.sql +-- PostgreSQL version of create-database.sql + +-- Create database (run this as a superuser, outside the target database) +-- Uncomment and edit the database name as needed +--CREATE DATABASE "Trek"; + +-- Connect to the target database before running the rest of the script +--\connect Trek; + +-- Drop tables in reverse order of creation due to foreign key dependencies +DROP TABLE IF EXISTS "Character_Species"; +DROP TABLE IF EXISTS "Series_Character"; +DROP TABLE IF EXISTS "Character"; +DROP TABLE IF EXISTS "Species"; +DROP TABLE IF EXISTS "Actor"; +DROP TABLE IF EXISTS "Series"; + +-- create tables +CREATE TABLE "Series" ( + "Id" INTEGER PRIMARY KEY, + "Name" VARCHAR(255) NOT NULL +); + +CREATE TABLE "Actor" ( + "Id" INTEGER PRIMARY KEY, + "Name" VARCHAR(255) NOT NULL, + "BirthYear" INTEGER NOT NULL +); + +CREATE TABLE "Species" ( + "Id" INTEGER PRIMARY KEY, + "Name" VARCHAR(255) NOT NULL +); + +CREATE TABLE "Character" ( + "Id" INTEGER PRIMARY KEY, + "Name" VARCHAR(255) NOT NULL, + "ActorId" INTEGER NOT NULL, + "Stardate" DECIMAL(10, 2), + FOREIGN KEY ("ActorId") REFERENCES "Actor"("Id") +); + +CREATE TABLE "Series_Character" ( + "SeriesId" INTEGER, + "CharacterId" INTEGER, + "Role" VARCHAR(500), + FOREIGN KEY ("SeriesId") REFERENCES "Series"("Id"), + FOREIGN KEY ("CharacterId") REFERENCES "Character"("Id"), + PRIMARY KEY ("SeriesId", "CharacterId") +); + +CREATE TABLE "Character_Species" ( + "CharacterId" INTEGER, + "SpeciesId" INTEGER, + FOREIGN KEY ("CharacterId") REFERENCES "Character"("Id"), + FOREIGN KEY ("SpeciesId") REFERENCES "Species"("Id"), + PRIMARY KEY ("CharacterId", "SpeciesId") +); + +-- create data +INSERT INTO "Series" ("Id", "Name") VALUES + (1, 'Star Trek'), + (2, 'Star Trek: The Next Generation'), + (3, 'Star Trek: Voyager'), + (4, 'Star Trek: Deep Space Nine'), + (5, 'Star Trek: Enterprise'); + +INSERT INTO "Species" ("Id", "Name") VALUES + (1, 'Human'), + (2, 'Vulcan'), + (3, 'Android'), + (4, 'Klingon'), + (5, 'Betazoid'), + (6, 'Hologram'), + (7, 'Bajoran'), + (8, 'Changeling'), + (9, 'Trill'), + (10, 'Ferengi'), + (11, 'Denobulan'), + (12, 'Borg'); + +INSERT INTO "Actor" ("Id", "Name", "BirthYear") VALUES + (1, 'William Shatner', 1931), + (2, 'Leonard Nimoy', 1931), + (3, 'DeForest Kelley', 1920), + (4, 'James Doohan', 1920), + (5, 'Nichelle Nichols', 1932), + (6, 'George Takei', 1937), + (7, 'Walter Koenig', 1936), + (8, 'Patrick Stewart', 1940), + (9, 'Jonathan Frakes', 1952), + (10, 'Brent Spiner', 1949), + (11, 'Michael Dorn', 1952), + (12, 'Gates McFadden', 1949), + (13, 'Marina Sirtis', 1955), + (14, 'LeVar Burton', 1957), + (15, 'Kate Mulgrew', 1955), + (16, 'Robert Beltran', 1953), + (17, 'Tim Russ', 1956), + (18, 'Roxann Dawson', 1958), + (19, 'Robert Duncan McNeill', 1964), + (20, 'Garrett Wang', 1968), + (21, 'Robert Picardo', 1953), + (22, 'Jeri Ryan', 1968), + (23, 'Avery Brooks', 1948), + (24, 'Nana Visitor', 1957), + (25, 'Rene Auberjonois', 1940), + (26, 'Terry Farrell', 1963), + (27, 'Alexander Siddig', 1965), + (28, 'Armin Shimerman', 1949), + (29, 'Cirroc Lofton', 1978), + (30, 'Scott Bakula', 1954), + (31, 'Jolene Blalock', 1975), + (32, 'John Billingsley', 1960), + (33, 'Connor Trinneer', 1969), + (34, 'Dominic Keating', 1962), + (35, 'Linda Park', 1978), + (36, 'Anthony Montgomery', 1971); + +INSERT INTO "Character" ("Id", "Name", "ActorId", "Stardate") VALUES + (1, 'James T. Kirk', 1, 2233.04), + (2, 'Spock', 2, 2230.06), + (3, 'Leonard McCoy', 3, 2227.00), + (4, 'Montgomery Scott', 4, 2222.00), + (5, 'Uhura', 5, 2233.00), + (6, 'Hikaru Sulu', 6, 2237.00), + (7, 'Pavel Chekov', 7, 2245.00), + (8, 'Jean-Luc Picard', 8, 2305.07), + (9, 'William Riker', 9, 2335.08), + (10, 'Data', 10, 2336.00), + (11, 'Worf', 11, 2340.00), + (12, 'Beverly Crusher', 12, 2324.00), + (13, 'Deanna Troi', 13, 2336.00), + (14, 'Geordi La Forge', 14, 2335.02), + (15, 'Kathryn Janeway', 15, 2336.05), + (16, 'Chakotay', 16, 2329.00), + (17, 'Tuvok', 17, 2264.00), + (18, 'B''Elanna Torres', 18, 2349.00), + (19, 'Tom Paris', 19, 2346.00), + (20, 'Harry Kim', 20, 2349.00), + (21, 'The Doctor', 21, 2371.00), -- Stardate of activation + (22, 'Seven of Nine', 22, 2348.00), + (23, 'Benjamin Sisko', 23, 2332.00), + (24, 'Kira Nerys', 24, 2343.00), + (25, 'Odo', 25, 2337.00), -- Approximate stardate of discovery + (27, 'Jadzia Dax', 26, 2341.00), + (28, 'Julian Bashir', 27, 2341.00), + (29, 'Quark', 28, 2333.00), + (30, 'Jake Sisko', 29, 2355.00), + (31, 'Jonathan Archer', 30, 2112.00), + (32, 'T''Pol', 31, 2088.00), + (33, 'Phlox', 32, 2102.00), + (34, 'Charles "Trip" Tucker III', 33, 2121.00), + (35, 'Malcolm Reed', 34, 2117.00), + (36, 'Hoshi Sato', 35, 2129.00), + (37, 'Travis Mayweather', 36, 2126.00); + +INSERT INTO "Series_Character" ("SeriesId", "CharacterId", "Role") VALUES + (1, 1, 'Captain'), -- James T. Kirk in Star Trek + (1, 2, 'Science Officer'), -- Spock in Star Trek + (1, 3, 'Doctor'), -- Leonard McCoy in Star Trek + (1, 4, 'Engineer'), -- Montgomery Scott in Star Trek + (1, 5, 'Communications Officer'), -- Uhura in Star Trek + (1, 6, 'Helmsman'), -- Hikaru Sulu in Star Trek + (1, 7, 'Navigator'), -- Pavel Chekov in Star Trek + (2, 8, 'Captain'), -- Jean-Luc Picard in Star Trek: The Next Generation + (2, 9, 'First Officer'), -- William Riker in Star Trek: The Next Generation + (2, 10, 'Operations Officer'),-- Data in Star Trek: The Next Generation + (2, 11, 'Security Officer'),-- Worf in Star Trek: The Next Generation + (2, 12, 'Doctor'),-- Beverly Crusher in Star Trek: The Next Generation + (2, 13, 'Counselor'),-- Deanna Troi in Star Trek: The Next Generation + (2, 14, 'Engineer'),-- Geordi La Forge in Star Trek: The Next Generation + (3, 15, 'Captain'),-- Kathryn Janeway in Star Trek: Voyager + (3, 16, 'First Officer'),-- Chakotay in Star Trek: Voyager + (3, 17, 'Tactical Officer'),-- Tuvok in Star Trek: Voyager + (3, 18, 'Engineer'),-- B'Elanna Torres in Star Trek: Voyager + (3, 19, 'Helmsman'),-- Tom Paris in Star Trek: Voyager + (3, 20, 'Operations Officer'),-- Harry Kim in Star Trek: Voyager + (3, 21, 'Doctor'),-- The Doctor in Star Trek: Voyager + (3, 22, 'Astrometrics Officer'),-- Seven of Nine in Star Trek: Voyager + (4, 23, 'Commanding Officer'),-- Benjamin Sisko in Star Trek: Deep Space Nine + (4, 24, 'First Officer'),-- Kira Nerys in Star Trek: Deep Space Nine + (4, 25, 'Security Officer'),-- Odo in Star Trek: Deep Space Nine + (4, 11, 'Strategic Operations Officer'),-- Worf in Star Trek: Deep Space Nine + (4, 27, 'Science Officer'),-- Jadzia Dax in Star Trek: Deep Space Nine + (4, 28, 'Doctor'),-- Julian Bashir in Star Trek: Deep Space Nine + (4, 29, 'Bar Owner'),-- Quark in Star Trek: Deep Space Nine + (4, 30, 'Civilian'),-- Jake Sisko in Star Trek: Deep Space Nine + (5, 31, 'Captain'),-- Jonathan Archer in Star Trek: Enterprise + (5, 32, 'Science Officer'),-- T'Pol in Star Trek: Enterprise + (5, 33, 'Doctor'),-- Phlox in Star Trek: Enterprise + (5, 34, 'Chief Engineer'),-- Charles "Trip" Tucker III in Star Trek: Enterprise + (5, 35, 'Armory Officer'),-- Malcolm Reed in Star Trek: Enterprise + (5, 36, 'Communications Officer'),-- Hoshi Sato in Star Trek: Enterprise + (5, 37, 'Helmsman');-- Travis Mayweather in Star Trek: Enterprise + +INSERT INTO "Character_Species" ("CharacterId", "SpeciesId") VALUES + (1, 1), -- James T. Kirk is Human + (2, 2), -- Spock is Vulcan + (2, 1), -- Spock is also Human + (3, 1), -- Leonard McCoy is Human + (4, 1), -- Montgomery Scott is Human + (5, 1), -- Uhura is Human + (6, 1), -- Hikaru Sulu is Human + (7, 1), -- Pavel Chekov is Human + (8, 1), -- Jean-Luc Picard is Human + (9, 1), -- William Riker is Human + (10, 3), -- Data is Android + (11, 4), -- Worf is Klingon + (12, 1), -- Beverly Crusher is Human + (13, 1), -- Deanna Troi is Human + (13, 5), -- Deanna Troi is also Betazoid + (14, 1), -- Geordi La Forge is Human + (15, 1), -- Kathryn Janeway is Human + (16, 1), -- Chakotay is Human + (17, 2), -- Tuvok is Vulcan + (18, 1), -- B'Elanna Torres is Human + (18, 4), -- B'Elanna Torres is also Klingon + (19, 1), -- Tom Paris is Human + (20, 1), -- Harry Kim is Human + (21, 6), -- The Doctor is a Hologram + (22, 1), -- Seven of Nine is Human + (22, 12),-- Seven of Nine is also Borg + (23, 1), -- Benjamin Sisko is Human + (24, 7), -- Kira Nerys is Bajoran + (25, 8), -- Odo is Changeling + (27, 9), -- Jadzia Dax is Trill + (28, 1), -- Julian Bashir is Human + (29, 10),-- Quark is Ferengi + (30, 1), -- Jake Sisko is Human + (31, 1), -- Jonathan Archer is Human + (32, 2), -- T'Pol is Vulcan + (33, 11),-- Phlox is Denobulan + (34, 1), -- Charles "Trip" Tucker III is Human + (35, 1), -- Malcolm Reed is Human + (36, 1), -- Hoshi Sato is Human + (37, 1); -- Travis Mayweather is Human diff --git a/src/Aspire.AppHost/init-scripts/sql/create-database.sql b/src/Aspire.AppHost/init-scripts/sql/create-database.sql new file mode 100644 index 0000000000..a12c71138e --- /dev/null +++ b/src/Aspire.AppHost/init-scripts/sql/create-database.sql @@ -0,0 +1,239 @@ + +USE [master] +GO + +CREATE DATABASE [Trek] +GO + +USE [Trek] +GO + +-- Drop tables in reverse order of creation due to foreign key dependencies +DROP TABLE IF EXISTS Character_Species; +DROP TABLE IF EXISTS Series_Character; +DROP TABLE IF EXISTS Character; +DROP TABLE IF EXISTS Species; +DROP TABLE IF EXISTS Actor; +DROP TABLE IF EXISTS Series; + +-- create tables +CREATE TABLE Series ( + Id INT PRIMARY KEY, + Name NVARCHAR(255) NOT NULL +); + +CREATE TABLE Actor ( + Id INT PRIMARY KEY, + Name NVARCHAR(255) NOT NULL, + [BirthYear] INT NOT NULL +); + +CREATE TABLE Species ( + Id INT PRIMARY KEY, + Name NVARCHAR(255) NOT NULL +); + +CREATE TABLE Character ( + Id INT PRIMARY KEY, + Name NVARCHAR(255) NOT NULL, + ActorId INT NOT NULL, + Stardate DECIMAL(10, 2), + FOREIGN KEY (ActorId) REFERENCES Actor(Id) +); + +CREATE TABLE Series_Character ( + SeriesId INT, + CharacterId INT, + Role VARCHAR(500), + FOREIGN KEY (SeriesId) REFERENCES Series(Id), + FOREIGN KEY (CharacterId) REFERENCES Character(Id), + PRIMARY KEY (SeriesId, CharacterId) +); + +CREATE TABLE Character_Species ( + CharacterId INT, + SpeciesId INT, + FOREIGN KEY (CharacterId) REFERENCES Character(Id), + FOREIGN KEY (SpeciesId) REFERENCES Species(Id), + PRIMARY KEY (CharacterId, SpeciesId) +); + +-- create data +INSERT INTO Series (Id, Name) VALUES + (1, 'Star Trek'), + (2, 'Star Trek: The Next Generation'), + (3, 'Star Trek: Voyager'), + (4, 'Star Trek: Deep Space Nine'), + (5, 'Star Trek: Enterprise'); + +INSERT INTO Species (Id, Name) VALUES + (1, 'Human'), + (2, 'Vulcan'), + (3, 'Android'), + (4, 'Klingon'), + (5, 'Betazoid'), + (6, 'Hologram'), + (7, 'Bajoran'), + (8, 'Changeling'), + (9, 'Trill'), + (10, 'Ferengi'), + (11, 'Denobulan'), + (12, 'Borg'); + +INSERT INTO Actor (Id, Name, [BirthYear]) VALUES + (1, 'William Shatner', 1931), + (2, 'Leonard Nimoy', 1931), + (3, 'DeForest Kelley', 1920), + (4, 'James Doohan', 1920), + (5, 'Nichelle Nichols', 1932), + (6, 'George Takei', 1937), + (7, 'Walter Koenig', 1936), + (8, 'Patrick Stewart', 1940), + (9, 'Jonathan Frakes', 1952), + (10, 'Brent Spiner', 1949), + (11, 'Michael Dorn', 1952), + (12, 'Gates McFadden', 1949), + (13, 'Marina Sirtis', 1955), + (14, 'LeVar Burton', 1957), + (15, 'Kate Mulgrew', 1955), + (16, 'Robert Beltran', 1953), + (17, 'Tim Russ', 1956), + (18, 'Roxann Dawson', 1958), + (19, 'Robert Duncan McNeill', 1964), + (20, 'Garrett Wang', 1968), + (21, 'Robert Picardo', 1953), + (22, 'Jeri Ryan', 1968), + (23, 'Avery Brooks', 1948), + (24, 'Nana Visitor', 1957), + (25, 'Rene Auberjonois', 1940), + (26, 'Terry Farrell', 1963), + (27, 'Alexander Siddig', 1965), + (28, 'Armin Shimerman', 1949), + (29, 'Cirroc Lofton', 1978), + (30, 'Scott Bakula', 1954), + (31, 'Jolene Blalock', 1975), + (32, 'John Billingsley', 1960), + (33, 'Connor Trinneer', 1969), + (34, 'Dominic Keating', 1962), + (35, 'Linda Park', 1978), + (36, 'Anthony Montgomery', 1971); + +INSERT INTO Character (Id, Name, ActorId, Stardate) VALUES + (1, 'James T. Kirk', 1, 2233.04), + (2, 'Spock', 2, 2230.06), + (3, 'Leonard McCoy', 3, 2227.00), + (4, 'Montgomery Scott', 4, 2222.00), + (5, 'Uhura', 5, 2233.00), + (6, 'Hikaru Sulu', 6, 2237.00), + (7, 'Pavel Chekov', 7, 2245.00), + (8, 'Jean-Luc Picard', 8, 2305.07), + (9, 'William Riker', 9, 2335.08), + (10, 'Data', 10, 2336.00), + (11, 'Worf', 11, 2340.00), + (12, 'Beverly Crusher', 12, 2324.00), + (13, 'Deanna Troi', 13, 2336.00), + (14, 'Geordi La Forge', 14, 2335.02), + (15, 'Kathryn Janeway', 15, 2336.05), + (16, 'Chakotay', 16, 2329.00), + (17, 'Tuvok', 17, 2264.00), + (18, 'B''Elanna Torres', 18, 2349.00), + (19, 'Tom Paris', 19, 2346.00), + (20, 'Harry Kim', 20, 2349.00), + (21, 'The Doctor', 21, 2371.00), -- Stardate of activation + (22, 'Seven of Nine', 22, 2348.00), + (23, 'Benjamin Sisko', 23, 2332.00), + (24, 'Kira Nerys', 24, 2343.00), + (25, 'Odo', 25, 2337.00), -- Approximate stardate of discovery + (27, 'Jadzia Dax', 26, 2341.00), + (28, 'Julian Bashir', 27, 2341.00), + (29, 'Quark', 28, 2333.00), + (30, 'Jake Sisko', 29, 2355.00), + (31, 'Jonathan Archer', 30, 2112.00), + (32, 'T''Pol', 31, 2088.00), + (33, 'Phlox', 32, 2102.00), + (34, 'Charles "Trip" Tucker III', 33, 2121.00), + (35, 'Malcolm Reed', 34, 2117.00), + (36, 'Hoshi Sato', 35, 2129.00), + (37, 'Travis Mayweather', 36, 2126.00); + +INSERT INTO Series_Character (SeriesId, CharacterId, Role) VALUES + (1, 1, 'Captain'), -- James T. Kirk in Star Trek + (1, 2, 'Science Officer'), -- Spock in Star Trek + (1, 3, 'Doctor'), -- Leonard McCoy in Star Trek + (1, 4, 'Engineer'), -- Montgomery Scott in Star Trek + (1, 5, 'Communications Officer'), -- Uhura in Star Trek + (1, 6, 'Helmsman'), -- Hikaru Sulu in Star Trek + (1, 7, 'Navigator'), -- Pavel Chekov in Star Trek + (2, 8, 'Captain'), -- Jean-Luc Picard in Star Trek: The Next Generation + (2, 9, 'First Officer'), -- William Riker in Star Trek: The Next Generation + (2, 10, 'Operations Officer'),-- Data in Star Trek: The Next Generation + (2, 11, 'Security Officer'),-- Worf in Star Trek: The Next Generation + (2, 12, 'Doctor'),-- Beverly Crusher in Star Trek: The Next Generation + (2, 13, 'Counselor'),-- Deanna Troi in Star Trek: The Next Generation + (2, 14, 'Engineer'),-- Geordi La Forge in Star Trek: The Next Generation + (3, 15, 'Captain'),-- Kathryn Janeway in Star Trek: Voyager + (3, 16, 'First Officer'),-- Chakotay in Star Trek: Voyager + (3, 17, 'Tactical Officer'),-- Tuvok in Star Trek: Voyager + (3, 18, 'Engineer'),-- B'Elanna Torres in Star Trek: Voyager + (3, 19, 'Helmsman'),-- Tom Paris in Star Trek: Voyager + (3, 20, 'Operations Officer'),-- Harry Kim in Star Trek: Voyager + (3, 21, 'Doctor'),-- The Doctor in Star Trek: Voyager + (3, 22, 'Astrometrics Officer'),-- Seven of Nine in Star Trek: Voyager + (4, 23, 'Commanding Officer'),-- Benjamin Sisko in Star Trek: Deep Space Nine + (4, 24, 'First Officer'),-- Kira Nerys in Star Trek: Deep Space Nine + (4, 25, 'Security Officer'),-- Odo in Star Trek: Deep Space Nine + (4, 11, 'Strategic Operations Officer'),-- Worf in Star Trek: Deep Space Nine + (4, 27, 'Science Officer'),-- Jadzia Dax in Star Trek: Deep Space Nine + (4, 28, 'Doctor'),-- Julian Bashir in Star Trek: Deep Space Nine + (4, 29, 'Bar Owner'),-- Quark in Star Trek: Deep Space Nine + (4, 30, 'Civilian'),-- Jake Sisko in Star Trek: Deep Space Nine + (5, 31, 'Captain'),-- Jonathan Archer in Star Trek: Enterprise + (5, 32, 'Science Officer'),-- T'Pol in Star Trek: Enterprise + (5, 33, 'Doctor'),-- Phlox in Star Trek: Enterprise + (5, 34, 'Chief Engineer'),-- Charles "Trip" Tucker III in Star Trek: Enterprise + (5, 35, 'Armory Officer'),-- Malcolm Reed in Star Trek: Enterprise + (5, 36, 'Communications Officer'),-- Hoshi Sato in Star Trek: Enterprise + (5, 37, 'Helmsman');-- Travis Mayweather in Star Trek: Enterprise + +INSERT INTO Character_Species (CharacterId, SpeciesId) VALUES + (1, 1), -- James T. Kirk is Human + (2, 2), -- Spock is Vulcan + (2, 1), -- Spock is also Human + (3, 1), -- Leonard McCoy is Human + (4, 1), -- Montgomery Scott is Human + (5, 1), -- Uhura is Human + (6, 1), -- Hikaru Sulu is Human + (7, 1), -- Pavel Chekov is Human + (8, 1), -- Jean-Luc Picard is Human + (9, 1), -- William Riker is Human + (10, 3), -- Data is Android + (11, 4), -- Worf is Klingon + (12, 1), -- Beverly Crusher is Human + (13, 1), -- Deanna Troi is Human + (13, 5), -- Deanna Troi is also Betazoid + (14, 1), -- Geordi La Forge is Human + (15, 1), -- Kathryn Janeway is Human + (16, 1), -- Chakotay is Human + (17, 2), -- Tuvok is Vulcan + (18, 1), -- B'Elanna Torres is Human + (18, 4), -- B'Elanna Torres is also Klingon + (19, 1), -- Tom Paris is Human + (20, 1), -- Harry Kim is Human + (21, 6), -- The Doctor is a Hologram + (22, 1), -- Seven of Nine is Human + (22, 12),-- Seven of Nine is also Borg + (23, 1), -- Benjamin Sisko is Human + (24, 7), -- Kira Nerys is Bajoran + (25, 8), -- Odo is Changeling + (27, 9), -- Jadzia Dax is Trill + (28, 1), -- Julian Bashir is Human + (29, 10),-- Quark is Ferengi + (30, 1), -- Jake Sisko is Human + (31, 1), -- Jonathan Archer is Human + (32, 2), -- T'Pol is Vulcan + (33, 11),-- Phlox is Denobulan + (34, 1), -- Charles "Trip" Tucker III is Human + (35, 1), -- Malcolm Reed is Human + (36, 1), -- Hoshi Sato is Human + (37, 1); -- Travis Mayweather is Human + \ No newline at end of file diff --git a/src/Azure.DataApiBuilder.sln b/src/Azure.DataApiBuilder.sln index aa3c8e2bad..58fe70b07c 100644 --- a/src/Azure.DataApiBuilder.sln +++ b/src/Azure.DataApiBuilder.sln @@ -1,3 +1,4 @@ + Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.3.32405.409 @@ -31,6 +32,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Azure.DataApiBuilder.Core", EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Azure.DataApiBuilder.Product", "Product\Azure.DataApiBuilder.Product.csproj", "{E3D2076C-EE49-43A0-8F92-5FC41EC99DA7}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Aspire.AppHost", "Aspire.AppHost\Aspire.AppHost.csproj", "{87B53030-EB52-4EB1-870D-72540DA4724E}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Azure.DataApiBuilder.Mcp", "Azure.DataApiBuilder.Mcp\Azure.DataApiBuilder.Mcp.csproj", "{A287E849-A043-4F37-BC40-A87C4705F583}" EndProject Global @@ -75,6 +78,10 @@ Global {E3D2076C-EE49-43A0-8F92-5FC41EC99DA7}.Debug|Any CPU.Build.0 = Debug|Any CPU {E3D2076C-EE49-43A0-8F92-5FC41EC99DA7}.Release|Any CPU.ActiveCfg = Release|Any CPU {E3D2076C-EE49-43A0-8F92-5FC41EC99DA7}.Release|Any CPU.Build.0 = Release|Any CPU + {87B53030-EB52-4EB1-870D-72540DA4724E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {87B53030-EB52-4EB1-870D-72540DA4724E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {87B53030-EB52-4EB1-870D-72540DA4724E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {87B53030-EB52-4EB1-870D-72540DA4724E}.Release|Any CPU.Build.0 = Release|Any CPU {A287E849-A043-4F37-BC40-A87C4705F583}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A287E849-A043-4F37-BC40-A87C4705F583}.Debug|Any CPU.Build.0 = Debug|Any CPU {A287E849-A043-4F37-BC40-A87C4705F583}.Release|Any CPU.ActiveCfg = Release|Any CPU diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 14f097915c..658e489bac 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -3,7 +3,11 @@ true - + + + + + @@ -29,6 +33,9 @@ + + + diff --git a/src/Service/Azure.DataApiBuilder.Service.csproj b/src/Service/Azure.DataApiBuilder.Service.csproj index 6ea9c8dad2..5cf762ca57 100644 --- a/src/Service/Azure.DataApiBuilder.Service.csproj +++ b/src/Service/Azure.DataApiBuilder.Service.csproj @@ -1,7 +1,7 @@  - net8.0 + net8.0 Debug;Release;Docker $(BaseOutputPath)\engine win-x64;linux-x64;osx-x64 diff --git a/src/Service/HealthCheck/HealthCheckHelper.cs b/src/Service/HealthCheck/HealthCheckHelper.cs index 452cb803a9..9b7bbd272c 100644 --- a/src/Service/HealthCheck/HealthCheckHelper.cs +++ b/src/Service/HealthCheck/HealthCheckHelper.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Data.Common; using System.Diagnostics; using System.Linq; using System.Threading.Tasks; @@ -161,7 +162,7 @@ private async Task UpdateDataSourceHealthCheckResultsAsync(ComprehensiveHealthCh if (comprehensiveHealthCheckReport.Checks != null && runtimeConfig.DataSource.IsDatasourceHealthEnabled) { string query = Utilities.GetDatSourceQuery(runtimeConfig.DataSource.DatabaseType); - (int, string?) response = await ExecuteDatasourceQueryCheckAsync(query, runtimeConfig.DataSource.ConnectionString); + (int, string?) response = await ExecuteDatasourceQueryCheckAsync(query, runtimeConfig.DataSource.ConnectionString, Utilities.GetDbProviderFactory(runtimeConfig.DataSource.DatabaseType)); bool isResponseTimeWithinThreshold = response.Item1 >= 0 && response.Item1 < runtimeConfig.DataSource.DatasourceThresholdMs; // Add DataSource Health Check Results @@ -181,14 +182,14 @@ private async Task UpdateDataSourceHealthCheckResultsAsync(ComprehensiveHealthCh } // Executes the DB Query and keeps track of the response time and error message. - private async Task<(int, string?)> ExecuteDatasourceQueryCheckAsync(string query, string connectionString) + private async Task<(int, string?)> ExecuteDatasourceQueryCheckAsync(string query, string connectionString, DbProviderFactory dbProviderFactory) { string? errorMessage = null; if (!string.IsNullOrEmpty(query) && !string.IsNullOrEmpty(connectionString)) { Stopwatch stopwatch = new(); stopwatch.Start(); - errorMessage = await _httpUtility.ExecuteDbQueryAsync(query, connectionString); + errorMessage = await _httpUtility.ExecuteDbQueryAsync(query, connectionString, dbProviderFactory); stopwatch.Stop(); return string.IsNullOrEmpty(errorMessage) ? ((int)stopwatch.ElapsedMilliseconds, errorMessage) : (HealthCheckConstants.ERROR_RESPONSE_TIME_MS, errorMessage); } diff --git a/src/Service/HealthCheck/HttpUtilities.cs b/src/Service/HealthCheck/HttpUtilities.cs index a3195d0d8a..9da596ae30 100644 --- a/src/Service/HealthCheck/HttpUtilities.cs +++ b/src/Service/HealthCheck/HttpUtilities.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Data.Common; using System.Linq; using System.Net.Http; using System.Text; @@ -14,7 +15,6 @@ using Azure.DataApiBuilder.Core.Services; using Azure.DataApiBuilder.Core.Services.MetadataProviders; using Azure.DataApiBuilder.Service.GraphQLBuilder; -using Microsoft.Data.SqlClient; using Microsoft.Extensions.Logging; namespace Azure.DataApiBuilder.Service.HealthCheck @@ -49,19 +49,32 @@ public HttpUtilities( } // Executes the DB query by establishing a connection to the DB. - public async Task ExecuteDbQueryAsync(string query, string connectionString) + public async Task ExecuteDbQueryAsync(string query, string connectionString, DbProviderFactory providerFactory) { string? errorMessage = null; // Execute the query on DB and return the response time. - using (SqlConnection connection = new(connectionString)) + DbConnection? connection = providerFactory.CreateConnection(); + if (connection == null) + { + errorMessage = "Failed to create database connection."; + _logger.LogError(errorMessage); + return errorMessage; + } + + using (connection) { try { - SqlCommand command = new(query, connection); - connection.Open(); - SqlDataReader reader = await command.ExecuteReaderAsync(); - _logger.LogTrace("The health check query for datasource executed successfully."); - reader.Close(); + connection.ConnectionString = connectionString; + using (DbCommand command = connection.CreateCommand()) + { + command.CommandText = query; + await connection.OpenAsync(); + using (DbDataReader reader = await command.ExecuteReaderAsync()) + { + _logger.LogTrace("The health check query for datasource executed successfully."); + } + } } catch (Exception ex) { @@ -145,7 +158,7 @@ public HttpUtilities( List columnNames = dbObject.SourceDefinition.Columns.Keys.ToList(); // In case of GraphQL API, use the plural value specified in [entity.graphql.type.plural]. - // Further, we need to camel case this plural value to match the GraphQL object name. + // Further, we need to camel case this plural value to match the GraphQL object name. string graphqlObjectName = GraphQLNaming.GenerateListQueryName(entityName, entity); // In case any primitive column names are present, execute the query diff --git a/src/Service/HealthCheck/Utilities.cs b/src/Service/HealthCheck/Utilities.cs index 4b61346736..290410291e 100644 --- a/src/Service/HealthCheck/Utilities.cs +++ b/src/Service/HealthCheck/Utilities.cs @@ -1,9 +1,13 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System; using System.Collections.Generic; +using System.Data.Common; using System.Text.Json; using Azure.DataApiBuilder.Config.ObjectModel; +using Microsoft.Data.SqlClient; +using Npgsql; namespace Azure.DataApiBuilder.Service.HealthCheck { @@ -32,6 +36,20 @@ public static string GetDatSourceQuery(DatabaseType dbType) } } + public static DbProviderFactory GetDbProviderFactory(DatabaseType dbType) + { + switch (dbType) + { + case DatabaseType.PostgreSQL: + return NpgsqlFactory.Instance; + case DatabaseType.MSSQL: + case DatabaseType.DWSQL: + return SqlClientFactory.Instance; + default: + throw new NotSupportedException($"Database type '{dbType}' is not supported."); + } + } + public static string CreateHttpGraphQLQuery(string entityName, List columnNames, int first) { var payload = new diff --git a/src/Service/Properties/launchSettings.json b/src/Service/Properties/launchSettings.json index acd7ab6646..becf6cd238 100644 --- a/src/Service/Properties/launchSettings.json +++ b/src/Service/Properties/launchSettings.json @@ -1,22 +1,6 @@ { - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iisExpress": { - "applicationUrl": "http://localhost:35704", - "sslPort": 44353 - } - }, "$schema": "http://json.schemastore.org/launchsettings.json", "profiles": { - "IIS Express": { - "commandName": "IISExpress", - "launchBrowser": true, - "launchUrl": "graphql", - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - }, "Azure.DataApiBuilder.Service": { "commandName": "Project", "launchBrowser": true, @@ -24,7 +8,7 @@ "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "MsSql" }, - "dotnetRunMessages": "true", + "dotnetRunMessages": true, "applicationUrl": "https://localhost:5001;http://localhost:5000" }, "Development": { @@ -34,7 +18,7 @@ "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, - "dotnetRunMessages": "true", + "dotnetRunMessages": true, "applicationUrl": "https://localhost:5001;http://localhost:5000" }, "PostgreSql": { @@ -44,7 +28,7 @@ "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "PostgreSql" }, - "dotnetRunMessages": "true", + "dotnetRunMessages": true, "applicationUrl": "https://localhost:5001;http://localhost:5000" }, "MsSql": { @@ -54,7 +38,7 @@ "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "MsSql" }, - "dotnetRunMessages": "true", + "dotnetRunMessages": true, "applicationUrl": "https://localhost:5001;http://localhost:5000" }, "MySql": { @@ -64,7 +48,7 @@ "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "MySql" }, - "dotnetRunMessages": "true", + "dotnetRunMessages": true, "applicationUrl": "https://localhost:5001;http://localhost:5000" }, "CosmosDb_NoSql": { diff --git a/src/Service/dab-config.json b/src/Service/dab-config.json new file mode 100644 index 0000000000..859267100e --- /dev/null +++ b/src/Service/dab-config.json @@ -0,0 +1,231 @@ +{ + "data-source": { + "database-type": "@env('db-type')", + "connection-string": "@env('ConnectionStrings__Database')", + "options": { + "set-session-context": false + } + }, + "runtime": { + "rest": { + "enabled": true, + "path": "/api", + "request-body-strict": true + }, + "graphql": { + "enabled": true, + "path": "/graphql", + "allow-introspection": true + }, + "host": { + "authentication": { + "provider": "StaticWebApps" + }, + "cors": { + "origins": [], + "allow-credentials": false + }, + "mode": "development" + }, + "telemetry": { + "open-telemetry": { + "enabled": true, + "endpoint": "@env('OTEL_EXPORTER_OTLP_ENDPOINT')", + "headers": "@env('OTEL_EXPORTER_OTLP_HEADERS')", + "exporter-protocol": "@env('OTEL_EXPORTER_OTLP_PROTOCOL')", + "service-name": "@env('OTEL_SERVICE_NAME')" + } + }, + "cache": { + "enabled": true, + "ttl-seconds": 60 + } + }, + "entities": { + "Actor": { + "source": { + "object": "Actor", + "type": "table" + }, + "graphql": { + "enabled": true, + "type": { + "singular": "Actor", + "plural": "Actors" + } + }, + "rest": { + "enabled": true + }, + "permissions": [ + { + "role": "anonymous", + "actions": [ + "*" + ] + } + ], + "relationships": { + "character": { + "cardinality": "many", + "target.entity": "Character", + "source.fields": [ + "Id" + ], + "target.fields": [ + "ActorId" + ], + "linking.source.fields": [], + "linking.target.fields": [] + } + } + }, + "Character": { + "source": { + "object": "Character", + "type": "table" + }, + "graphql": { + "enabled": true, + "type": { + "singular": "Character", + "plural": "Characters" + } + }, + "rest": { + "enabled": true + }, + "permissions": [ + { + "role": "anonymous", + "actions": [ + { + "action": "*" + } + ] + } + ], + "relationships": { + "actor": { + "cardinality": "one", + "target.entity": "Actor", + "source.fields": [ + "ActorId" + ], + "target.fields": [ + "Id" + ], + "linking.source.fields": [], + "linking.target.fields": [] + }, + "series": { + "cardinality": "many", + "target.entity": "Series", + "source.fields": [ + "Id" + ], + "target.fields": [ + "Id" + ], + "linking.object": "series_character", + "linking.source.fields": [ + "CharacterId" + ], + "linking.target.fields": [ + "SeriesId" + ] + } + } + }, + "Series": { + "source": { + "object": "Series", + "type": "table" + }, + "graphql": { + "enabled": true, + "type": { + "singular": "Series", + "plural": "Series" + } + }, + "rest": { + "enabled": true + }, + "permissions": [ + { + "role": "anonymous", + "actions": [ + { + "action": "*" + } + ] + } + ], + "relationships": { + "character": { + "cardinality": "many", + "target.entity": "Character", + "source.fields": [ + "Id" + ], + "target.fields": [ + "Id" + ], + "linking.object": "series_character", + "linking.source.fields": [ + "SeriesId" + ], + "linking.target.fields": [ + "CharacterId" + ] + } + } + }, + "Species": { + "source": { + "object": "Species", + "type": "table" + }, + "graphql": { + "enabled": true, + "type": { + "singular": "Species", + "plural": "Species" + } + }, + "rest": { + "enabled": true + }, + "permissions": [ + { + "role": "anonymous", + "actions": [ + { + "action": "*" + } + ] + } + ], + "relationships": { + "character": { + "cardinality": "many", + "target.entity": "Character", + "source.fields": [ + "Id" + ], + "target.fields": [ + "Id" + ], + "linking.object": "character_species", + "linking.source.fields": [ + "SpeciesId" + ], + "linking.target.fields": [ + "CharacterId" + ] + } + } + } + } + } From 35541182547c85bb64769109d7a97d0a160b9580 Mon Sep 17 00:00:00 2001 From: aaronburtle <93220300+aaronburtle@users.noreply.github.com> Date: Thu, 20 Nov 2025 00:06:25 +0000 Subject: [PATCH 10/36] Update variable replacement during deserialization to use replacement settings class and add AKV replacement logic. (#2882) ## Why make this change? Adds AKV variable replacement and expands our design for doing variable replacements to be more extensible when new variable replacement logic is added. Closes #2708 Closes #2748 Related to #2863 ## What is this change? Change the way that variable replacement is handled to instead of simply using a `bool` to indicate that we want env variable replacement, we add a class which holds all of the replacement settings. This will hold whether or not we will do replacement for each kind of variable that we will handle replacement for during deserialization. We also include the replacement failure mode, and put the logic for handling the replacements into a strategy dictionary which pairs the replacement variable type with the strategy for doing that replacement. Because Azure Key Vault secret replacement requires having the retry and connection settings in order to do the AKV replacement, we must do a first pass where we only do non-AKV replacement and get the required settings so that if AKV replacement is used we have the required settings to do that replacement. We also have to keep in mind that the legacy of the `Configuration Controller` will ignore all variable replacement, so we construct the replacement settings for this code path to not use any variable replacement at all. ## How was this tested? We have updated the logic for the tests to use the new system, however manual testing using an actual AKV is still required. ## Sample Request(s) - Example REST and/or GraphQL request to demonstrate modifications - Example of CLI usage to demonstrate modifications --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Aniruddh Munde --- src/Cli.Tests/EndToEndTests.cs | 6 +- src/Cli.Tests/EnvironmentTests.cs | 8 +- src/Cli/ConfigGenerator.cs | 22 +- src/Cli/Exporter.cs | 3 +- src/Config/Azure.DataApiBuilder.Config.csproj | 1 + .../AKVRetryPolicyOptionsConverterFactory.cs | 34 +-- .../AzureKeyVaultOptionsConverterFactory.cs | 128 +++++++++++ .../AzureLogAnalyticsAuthOptionsConverter.cs | 19 +- ...zureLogAnalyticsOptionsConverterFactory.cs | 32 ++- .../Converters/DataSourceConverterFactory.cs | 34 ++- ...DatasourceHealthOptionsConvertorFactory.cs | 16 +- .../EntityCacheOptionsConverterFactory.cs | 22 +- .../EntityGraphQLOptionsConverterFactory.cs | 38 ++-- .../EntityRestOptionsConverterFactory.cs | 34 ++- .../EntitySourceConverterFactory.cs | 30 ++- .../EnumMemberJsonEnumConverterFactory.cs | 2 +- src/Config/Converters/FileSinkConverter.cs | 21 +- .../GraphQLRuntimeOptionsConverterFactory.cs | 28 +-- .../McpRuntimeOptionsConverterFactory.cs | 16 +- .../RuntimeHealthOptionsConvertorFactory.cs | 16 +- .../Converters/StringJsonConverterFactory.cs | 73 ++---- .../Converters/Utf8JsonReaderExtensions.cs | 10 +- ...erializationVariableReplacementSettings.cs | 213 ++++++++++++++++++ src/Config/FileSystemRuntimeConfigLoader.cs | 32 ++- .../ObjectModel/AzureKeyVaultOptions.cs | 37 +++ src/Config/ObjectModel/RuntimeConfig.cs | 7 +- src/Config/RuntimeConfigLoader.cs | 122 +++++++--- .../Configurations/RuntimeConfigProvider.cs | 9 +- src/Directory.Packages.props | 1 + .../Caching/CachingConfigProcessingTests.cs | 41 +--- .../Configuration/ConfigurationTests.cs | 41 ++-- .../UnitTests/MySqlQueryExecutorUnitTests.cs | 3 +- .../PostgreSqlQueryExecutorUnitTests.cs | 3 +- ...untimeConfigLoaderJsonDeserializerTests.cs | 17 +- .../SerializationDeserializationTests.cs | 66 +++++- .../UnitTests/SqlQueryExecutorUnitTests.cs | 3 +- .../Controllers/ConfigurationController.cs | 4 +- 37 files changed, 835 insertions(+), 357 deletions(-) create mode 100644 src/Config/Converters/AzureKeyVaultOptionsConverterFactory.cs create mode 100644 src/Config/DeserializationVariableReplacementSettings.cs diff --git a/src/Cli.Tests/EndToEndTests.cs b/src/Cli.Tests/EndToEndTests.cs index 7fe017501f..5dbf97ca5e 100644 --- a/src/Cli.Tests/EndToEndTests.cs +++ b/src/Cli.Tests/EndToEndTests.cs @@ -116,10 +116,11 @@ public void TestInitializingRestAndGraphQLGlobalSettings() string[] args = { "init", "-c", TEST_RUNTIME_CONFIG_FILE, "--connection-string", SAMPLE_TEST_CONN_STRING, "--database-type", "mssql", "--rest.path", "/rest-api", "--rest.enabled", "false", "--graphql.path", "/graphql-api" }; Program.Execute(args, _cliLogger!, _fileSystem!, _runtimeConfigLoader!); + DeserializationVariableReplacementSettings replacementSettings = new(azureKeyVaultOptions: null, doReplaceEnvVar: true, doReplaceAkvVar: true); Assert.IsTrue(_runtimeConfigLoader!.TryLoadConfig( TEST_RUNTIME_CONFIG_FILE, out RuntimeConfig? runtimeConfig, - replaceEnvVar: true)); + replacementSettings: replacementSettings)); SqlConnectionStringBuilder builder = new(runtimeConfig.DataSource.ConnectionString); Assert.AreEqual(ProductInfo.GetDataApiBuilderUserAgent(), builder.ApplicationName); @@ -195,10 +196,11 @@ public void TestEnablingMultipleCreateOperation(CliBool isMultipleCreateEnabled, Program.Execute(args.ToArray(), _cliLogger!, _fileSystem!, _runtimeConfigLoader!); + DeserializationVariableReplacementSettings replacementSettings = new(azureKeyVaultOptions: null, doReplaceEnvVar: true, doReplaceAkvVar: true); Assert.IsTrue(_runtimeConfigLoader!.TryLoadConfig( TEST_RUNTIME_CONFIG_FILE, out RuntimeConfig? runtimeConfig, - replaceEnvVar: true)); + replacementSettings: replacementSettings)); Assert.IsNotNull(runtimeConfig); Assert.AreEqual(expectedDbType, runtimeConfig.DataSource.DatabaseType); diff --git a/src/Cli.Tests/EnvironmentTests.cs b/src/Cli.Tests/EnvironmentTests.cs index 151d5babb2..2d6378cf74 100644 --- a/src/Cli.Tests/EnvironmentTests.cs +++ b/src/Cli.Tests/EnvironmentTests.cs @@ -19,7 +19,13 @@ public class EnvironmentTests [TestInitialize] public void TestInitialize() { - StringJsonConverterFactory converterFactory = new(EnvironmentVariableReplacementFailureMode.Throw); + DeserializationVariableReplacementSettings replacementSettings = new( + azureKeyVaultOptions: null, + doReplaceEnvVar: true, + doReplaceAkvVar: false, + envFailureMode: EnvironmentVariableReplacementFailureMode.Throw); + + StringJsonConverterFactory converterFactory = new(replacementSettings); _options = new() { PropertyNameCaseInsensitive = true diff --git a/src/Cli/ConfigGenerator.cs b/src/Cli/ConfigGenerator.cs index 9a56f83c4a..7c35335089 100644 --- a/src/Cli/ConfigGenerator.cs +++ b/src/Cli/ConfigGenerator.cs @@ -2700,9 +2700,10 @@ private static bool TryUpdateConfiguredAzureKeyVaultOptions( // Azure Key Vault Endpoint if (options.AzureKeyVaultEndpoint is not null) { + // Ensure endpoint flag is marked user provided so converter writes it. updatedAzureKeyVaultOptions = updatedAzureKeyVaultOptions is not null - ? updatedAzureKeyVaultOptions with { Endpoint = options.AzureKeyVaultEndpoint } - : new AzureKeyVaultOptions { Endpoint = options.AzureKeyVaultEndpoint }; + ? updatedAzureKeyVaultOptions with { Endpoint = options.AzureKeyVaultEndpoint, UserProvidedEndpoint = true } + : new AzureKeyVaultOptions(endpoint: options.AzureKeyVaultEndpoint); _logger.LogInformation("Updated RuntimeConfig with azure-key-vault.endpoint as '{endpoint}'", options.AzureKeyVaultEndpoint); } @@ -2711,7 +2712,7 @@ private static bool TryUpdateConfiguredAzureKeyVaultOptions( { updatedRetryPolicyOptions = updatedRetryPolicyOptions is not null ? updatedRetryPolicyOptions with { Mode = options.AzureKeyVaultRetryPolicyMode.Value, UserProvidedMode = true } - : new AKVRetryPolicyOptions { Mode = options.AzureKeyVaultRetryPolicyMode.Value, UserProvidedMode = true }; + : new AKVRetryPolicyOptions(mode: options.AzureKeyVaultRetryPolicyMode.Value); _logger.LogInformation("Updated RuntimeConfig with azure-key-vault.retry-policy.mode as '{mode}'", options.AzureKeyVaultRetryPolicyMode.Value); } @@ -2726,7 +2727,7 @@ private static bool TryUpdateConfiguredAzureKeyVaultOptions( updatedRetryPolicyOptions = updatedRetryPolicyOptions is not null ? updatedRetryPolicyOptions with { MaxCount = options.AzureKeyVaultRetryPolicyMaxCount.Value, UserProvidedMaxCount = true } - : new AKVRetryPolicyOptions { MaxCount = options.AzureKeyVaultRetryPolicyMaxCount.Value, UserProvidedMaxCount = true }; + : new AKVRetryPolicyOptions(maxCount: options.AzureKeyVaultRetryPolicyMaxCount.Value); _logger.LogInformation("Updated RuntimeConfig with azure-key-vault.retry-policy.max-count as '{maxCount}'", options.AzureKeyVaultRetryPolicyMaxCount.Value); } @@ -2741,7 +2742,7 @@ private static bool TryUpdateConfiguredAzureKeyVaultOptions( updatedRetryPolicyOptions = updatedRetryPolicyOptions is not null ? updatedRetryPolicyOptions with { DelaySeconds = options.AzureKeyVaultRetryPolicyDelaySeconds.Value, UserProvidedDelaySeconds = true } - : new AKVRetryPolicyOptions { DelaySeconds = options.AzureKeyVaultRetryPolicyDelaySeconds.Value, UserProvidedDelaySeconds = true }; + : new AKVRetryPolicyOptions(delaySeconds: options.AzureKeyVaultRetryPolicyDelaySeconds.Value); _logger.LogInformation("Updated RuntimeConfig with azure-key-vault.retry-policy.delay-seconds as '{delaySeconds}'", options.AzureKeyVaultRetryPolicyDelaySeconds.Value); } @@ -2756,7 +2757,7 @@ private static bool TryUpdateConfiguredAzureKeyVaultOptions( updatedRetryPolicyOptions = updatedRetryPolicyOptions is not null ? updatedRetryPolicyOptions with { MaxDelaySeconds = options.AzureKeyVaultRetryPolicyMaxDelaySeconds.Value, UserProvidedMaxDelaySeconds = true } - : new AKVRetryPolicyOptions { MaxDelaySeconds = options.AzureKeyVaultRetryPolicyMaxDelaySeconds.Value, UserProvidedMaxDelaySeconds = true }; + : new AKVRetryPolicyOptions(maxDelaySeconds: options.AzureKeyVaultRetryPolicyMaxDelaySeconds.Value); _logger.LogInformation("Updated RuntimeConfig with azure-key-vault.retry-policy.max-delay-seconds as '{maxDelaySeconds}'", options.AzureKeyVaultRetryPolicyMaxDelaySeconds.Value); } @@ -2771,16 +2772,17 @@ private static bool TryUpdateConfiguredAzureKeyVaultOptions( updatedRetryPolicyOptions = updatedRetryPolicyOptions is not null ? updatedRetryPolicyOptions with { NetworkTimeoutSeconds = options.AzureKeyVaultRetryPolicyNetworkTimeoutSeconds.Value, UserProvidedNetworkTimeoutSeconds = true } - : new AKVRetryPolicyOptions { NetworkTimeoutSeconds = options.AzureKeyVaultRetryPolicyNetworkTimeoutSeconds.Value, UserProvidedNetworkTimeoutSeconds = true }; + : new AKVRetryPolicyOptions(networkTimeoutSeconds: options.AzureKeyVaultRetryPolicyNetworkTimeoutSeconds.Value); _logger.LogInformation("Updated RuntimeConfig with azure-key-vault.retry-policy.network-timeout-seconds as '{networkTimeoutSeconds}'", options.AzureKeyVaultRetryPolicyNetworkTimeoutSeconds.Value); } - // Update Azure Key Vault options with retry policy if retry policy was modified + // Update Azure Key Vault options with retry policy if modified if (updatedRetryPolicyOptions is not null) { + // Ensure outer AKV object marks retry policy as user provided so it serializes. updatedAzureKeyVaultOptions = updatedAzureKeyVaultOptions is not null - ? updatedAzureKeyVaultOptions with { RetryPolicy = updatedRetryPolicyOptions } - : new AzureKeyVaultOptions { RetryPolicy = updatedRetryPolicyOptions }; + ? updatedAzureKeyVaultOptions with { RetryPolicy = updatedRetryPolicyOptions, UserProvidedRetryPolicy = true } + : new AzureKeyVaultOptions(retryPolicy: updatedRetryPolicyOptions); } // Update runtime config if Azure Key Vault options were modified diff --git a/src/Cli/Exporter.cs b/src/Cli/Exporter.cs index d4f103e868..896b485692 100644 --- a/src/Cli/Exporter.cs +++ b/src/Cli/Exporter.cs @@ -44,7 +44,8 @@ public static bool Export(ExportOptions options, ILogger logger, FileSystemRunti } // Load the runtime configuration from the file - if (!loader.TryLoadConfig(runtimeConfigFile, out RuntimeConfig? runtimeConfig, replaceEnvVar: true)) + DeserializationVariableReplacementSettings replacementSettings = new(azureKeyVaultOptions: null, doReplaceEnvVar: true, doReplaceAkvVar: true); + if (!loader.TryLoadConfig(runtimeConfigFile, out RuntimeConfig? runtimeConfig, replacementSettings: replacementSettings)) { logger.LogError("Failed to read the config file: {0}.", runtimeConfigFile); return false; diff --git a/src/Config/Azure.DataApiBuilder.Config.csproj b/src/Config/Azure.DataApiBuilder.Config.csproj index a494bc38ae..6b5bdf0955 100644 --- a/src/Config/Azure.DataApiBuilder.Config.csproj +++ b/src/Config/Azure.DataApiBuilder.Config.csproj @@ -15,6 +15,7 @@ + diff --git a/src/Config/Converters/AKVRetryPolicyOptionsConverterFactory.cs b/src/Config/Converters/AKVRetryPolicyOptionsConverterFactory.cs index 06d00b64d3..553e43db53 100644 --- a/src/Config/Converters/AKVRetryPolicyOptionsConverterFactory.cs +++ b/src/Config/Converters/AKVRetryPolicyOptionsConverterFactory.cs @@ -12,9 +12,9 @@ namespace Azure.DataApiBuilder.Config.Converters; /// internal class AKVRetryPolicyOptionsConverterFactory : JsonConverterFactory { - // Determines whether to replace environment variable with its - // value or not while deserializing. - private bool _replaceEnvVar; + // Settings for variable replacement during deserialization. + // Currently allows for Azure Key Vault (via @akv('secret-name')) and Environment Variable replacement. + private readonly DeserializationVariableReplacementSettings? _replacementSettings; /// public override bool CanConvert(Type typeToConvert) @@ -25,34 +25,34 @@ public override bool CanConvert(Type typeToConvert) /// public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) { - return new AKVRetryPolicyOptionsConverter(_replaceEnvVar); + return new AKVRetryPolicyOptionsConverter(_replacementSettings); } - /// Whether to replace environment variable with its - /// value or not while deserializing. - internal AKVRetryPolicyOptionsConverterFactory(bool replaceEnvVar) + /// Settings for variable replacement during deserialization. + /// If null, no variable replacement will be performed. + internal AKVRetryPolicyOptionsConverterFactory(DeserializationVariableReplacementSettings? replacementSettings = null) { - _replaceEnvVar = replaceEnvVar; + _replacementSettings = replacementSettings; } private class AKVRetryPolicyOptionsConverter : JsonConverter { - // Determines whether to replace environment variable with its - // value or not while deserializing. - private bool _replaceEnvVar; + // Settings for variable replacement during deserialization. + // Currently allows for Azure Key Vault (via @akv('')) and Environment Variable replacement. + private readonly DeserializationVariableReplacementSettings? _replacementSettings; - /// Whether to replace environment variable with its - /// value or not while deserializing. - public AKVRetryPolicyOptionsConverter(bool replaceEnvVar) + /// Settings for variable replacement during deserialization. + /// If null, no variable replacement will be performed. + public AKVRetryPolicyOptionsConverter(DeserializationVariableReplacementSettings? replacementSettings) { - _replaceEnvVar = replaceEnvVar; + _replacementSettings = replacementSettings; } /// /// Defines how DAB reads AKV Retry Policy options and defines which values are /// used to instantiate those options. /// - /// Thrown when improperly formatted cache options are provided. + /// Thrown when improperly formatted retry policy options are provided. public override AKVRetryPolicyOptions? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { if (reader.TokenType is JsonTokenType.StartObject) @@ -82,7 +82,7 @@ public AKVRetryPolicyOptionsConverter(bool replaceEnvVar) } else { - mode = EnumExtensions.Deserialize(reader.DeserializeString(_replaceEnvVar)!); + mode = EnumExtensions.Deserialize(reader.DeserializeString(_replacementSettings)!); } break; diff --git a/src/Config/Converters/AzureKeyVaultOptionsConverterFactory.cs b/src/Config/Converters/AzureKeyVaultOptionsConverterFactory.cs new file mode 100644 index 0000000000..92ed0c1a85 --- /dev/null +++ b/src/Config/Converters/AzureKeyVaultOptionsConverterFactory.cs @@ -0,0 +1,128 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json; +using System.Text.Json.Serialization; +using Azure.DataApiBuilder.Config.ObjectModel; + +namespace Azure.DataApiBuilder.Config.Converters; + +/// +/// Converter factory for AzureKeyVaultOptions that can optionally perform variable replacement. +/// +internal class AzureKeyVaultOptionsConverterFactory : JsonConverterFactory +{ + // Determines whether to replace environment variable with its + // value or not while deserializing. + private readonly DeserializationVariableReplacementSettings? _replacementSettings; + + /// How to handle variable replacement during deserialization. + internal AzureKeyVaultOptionsConverterFactory(DeserializationVariableReplacementSettings? replacementSettings = null) + { + _replacementSettings = replacementSettings; + } + + /// + public override bool CanConvert(Type typeToConvert) + { + return typeToConvert.IsAssignableTo(typeof(AzureKeyVaultOptions)); + } + + /// + public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) + { + return new AzureKeyVaultOptionsConverter(_replacementSettings); + } + + private class AzureKeyVaultOptionsConverter : JsonConverter + { + // Determines whether to replace environment variable with its + // value or not while deserializing. + private readonly DeserializationVariableReplacementSettings? _replacementSettings; + + /// Whether to replace environment variable with its + /// value or not while deserializing. + public AzureKeyVaultOptionsConverter(DeserializationVariableReplacementSettings? replacementSettings) + { + _replacementSettings = replacementSettings; + } + + /// + /// Reads AzureKeyVaultOptions with optional variable replacement. + /// + public override AzureKeyVaultOptions? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType is JsonTokenType.Null) + { + return null; + } + + if (reader.TokenType is JsonTokenType.StartObject) + { + string? endpoint = null; + AKVRetryPolicyOptions? retryPolicy = null; + + while (reader.Read()) + { + if (reader.TokenType is JsonTokenType.EndObject) + { + return new AzureKeyVaultOptions(endpoint, retryPolicy); + } + + string? property = reader.GetString(); + reader.Read(); + + switch (property) + { + case "endpoint": + if (reader.TokenType is JsonTokenType.String) + { + endpoint = reader.DeserializeString(_replacementSettings); + } + + break; + + case "retry-policy": + if (reader.TokenType is JsonTokenType.StartObject) + { + // Uses the AKVRetryPolicyOptionsConverter to read the retry-policy object. + retryPolicy = JsonSerializer.Deserialize(ref reader, options); + } + + break; + + default: + throw new JsonException($"Unexpected property {property}"); + } + } + } + + throw new JsonException("Invalid AzureKeyVaultOptions format"); + } + + /// + /// When writing the AzureKeyVaultOptions back to a JSON file, only write the properties + /// if they are user provided. This avoids polluting the written JSON file with properties + /// the user most likely omitted when writing the original DAB runtime config file. + /// This Write operation is only used when a RuntimeConfig object is serialized to JSON. + /// + public override void Write(Utf8JsonWriter writer, AzureKeyVaultOptions value, JsonSerializerOptions options) + { + writer.WriteStartObject(); + + if (value?.UserProvidedEndpoint is true) + { + writer.WritePropertyName("endpoint"); + JsonSerializer.Serialize(writer, value.Endpoint, options); + } + + if (value?.UserProvidedRetryPolicy is true) + { + writer.WritePropertyName("retry-policy"); + JsonSerializer.Serialize(writer, value.RetryPolicy, options); + } + + writer.WriteEndObject(); + } + } +} diff --git a/src/Config/Converters/AzureLogAnalyticsAuthOptionsConverter.cs b/src/Config/Converters/AzureLogAnalyticsAuthOptionsConverter.cs index 1428c0d75f..d4b7623aa2 100644 --- a/src/Config/Converters/AzureLogAnalyticsAuthOptionsConverter.cs +++ b/src/Config/Converters/AzureLogAnalyticsAuthOptionsConverter.cs @@ -9,15 +9,14 @@ namespace Azure.DataApiBuilder.Config.Converters; internal class AzureLogAnalyticsAuthOptionsConverter : JsonConverter { - // Determines whether to replace environment variable with its - // value or not while deserializing. - private bool _replaceEnvVar; + // Settings for variable replacement during deserialization. + private readonly DeserializationVariableReplacementSettings? _replacementSettings; - /// Whether to replace environment variable with its - /// value or not while deserializing. - public AzureLogAnalyticsAuthOptionsConverter(bool replaceEnvVar) + /// Settings for variable replacement during deserialization. + /// If null, no variable replacement will be performed. + public AzureLogAnalyticsAuthOptionsConverter(DeserializationVariableReplacementSettings? replacementSettings = null) { - _replaceEnvVar = replaceEnvVar; + _replacementSettings = replacementSettings; } /// @@ -48,7 +47,7 @@ public AzureLogAnalyticsAuthOptionsConverter(bool replaceEnvVar) case "custom-table-name": if (reader.TokenType is not JsonTokenType.Null) { - customTableName = reader.DeserializeString(_replaceEnvVar); + customTableName = reader.DeserializeString(_replacementSettings); } break; @@ -56,7 +55,7 @@ public AzureLogAnalyticsAuthOptionsConverter(bool replaceEnvVar) case "dcr-immutable-id": if (reader.TokenType is not JsonTokenType.Null) { - dcrImmutableId = reader.DeserializeString(_replaceEnvVar); + dcrImmutableId = reader.DeserializeString(_replacementSettings); } break; @@ -64,7 +63,7 @@ public AzureLogAnalyticsAuthOptionsConverter(bool replaceEnvVar) case "dce-endpoint": if (reader.TokenType is not JsonTokenType.Null) { - dceEndpoint = reader.DeserializeString(_replaceEnvVar); + dceEndpoint = reader.DeserializeString(_replacementSettings); } break; diff --git a/src/Config/Converters/AzureLogAnalyticsOptionsConverterFactory.cs b/src/Config/Converters/AzureLogAnalyticsOptionsConverterFactory.cs index 3fcbe8c7bd..fc7c72d655 100644 --- a/src/Config/Converters/AzureLogAnalyticsOptionsConverterFactory.cs +++ b/src/Config/Converters/AzureLogAnalyticsOptionsConverterFactory.cs @@ -12,9 +12,8 @@ namespace Azure.DataApiBuilder.Config.Converters; /// internal class AzureLogAnalyticsOptionsConverterFactory : JsonConverterFactory { - // Determines whether to replace environment variable with its - // value or not while deserializing. - private bool _replaceEnvVar; + // Settings for variable replacement during deserialization. + private readonly DeserializationVariableReplacementSettings? _replacementSettings; /// public override bool CanConvert(Type typeToConvert) @@ -25,27 +24,26 @@ public override bool CanConvert(Type typeToConvert) /// public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) { - return new AzureLogAnalyticsOptionsConverter(_replaceEnvVar); + return new AzureLogAnalyticsOptionsConverter(_replacementSettings); } - /// Whether to replace environment variable with its - /// value or not while deserializing. - internal AzureLogAnalyticsOptionsConverterFactory(bool replaceEnvVar) + /// Settings for variable replacement during deserialization. + /// If null, no variable replacement will be performed. + internal AzureLogAnalyticsOptionsConverterFactory(DeserializationVariableReplacementSettings? replacementSettings = null) { - _replaceEnvVar = replaceEnvVar; + _replacementSettings = replacementSettings; } private class AzureLogAnalyticsOptionsConverter : JsonConverter { - // Determines whether to replace environment variable with its - // value or not while deserializing. - private bool _replaceEnvVar; + // Settings for variable replacement during deserialization. + private readonly DeserializationVariableReplacementSettings? _replacementSettings; - /// Whether to replace environment variable with its - /// value or not while deserializing. - internal AzureLogAnalyticsOptionsConverter(bool replaceEnvVar) + /// Settings for variable replacement during deserialization. + /// If null, no variable replacement will be performed. + internal AzureLogAnalyticsOptionsConverter(DeserializationVariableReplacementSettings? replacementSettings) { - _replaceEnvVar = replaceEnvVar; + _replacementSettings = replacementSettings; } /// @@ -57,7 +55,7 @@ internal AzureLogAnalyticsOptionsConverter(bool replaceEnvVar) { if (reader.TokenType is JsonTokenType.StartObject) { - AzureLogAnalyticsAuthOptionsConverter authOptionsConverter = new(_replaceEnvVar); + AzureLogAnalyticsAuthOptionsConverter authOptionsConverter = new(_replacementSettings); bool? enabled = null; AzureLogAnalyticsAuthOptions? auth = null; @@ -91,7 +89,7 @@ internal AzureLogAnalyticsOptionsConverter(bool replaceEnvVar) case "dab-identifier": if (reader.TokenType is not JsonTokenType.Null) { - logType = reader.DeserializeString(_replaceEnvVar); + logType = reader.DeserializeString(_replacementSettings); } break; diff --git a/src/Config/Converters/DataSourceConverterFactory.cs b/src/Config/Converters/DataSourceConverterFactory.cs index dabbee405e..1788ebf2b4 100644 --- a/src/Config/Converters/DataSourceConverterFactory.cs +++ b/src/Config/Converters/DataSourceConverterFactory.cs @@ -9,9 +9,8 @@ namespace Azure.DataApiBuilder.Config.Converters; internal class DataSourceConverterFactory : JsonConverterFactory { - // Determines whether to replace environment variable with its - // value or not while deserializing. - private bool _replaceEnvVar; + // Settings for variable replacement during deserialization. + private readonly DeserializationVariableReplacementSettings? _replacementSettings; /// public override bool CanConvert(Type typeToConvert) @@ -22,27 +21,26 @@ public override bool CanConvert(Type typeToConvert) /// public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) { - return new DataSourceConverter(_replaceEnvVar); + return new DataSourceConverter(_replacementSettings); } - /// Whether to replace environment variable with its - /// value or not while deserializing. - internal DataSourceConverterFactory(bool replaceEnvVar) + /// Settings for variable replacement during deserialization. + /// If null, no variable replacement will be performed. + internal DataSourceConverterFactory(DeserializationVariableReplacementSettings? replacementSettings = null) { - _replaceEnvVar = replaceEnvVar; + _replacementSettings = replacementSettings; } private class DataSourceConverter : JsonConverter { - // Determines whether to replace environment variable with its - // value or not while deserializing. - private bool _replaceEnvVar; + // Settings for variable replacement during deserialization. + private readonly DeserializationVariableReplacementSettings? _replacementSettings; - /// Whether to replace environment variable with its - /// value or not while deserializing. - public DataSourceConverter(bool replaceEnvVar) + /// Settings for variable replacement during deserialization. + /// If null, no variable replacement will be performed. + public DataSourceConverter(DeserializationVariableReplacementSettings? replacementSettings) { - _replaceEnvVar = replaceEnvVar; + _replacementSettings = replacementSettings; } public override DataSource? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) @@ -69,11 +67,11 @@ public DataSourceConverter(bool replaceEnvVar) switch (propertyName) { case "database-type": - databaseType = EnumExtensions.Deserialize(reader.DeserializeString(_replaceEnvVar)!); + databaseType = EnumExtensions.Deserialize(reader.DeserializeString(_replacementSettings)!); break; case "connection-string": - connectionString = reader.DeserializeString(replaceEnvVar: _replaceEnvVar)!; + connectionString = reader.DeserializeString(_replacementSettings)!; break; case "health": @@ -106,7 +104,7 @@ public DataSourceConverter(bool replaceEnvVar) if (reader.TokenType is JsonTokenType.String) { // Determine whether to resolve the environment variable or keep as-is. - string stringValue = reader.DeserializeString(replaceEnvVar: _replaceEnvVar)!; + string stringValue = reader.DeserializeString(_replacementSettings)!; if (bool.TryParse(stringValue, out bool boolValue)) { diff --git a/src/Config/Converters/DatasourceHealthOptionsConvertorFactory.cs b/src/Config/Converters/DatasourceHealthOptionsConvertorFactory.cs index 52272c57a7..d8286ff7a0 100644 --- a/src/Config/Converters/DatasourceHealthOptionsConvertorFactory.cs +++ b/src/Config/Converters/DatasourceHealthOptionsConvertorFactory.cs @@ -11,7 +11,7 @@ internal class DataSourceHealthOptionsConvertorFactory : JsonConverterFactory { // Determines whether to replace environment variable with its // value or not while deserializing. - private bool _replaceEnvVar; + private readonly DeserializationVariableReplacementSettings? _replacementSettings; /// public override bool CanConvert(Type typeToConvert) @@ -22,27 +22,27 @@ public override bool CanConvert(Type typeToConvert) /// public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) { - return new HealthCheckOptionsConverter(_replaceEnvVar); + return new HealthCheckOptionsConverter(_replacementSettings); } /// Whether to replace environment variable with its /// value or not while deserializing. - internal DataSourceHealthOptionsConvertorFactory(bool replaceEnvVar) + internal DataSourceHealthOptionsConvertorFactory(DeserializationVariableReplacementSettings? replacementSettings) { - _replaceEnvVar = replaceEnvVar; + _replacementSettings = replacementSettings; } private class HealthCheckOptionsConverter : JsonConverter { // Determines whether to replace environment variable with its // value or not while deserializing. - private bool _replaceEnvVar; + private readonly DeserializationVariableReplacementSettings? _replacementSettings; /// Whether to replace environment variable with its /// value or not while deserializing. - public HealthCheckOptionsConverter(bool replaceEnvVar) + public HealthCheckOptionsConverter(DeserializationVariableReplacementSettings? replacementSettings) { - _replaceEnvVar = replaceEnvVar; + _replacementSettings = replacementSettings; } /// @@ -85,7 +85,7 @@ public HealthCheckOptionsConverter(bool replaceEnvVar) case "name": if (reader.TokenType is not JsonTokenType.Null) { - name = reader.DeserializeString(_replaceEnvVar); + name = reader.DeserializeString(_replacementSettings); } break; diff --git a/src/Config/Converters/EntityCacheOptionsConverterFactory.cs b/src/Config/Converters/EntityCacheOptionsConverterFactory.cs index 32a616ab81..641efd062f 100644 --- a/src/Config/Converters/EntityCacheOptionsConverterFactory.cs +++ b/src/Config/Converters/EntityCacheOptionsConverterFactory.cs @@ -14,7 +14,7 @@ internal class EntityCacheOptionsConverterFactory : JsonConverterFactory { // Determines whether to replace environment variable with its // value or not while deserializing. - private bool _replaceEnvVar; + private readonly DeserializationVariableReplacementSettings? _replacementSettings; /// public override bool CanConvert(Type typeToConvert) @@ -25,27 +25,25 @@ public override bool CanConvert(Type typeToConvert) /// public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) { - return new EntityCacheOptionsConverter(_replaceEnvVar); + return new EntityCacheOptionsConverter(_replacementSettings); } - /// Whether to replace environment variable with its - /// value or not while deserializing. - internal EntityCacheOptionsConverterFactory(bool replaceEnvVar) + /// The replacement settings to use while deserializing. + internal EntityCacheOptionsConverterFactory(DeserializationVariableReplacementSettings? replacementSettings) { - _replaceEnvVar = replaceEnvVar; + _replacementSettings = replacementSettings; } private class EntityCacheOptionsConverter : JsonConverter { // Determines whether to replace environment variable with its // value or not while deserializing. - private bool _replaceEnvVar; + private readonly DeserializationVariableReplacementSettings? _replacementSettings; - /// Whether to replace environment variable with its - /// value or not while deserializing. - public EntityCacheOptionsConverter(bool replaceEnvVar) + /// The replacement settings to use while deserializing. + public EntityCacheOptionsConverter(DeserializationVariableReplacementSettings? replacementSettings) { - _replaceEnvVar = replaceEnvVar; + _replacementSettings = replacementSettings; } /// @@ -110,7 +108,7 @@ public EntityCacheOptionsConverter(bool replaceEnvVar) throw new JsonException("level property cannot be null."); } - level = EnumExtensions.Deserialize(reader.DeserializeString(_replaceEnvVar)!); + level = EnumExtensions.Deserialize(reader.DeserializeString(_replacementSettings)!); break; } diff --git a/src/Config/Converters/EntityGraphQLOptionsConverterFactory.cs b/src/Config/Converters/EntityGraphQLOptionsConverterFactory.cs index 576850b1cb..abe094e970 100644 --- a/src/Config/Converters/EntityGraphQLOptionsConverterFactory.cs +++ b/src/Config/Converters/EntityGraphQLOptionsConverterFactory.cs @@ -9,9 +9,8 @@ namespace Azure.DataApiBuilder.Config.Converters; internal class EntityGraphQLOptionsConverterFactory : JsonConverterFactory { - /// Determines whether to replace environment variable with its - /// value or not while deserializing. - private bool _replaceEnvVar; + /// Settings for variable replacement during deserialization. + private readonly DeserializationVariableReplacementSettings? _replacementSettings; /// public override bool CanConvert(Type typeToConvert) @@ -22,27 +21,26 @@ public override bool CanConvert(Type typeToConvert) /// public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) { - return new EntityGraphQLOptionsConverter(_replaceEnvVar); + return new EntityGraphQLOptionsConverter(_replacementSettings); } - /// Whether to replace environment variable with its - /// value or not while deserializing. - internal EntityGraphQLOptionsConverterFactory(bool replaceEnvVar) + /// Settings for variable replacement during deserialization. + /// If null, no variable replacement will be performed. + internal EntityGraphQLOptionsConverterFactory(DeserializationVariableReplacementSettings? replacementSettings = null) { - _replaceEnvVar = replaceEnvVar; + _replacementSettings = replacementSettings; } private class EntityGraphQLOptionsConverter : JsonConverter { - // Determines whether to replace environment variable with its - // value or not while deserializing. - private bool _replaceEnvVar; + // Settings for variable replacement during deserialization. + private readonly DeserializationVariableReplacementSettings? _replacementSettings; - /// Whether to replace environment variable with its - /// value or not while deserializing. - public EntityGraphQLOptionsConverter(bool replaceEnvVar) + /// Settings for variable replacement during deserialization. + /// If null, no variable replacement will be performed. + public EntityGraphQLOptionsConverter(DeserializationVariableReplacementSettings? replacementSettings) { - _replaceEnvVar = replaceEnvVar; + _replacementSettings = replacementSettings; } /// @@ -73,7 +71,7 @@ public EntityGraphQLOptionsConverter(bool replaceEnvVar) case "type": if (reader.TokenType is JsonTokenType.String) { - singular = reader.DeserializeString(_replaceEnvVar) ?? string.Empty; + singular = reader.DeserializeString(_replacementSettings) ?? string.Empty; } else if (reader.TokenType is JsonTokenType.StartObject) { @@ -95,10 +93,10 @@ public EntityGraphQLOptionsConverter(bool replaceEnvVar) switch (property2) { case "singular": - singular = reader.DeserializeString(_replaceEnvVar) ?? string.Empty; + singular = reader.DeserializeString(_replacementSettings) ?? string.Empty; break; case "plural": - plural = reader.DeserializeString(_replaceEnvVar) ?? string.Empty; + plural = reader.DeserializeString(_replacementSettings) ?? string.Empty; break; } } @@ -112,7 +110,7 @@ public EntityGraphQLOptionsConverter(bool replaceEnvVar) break; case "operation": - string? op = reader.DeserializeString(_replaceEnvVar); + string? op = reader.DeserializeString(_replacementSettings); if (op is not null) { @@ -136,7 +134,7 @@ public EntityGraphQLOptionsConverter(bool replaceEnvVar) if (reader.TokenType is JsonTokenType.String) { - string? singular = reader.DeserializeString(_replaceEnvVar); + string? singular = reader.DeserializeString(_replacementSettings); return new EntityGraphQLOptions(singular ?? string.Empty, string.Empty); } diff --git a/src/Config/Converters/EntityRestOptionsConverterFactory.cs b/src/Config/Converters/EntityRestOptionsConverterFactory.cs index cc33943caa..f8c9096673 100644 --- a/src/Config/Converters/EntityRestOptionsConverterFactory.cs +++ b/src/Config/Converters/EntityRestOptionsConverterFactory.cs @@ -9,9 +9,8 @@ namespace Azure.DataApiBuilder.Config.Converters; internal class EntityRestOptionsConverterFactory : JsonConverterFactory { - /// Determines whether to replace environment variable with its - /// value or not while deserializing. - private bool _replaceEnvVar; + /// Settings for variable replacement during deserialization. + private readonly DeserializationVariableReplacementSettings? _replacementSettings; /// public override bool CanConvert(Type typeToConvert) @@ -22,27 +21,26 @@ public override bool CanConvert(Type typeToConvert) /// public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) { - return new EntityRestOptionsConverter(_replaceEnvVar); + return new EntityRestOptionsConverter(_replacementSettings); } - /// Whether to replace environment variable with its - /// value or not while deserializing. - internal EntityRestOptionsConverterFactory(bool replaceEnvVar) + /// Settings for variable replacement during deserialization. + /// If null, no variable replacement will be performed. + internal EntityRestOptionsConverterFactory(DeserializationVariableReplacementSettings? replacementSettings = null) { - _replaceEnvVar = replaceEnvVar; + _replacementSettings = replacementSettings; } internal class EntityRestOptionsConverter : JsonConverter { - // Determines whether to replace environment variable with its - // value or not while deserializing. - private bool _replaceEnvVar; + // Settings for variable replacement during deserialization. + private readonly DeserializationVariableReplacementSettings? _replacementSettings; - /// Whether to replace environment variable with its - /// value or not while deserializing. - public EntityRestOptionsConverter(bool replaceEnvVar) + /// Settings for variable replacement during deserialization. + /// If null, no variable replacement will be performed. + public EntityRestOptionsConverter(DeserializationVariableReplacementSettings? replacementSettings) { - _replaceEnvVar = replaceEnvVar; + _replacementSettings = replacementSettings; } /// @@ -67,7 +65,7 @@ public EntityRestOptionsConverter(bool replaceEnvVar) if (reader.TokenType is JsonTokenType.String || reader.TokenType is JsonTokenType.Null) { - restOptions = restOptions with { Path = reader.DeserializeString(_replaceEnvVar) }; + restOptions = restOptions with { Path = reader.DeserializeString(_replacementSettings) }; break; } @@ -87,7 +85,7 @@ public EntityRestOptionsConverter(bool replaceEnvVar) break; } - methods.Add(EnumExtensions.Deserialize(reader.DeserializeString(replaceEnvVar: true)!)); + methods.Add(EnumExtensions.Deserialize(reader.DeserializeString(new DeserializationVariableReplacementSettings())!)); } restOptions = restOptions with { Methods = methods.ToArray() }; @@ -107,7 +105,7 @@ public EntityRestOptionsConverter(bool replaceEnvVar) if (reader.TokenType is JsonTokenType.String) { - return new EntityRestOptions(Array.Empty(), reader.DeserializeString(_replaceEnvVar), true); + return new EntityRestOptions(Array.Empty(), reader.DeserializeString(_replacementSettings), true); } if (reader.TokenType is JsonTokenType.True || reader.TokenType is JsonTokenType.False) diff --git a/src/Config/Converters/EntitySourceConverterFactory.cs b/src/Config/Converters/EntitySourceConverterFactory.cs index a748382e01..2edafe31e1 100644 --- a/src/Config/Converters/EntitySourceConverterFactory.cs +++ b/src/Config/Converters/EntitySourceConverterFactory.cs @@ -9,9 +9,8 @@ namespace Azure.DataApiBuilder.Config.Converters; internal class EntitySourceConverterFactory : JsonConverterFactory { - // Determines whether to replace environment variable with its - // value or not while deserializing. - private bool _replaceEnvVar; + // Settings for variable replacement during deserialization. + private readonly DeserializationVariableReplacementSettings? _replacementSettings; /// public override bool CanConvert(Type typeToConvert) @@ -22,34 +21,33 @@ public override bool CanConvert(Type typeToConvert) /// public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) { - return new EntitySourceConverter(_replaceEnvVar); + return new EntitySourceConverter(_replacementSettings); } - /// Whether to replace environment variable with its - /// value or not while deserializing. - internal EntitySourceConverterFactory(bool replaceEnvVar) + /// Settings for variable replacement during deserialization. + /// If null, no variable replacement will be performed. + internal EntitySourceConverterFactory(DeserializationVariableReplacementSettings? replacementSettings = null) { - _replaceEnvVar = replaceEnvVar; + _replacementSettings = replacementSettings; } private class EntitySourceConverter : JsonConverter { - // Determines whether to replace environment variable with its - // value or not while deserializing. - private bool _replaceEnvVar; + // Settings for variable replacement during deserialization. + private readonly DeserializationVariableReplacementSettings? _replacementSettings; - /// Whether to replace environment variable with its - /// value or not while deserializing. - public EntitySourceConverter(bool replaceEnvVar) + /// Settings for variable replacement during deserialization. + /// If null, no variable replacement will be performed. + public EntitySourceConverter(DeserializationVariableReplacementSettings? replacementSettings) { - _replaceEnvVar = replaceEnvVar; + _replacementSettings = replacementSettings; } public override EntitySource? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { if (reader.TokenType == JsonTokenType.String) { - string? obj = reader.DeserializeString(_replaceEnvVar); + string? obj = reader.DeserializeString(_replacementSettings); return new EntitySource(obj ?? string.Empty, EntitySourceType.Table, new(), Array.Empty()); } diff --git a/src/Config/Converters/EnumMemberJsonEnumConverterFactory.cs b/src/Config/Converters/EnumMemberJsonEnumConverterFactory.cs index 1d6dd9f7c4..4455a474e1 100644 --- a/src/Config/Converters/EnumMemberJsonEnumConverterFactory.cs +++ b/src/Config/Converters/EnumMemberJsonEnumConverterFactory.cs @@ -114,7 +114,7 @@ public JsonStringEnumConverterEx() public override TEnum Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { // Always replace env variable in case of Enum otherwise string to enum conversion will fail. - string? stringValue = reader.DeserializeString(replaceEnvVar: true); + string? stringValue = reader.DeserializeString(new(doReplaceEnvVar: true)); if (stringValue == null) { diff --git a/src/Config/Converters/FileSinkConverter.cs b/src/Config/Converters/FileSinkConverter.cs index cc7d138a1b..4299fb913b 100644 --- a/src/Config/Converters/FileSinkConverter.cs +++ b/src/Config/Converters/FileSinkConverter.cs @@ -7,18 +7,17 @@ using Serilog; namespace Azure.DataApiBuilder.Config.Converters; + class FileSinkConverter : JsonConverter { - // Determines whether to replace environment variable with its - // value or not while deserializing. - private bool _replaceEnvVar; - - /// - /// Whether to replace environment variable with its value or not while deserializing. - /// - public FileSinkConverter(bool replaceEnvVar) + // Settings for variable replacement during deserialization. + private readonly DeserializationVariableReplacementSettings? _replacementSettings; + + /// Settings for variable replacement during deserialization. + /// If null, no variable replacement will be performed. + public FileSinkConverter(DeserializationVariableReplacementSettings? replacementSettings = null) { - _replaceEnvVar = replaceEnvVar; + _replacementSettings = replacementSettings; } /// @@ -59,7 +58,7 @@ public FileSinkConverter(bool replaceEnvVar) case "path": if (reader.TokenType is not JsonTokenType.Null) { - path = reader.DeserializeString(_replaceEnvVar); + path = reader.DeserializeString(_replacementSettings); } break; @@ -67,7 +66,7 @@ public FileSinkConverter(bool replaceEnvVar) case "rolling-interval": if (reader.TokenType is not JsonTokenType.Null) { - rollingInterval = EnumExtensions.Deserialize(reader.DeserializeString(_replaceEnvVar)!); + rollingInterval = EnumExtensions.Deserialize(reader.DeserializeString(_replacementSettings)!); } break; diff --git a/src/Config/Converters/GraphQLRuntimeOptionsConverterFactory.cs b/src/Config/Converters/GraphQLRuntimeOptionsConverterFactory.cs index 082c982e7e..109caef0d5 100644 --- a/src/Config/Converters/GraphQLRuntimeOptionsConverterFactory.cs +++ b/src/Config/Converters/GraphQLRuntimeOptionsConverterFactory.cs @@ -9,9 +9,8 @@ namespace Azure.DataApiBuilder.Config.Converters; internal class GraphQLRuntimeOptionsConverterFactory : JsonConverterFactory { - // Determines whether to replace environment variable with its - // value or not while deserializing. - private bool _replaceEnvVar; + // Settings for variable replacement during deserialization. + private readonly DeserializationVariableReplacementSettings? _replacementSettings; /// public override bool CanConvert(Type typeToConvert) @@ -22,25 +21,26 @@ public override bool CanConvert(Type typeToConvert) /// public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) { - return new GraphQLRuntimeOptionsConverter(_replaceEnvVar); + return new GraphQLRuntimeOptionsConverter(_replacementSettings); } - internal GraphQLRuntimeOptionsConverterFactory(bool replaceEnvVar) + /// Settings for variable replacement during deserialization. + /// If null, no variable replacement will be performed. + internal GraphQLRuntimeOptionsConverterFactory(DeserializationVariableReplacementSettings? replacementSettings = null) { - _replaceEnvVar = replaceEnvVar; + _replacementSettings = replacementSettings; } private class GraphQLRuntimeOptionsConverter : JsonConverter { - // Determines whether to replace environment variable with its - // value or not while deserializing. - private bool _replaceEnvVar; + // Settings for variable replacement during deserialization. + private readonly DeserializationVariableReplacementSettings? _replacementSettings; - /// Whether to replace environment variable with its - /// value or not while deserializing. - internal GraphQLRuntimeOptionsConverter(bool replaceEnvVar) + /// Settings for variable replacement during deserialization. + /// If null, no variable replacement will be performed. + internal GraphQLRuntimeOptionsConverter(DeserializationVariableReplacementSettings? replacementSettings) { - _replaceEnvVar = replaceEnvVar; + _replacementSettings = replacementSettings; } public override GraphQLRuntimeOptions? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) @@ -117,7 +117,7 @@ internal GraphQLRuntimeOptionsConverter(bool replaceEnvVar) case "path": if (reader.TokenType is JsonTokenType.String) { - string? path = reader.DeserializeString(_replaceEnvVar); + string? path = reader.DeserializeString(_replacementSettings); if (path is null) { path = "/graphql"; diff --git a/src/Config/Converters/McpRuntimeOptionsConverterFactory.cs b/src/Config/Converters/McpRuntimeOptionsConverterFactory.cs index db9acfa603..d75cbbef5a 100644 --- a/src/Config/Converters/McpRuntimeOptionsConverterFactory.cs +++ b/src/Config/Converters/McpRuntimeOptionsConverterFactory.cs @@ -14,7 +14,7 @@ internal class McpRuntimeOptionsConverterFactory : JsonConverterFactory { // Determines whether to replace environment variable with its // value or not while deserializing. - private bool _replaceEnvVar; + private DeserializationVariableReplacementSettings? _replacementSettings; /// public override bool CanConvert(Type typeToConvert) @@ -25,25 +25,25 @@ public override bool CanConvert(Type typeToConvert) /// public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) { - return new McpRuntimeOptionsConverter(_replaceEnvVar); + return new McpRuntimeOptionsConverter(_replacementSettings); } - internal McpRuntimeOptionsConverterFactory(bool replaceEnvVar) + internal McpRuntimeOptionsConverterFactory(DeserializationVariableReplacementSettings? replacementSettings) { - _replaceEnvVar = replaceEnvVar; + _replacementSettings = replacementSettings; } private class McpRuntimeOptionsConverter : JsonConverter { // Determines whether to replace environment variable with its // value or not while deserializing. - private bool _replaceEnvVar; + private readonly DeserializationVariableReplacementSettings? _replacementSettings; /// Whether to replace environment variable with its /// value or not while deserializing. - internal McpRuntimeOptionsConverter(bool replaceEnvVar) + internal McpRuntimeOptionsConverter(DeserializationVariableReplacementSettings? replacementSettings) { - _replaceEnvVar = replaceEnvVar; + _replacementSettings = replacementSettings; } /// @@ -89,7 +89,7 @@ internal McpRuntimeOptionsConverter(bool replaceEnvVar) case "path": if (reader.TokenType is not JsonTokenType.Null) { - path = reader.DeserializeString(_replaceEnvVar); + path = reader.DeserializeString(_replacementSettings); } break; diff --git a/src/Config/Converters/RuntimeHealthOptionsConvertorFactory.cs b/src/Config/Converters/RuntimeHealthOptionsConvertorFactory.cs index d49cc264e7..9c5f46dce2 100644 --- a/src/Config/Converters/RuntimeHealthOptionsConvertorFactory.cs +++ b/src/Config/Converters/RuntimeHealthOptionsConvertorFactory.cs @@ -11,7 +11,7 @@ internal class RuntimeHealthOptionsConvertorFactory : JsonConverterFactory { // Determines whether to replace environment variable with its // value or not while deserializing. - private bool _replaceEnvVar; + private readonly DeserializationVariableReplacementSettings? _replacementSettings; /// public override bool CanConvert(Type typeToConvert) @@ -22,25 +22,25 @@ public override bool CanConvert(Type typeToConvert) /// public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) { - return new HealthCheckOptionsConverter(_replaceEnvVar); + return new HealthCheckOptionsConverter(_replacementSettings); } - internal RuntimeHealthOptionsConvertorFactory(bool replaceEnvVar) + internal RuntimeHealthOptionsConvertorFactory(DeserializationVariableReplacementSettings? replacementSettings) { - _replaceEnvVar = replaceEnvVar; + _replacementSettings = replacementSettings; } private class HealthCheckOptionsConverter : JsonConverter { // Determines whether to replace environment variable with its // value or not while deserializing. - private bool _replaceEnvVar; + private readonly DeserializationVariableReplacementSettings? _replacementSettings; /// Whether to replace environment variable with its /// value or not while deserializing. - internal HealthCheckOptionsConverter(bool replaceEnvVar) + internal HealthCheckOptionsConverter(DeserializationVariableReplacementSettings? replacementSettings) { - _replaceEnvVar = replaceEnvVar; + _replacementSettings = replacementSettings; } /// @@ -102,7 +102,7 @@ internal HealthCheckOptionsConverter(bool replaceEnvVar) { if (reader.TokenType == JsonTokenType.String) { - string? currentRole = reader.DeserializeString(_replaceEnvVar); + string? currentRole = reader.DeserializeString(_replacementSettings); if (!string.IsNullOrEmpty(currentRole)) { stringList.Add(currentRole); diff --git a/src/Config/Converters/StringJsonConverterFactory.cs b/src/Config/Converters/StringJsonConverterFactory.cs index 078b611789..c3f5333237 100644 --- a/src/Config/Converters/StringJsonConverterFactory.cs +++ b/src/Config/Converters/StringJsonConverterFactory.cs @@ -4,21 +4,20 @@ using System.Text.Json; using System.Text.Json.Serialization; using System.Text.RegularExpressions; -using Azure.DataApiBuilder.Service.Exceptions; namespace Azure.DataApiBuilder.Config.Converters; /// -/// Custom string json converter factory to replace environment variables of the pattern -/// @env('ENV_NAME') with their value during deserialization. +/// Custom string json converter factory to replace environment variables and other variable patterns +/// during deserialization using the DeserializationVariableReplacementSettings. /// public class StringJsonConverterFactory : JsonConverterFactory { - private EnvironmentVariableReplacementFailureMode _replacementFailureMode; + private readonly DeserializationVariableReplacementSettings _replacementSettings; - public StringJsonConverterFactory(EnvironmentVariableReplacementFailureMode replacementFailureMode) + public StringJsonConverterFactory(DeserializationVariableReplacementSettings replacementSettings) { - _replacementFailureMode = replacementFailureMode; + _replacementSettings = replacementSettings; } public override bool CanConvert(Type typeToConvert) @@ -28,32 +27,16 @@ public override bool CanConvert(Type typeToConvert) public override JsonConverter? CreateConverter(Type typeToConvert, JsonSerializerOptions options) { - return new StringJsonConverter(_replacementFailureMode); + return new StringJsonConverter(_replacementSettings); } class StringJsonConverter : JsonConverter { - // @env\(' : match @env(' - // .*? : lazy match any character except newline 0 or more times - // (?='\)) : look ahead for ') which will combine with our lazy match - // ie: in @env('hello')goodbye') we match @env('hello') - // '\) : consume the ') into the match (look ahead doesn't capture) - // This pattern lazy matches any string that starts with @env(' and ends with ') - // ie: fooBAR@env('hello-world')bash)FOO') match: @env('hello-world') - // This matching pattern allows for the @env('') to be safely nested - // within strings that contain ') after our match. - // ie: if the environment variable "Baz" has the value of "Bar" - // fooBarBaz: "('foo@env('Baz')Baz')" would parse into - // fooBarBaz: "('fooBarBaz')" - // Note that there is no escape character currently for ') to exist - // within the name of the environment variable, but that ') is not - // a valid environment variable name in certain shells. - const string ENV_PATTERN = @"@env\('.*?(?='\))'\)"; - private EnvironmentVariableReplacementFailureMode _replacementFailureMode; + private DeserializationVariableReplacementSettings _replacementSettings; - public StringJsonConverter(EnvironmentVariableReplacementFailureMode replacementFailureMode) + public StringJsonConverter(DeserializationVariableReplacementSettings replacementSettings) { - _replacementFailureMode = replacementFailureMode; + _replacementSettings = replacementSettings; } public override string? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) @@ -61,7 +44,18 @@ public StringJsonConverter(EnvironmentVariableReplacementFailureMode replacement if (reader.TokenType == JsonTokenType.String) { string? value = reader.GetString(); - return Regex.Replace(value!, ENV_PATTERN, new MatchEvaluator(ReplaceMatchWithEnvVariable)); + if (string.IsNullOrWhiteSpace(value)) + { + return value; + } + + // Apply all replacement strategies configured in the settings + foreach (KeyValuePair> strategy in _replacementSettings.ReplacementStrategies) + { + value = strategy.Key.Replace(value, new MatchEvaluator(strategy.Value)); + } + + return value; } if (reader.TokenType == JsonTokenType.Null) @@ -76,30 +70,5 @@ public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOp { writer.WriteStringValue(value); } - - private string ReplaceMatchWithEnvVariable(Match match) - { - // [^@env\(] : any substring that is not @env( - // .* : any char except newline any number of times - // (?=\)) : look ahead for end char of ) - // This pattern greedy matches all characters that are not a part of @env() - // ie: @env('hello@env('goodbye')world') match: 'hello@env('goodbye')world' - string innerPattern = @"[^@env\(].*(?=\))"; - - // strips first and last characters, ie: '''hello'' --> ''hello' - string envName = Regex.Match(match.Value, innerPattern).Value[1..^1]; - string? envValue = Environment.GetEnvironmentVariable(envName); - if (_replacementFailureMode == EnvironmentVariableReplacementFailureMode.Throw) - { - return envValue is not null ? envValue : - throw new DataApiBuilderException(message: $"Environmental Variable, {envName}, not found.", - statusCode: System.Net.HttpStatusCode.ServiceUnavailable, - subStatusCode: DataApiBuilderException.SubStatusCodes.ErrorInInitialization); - } - else - { - return envValue ?? match.Value; - } - } } } diff --git a/src/Config/Converters/Utf8JsonReaderExtensions.cs b/src/Config/Converters/Utf8JsonReaderExtensions.cs index 20c6821d02..5e16357227 100644 --- a/src/Config/Converters/Utf8JsonReaderExtensions.cs +++ b/src/Config/Converters/Utf8JsonReaderExtensions.cs @@ -13,14 +13,12 @@ static internal class Utf8JsonReaderExtensions /// substitution is applied. /// /// The reader that we want to pull the string from. - /// Whether to replace environment variable with its - /// value or not while deserializing. + /// The replacement settings to use while deserializing. /// The failure mode to use when replacing environment variables. /// The result of deserialization. /// Thrown if the is not String. public static string? DeserializeString(this Utf8JsonReader reader, - bool replaceEnvVar, - EnvironmentVariableReplacementFailureMode replacementFailureMode = EnvironmentVariableReplacementFailureMode.Throw) + DeserializationVariableReplacementSettings? replacementSettings) { if (reader.TokenType is JsonTokenType.Null) { @@ -34,9 +32,9 @@ static internal class Utf8JsonReaderExtensions // Add the StringConverterFactory so that we can do environment variable substitution. JsonSerializerOptions options = new(); - if (replaceEnvVar) + if (replacementSettings is not null) { - options.Converters.Add(new StringJsonConverterFactory(replacementFailureMode)); + options.Converters.Add(new StringJsonConverterFactory(replacementSettings)); } return JsonSerializer.Deserialize(ref reader, options); diff --git a/src/Config/DeserializationVariableReplacementSettings.cs b/src/Config/DeserializationVariableReplacementSettings.cs new file mode 100644 index 0000000000..d4c02b5252 --- /dev/null +++ b/src/Config/DeserializationVariableReplacementSettings.cs @@ -0,0 +1,213 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.RegularExpressions; +using Azure.Core; +using Azure.DataApiBuilder.Config.Converters; +using Azure.DataApiBuilder.Config.ObjectModel; +using Azure.DataApiBuilder.Service.Exceptions; +using Azure.Identity; +using Azure.Security.KeyVault.Secrets; + +namespace Azure.DataApiBuilder.Config +{ + public class DeserializationVariableReplacementSettings + { + public bool DoReplaceEnvVar { get; set; } + public bool DoReplaceAkvVar { get; set; } + public EnvironmentVariableReplacementFailureMode EnvFailureMode { get; set; } = EnvironmentVariableReplacementFailureMode.Throw; + + // @env\(' : match @env(' + // @akv\(' : match @akv(' + // .*? : lazy match any character except newline 0 or more times + // (?='\)) : look ahead for ')' which will combine with our lazy match + // ie: in @env('hello')goodbye') we match @env('hello') + // '\) : consume the ') into the match (look ahead doesn't capture) + // This pattern lazy matches any string that starts with @env(' and ends with ') OR @akv(' and ends with ') + // Example: fooBAR@env('hello-world')bash)FOO') match: @env('hello-world') + // Example: fooBAR@akv('secret-name')bash)FOO') match: @akv('secret-name') + // This matching pattern allows for the @env('') / @akv('') to be safely nested + // within strings that contain ')' after our match. + // Note that there is no escape character currently for ')' to exist within the name of the variable. + public const string OUTER_ENV_PATTERN = @"@env\('.*?(?='\))'\)"; + public const string OUTER_AKV_PATTERN = @"@akv\('.*?(?='\))'\)"; + + // [^@env\(] : any substring that is not @env( + // [^@akv\(] : any substring that is not @akv( + // .* : any char except newline any number of times + // (?=\)) : look ahead for end char of ) + // This pattern greedy matches all characters that are not a part of @env() / @akv() + // ie: @env('hello@env('goodbye')world') match: 'hello@env('goodbye')world' + public const string INNER_ENV_PATTERN = @"[^@env\(].*(?=\))"; + public const string INNER_AKV_PATTERN = @"[^@akv\(].*(?=\))"; + + private readonly AzureKeyVaultOptions? _azureKeyVaultOptions; + private readonly SecretClient? _akvClient; + + public Dictionary> ReplacementStrategies { get; private set; } = new(); + + public DeserializationVariableReplacementSettings( + AzureKeyVaultOptions? azureKeyVaultOptions = null, + bool doReplaceEnvVar = false, + bool doReplaceAkvVar = false, + EnvironmentVariableReplacementFailureMode envFailureMode = EnvironmentVariableReplacementFailureMode.Throw) + { + _azureKeyVaultOptions = azureKeyVaultOptions; + DoReplaceEnvVar = doReplaceEnvVar; + DoReplaceAkvVar = doReplaceAkvVar; + EnvFailureMode = envFailureMode; + + if (DoReplaceEnvVar) + { + ReplacementStrategies.Add( + new Regex(OUTER_ENV_PATTERN, RegexOptions.Compiled), + ReplaceEnvVariable); + } + + if (DoReplaceAkvVar && _azureKeyVaultOptions is not null) + { + _akvClient = CreateSecretClient(_azureKeyVaultOptions); + ReplacementStrategies.Add( + new Regex(OUTER_AKV_PATTERN, RegexOptions.Compiled), + ReplaceAkvVariable); + } + } + + private string ReplaceEnvVariable(Match match) + { + // strips first and last characters, ie: '''hello'' --> ''hello' + string name = Regex.Match(match.Value, INNER_ENV_PATTERN).Value[1..^1]; + string? value = Environment.GetEnvironmentVariable(name); + if (EnvFailureMode is EnvironmentVariableReplacementFailureMode.Throw) + { + return value is not null ? value : + throw new DataApiBuilderException( + message: $"Environmental Variable, {name}, not found.", + statusCode: System.Net.HttpStatusCode.ServiceUnavailable, + subStatusCode: DataApiBuilderException.SubStatusCodes.ErrorInInitialization); + } + else + { + return value ?? match.Value; + } + } + + private string ReplaceAkvVariable(Match match) + { + // strips first and last characters, ie: '''hello'' --> ''hello' + string name = Regex.Match(match.Value, INNER_AKV_PATTERN).Value[1..^1]; + + // Validate AKV secret name per rules: + // Allowed: alphanumeric and hyphen (-) + // Disallowed: spaces or any other symbols + // Must start and end with alphanumeric + // Length: 1 to 127 chars + if (!IsValidAkvSecretName(name, out string validationError)) + { + throw new DataApiBuilderException( + message: $"Azure Key Vault secret name '{name}' is invalid. {validationError} Requirements: allowed characters are alphanumeric and hyphen (-); must start and end with an alphanumeric character; length 1-127 characters; case-insensitive.", + statusCode: System.Net.HttpStatusCode.ServiceUnavailable, + subStatusCode: DataApiBuilderException.SubStatusCodes.ErrorInInitialization); + } + + string? value = GetAkvVariable(name); + if (EnvFailureMode == EnvironmentVariableReplacementFailureMode.Throw) + { + return value is not null ? value : + throw new DataApiBuilderException(message: $"Azure Key Vault Variable, '{name}', not found.", + statusCode: System.Net.HttpStatusCode.ServiceUnavailable, + subStatusCode: DataApiBuilderException.SubStatusCodes.ErrorInInitialization); + } + else + { + return value ?? match.Value; + } + } + + private static bool IsValidAkvSecretName(string name, out string error) + { + error = string.Empty; + if (string.IsNullOrEmpty(name)) + { + error = "Name cannot be null or empty."; + return false; + } + + if (name.Length < 1 || name.Length > 127) + { + error = $"Length {name.Length} is outside allowed range (1-127)."; + return false; + } + + // Must start and end with alphanumeric + if (!char.IsLetterOrDigit(name[0]) || !char.IsLetterOrDigit(name[^1])) + { + error = "Must start and end with an alphanumeric character."; + return false; + } + + // Allowed characters: letters, digits, hyphen. + for (int i = 0; i < name.Length; i++) + { + char c = name[i]; + if (!(char.IsLetterOrDigit(c) || c == '-')) + { + error = $"Invalid character '{c}' at position {i}."; + return false; + } + } + + return true; + } + + private static SecretClient CreateSecretClient(AzureKeyVaultOptions options) + { + if (string.IsNullOrWhiteSpace(options.Endpoint)) + { + throw new DataApiBuilderException( + "Missing 'endpoint' property is required to connect to Azure Key Vault.", + System.Net.HttpStatusCode.InternalServerError, + DataApiBuilderException.SubStatusCodes.ErrorInInitialization); + } + + SecretClientOptions clientOptions = new(); + + if (options.RetryPolicy is not null) + { + // Convert AKVRetryPolicyMode to RetryMode + RetryMode retryMode = options.RetryPolicy.Mode switch + { + AKVRetryPolicyMode.Fixed => RetryMode.Fixed, + AKVRetryPolicyMode.Exponential => RetryMode.Exponential, + null => RetryMode.Exponential, + _ => RetryMode.Exponential + }; + + clientOptions.Retry.Mode = retryMode; + clientOptions.Retry.MaxRetries = options.RetryPolicy.MaxCount ?? AKVRetryPolicyOptions.DEFAULT_MAX_COUNT; + clientOptions.Retry.Delay = TimeSpan.FromSeconds(options.RetryPolicy.DelaySeconds ?? AKVRetryPolicyOptions.DEFAULT_DELAY_SECONDS); + clientOptions.Retry.MaxDelay = TimeSpan.FromSeconds(options.RetryPolicy.MaxDelaySeconds ?? AKVRetryPolicyOptions.DEFAULT_MAX_DELAY_SECONDS); + clientOptions.Retry.NetworkTimeout = TimeSpan.FromSeconds(options.RetryPolicy.NetworkTimeoutSeconds ?? AKVRetryPolicyOptions.DEFAULT_NETWORK_TIMEOUT_SECONDS); + } + + return new SecretClient(new Uri(options.Endpoint), new DefaultAzureCredential(), clientOptions); + } + + private string? GetAkvVariable(string name) + { + if (_akvClient is null) + { + throw new InvalidOperationException("Azure Key Vault client is not initialized."); + } + + try + { + return _akvClient.GetSecret(name).Value.Value; + } + catch (Azure.RequestFailedException ex) when (ex.Status == 404) + { + return null; + } + } + } +} diff --git a/src/Config/FileSystemRuntimeConfigLoader.cs b/src/Config/FileSystemRuntimeConfigLoader.cs index 9c2a8e50b5..614cfbd11c 100644 --- a/src/Config/FileSystemRuntimeConfigLoader.cs +++ b/src/Config/FileSystemRuntimeConfigLoader.cs @@ -182,17 +182,16 @@ private void OnNewFileContentsDetected(object? sender, EventArgs e) /// /// The path to the dab-config.json file. /// The loaded RuntimeConfig, or null if none was loaded. - /// Whether to replace environment variable with its - /// value or not while deserializing. /// ILogger for logging errors. /// When not null indicates we need to overwrite mode and how to do so. + /// Settings for variable replacement during deserialization. If null, uses default settings with environment variable replacement disabled. /// True if the config was loaded, otherwise false. public bool TryLoadConfig( string path, [NotNullWhen(true)] out RuntimeConfig? config, - bool replaceEnvVar = false, ILogger? logger = null, - bool? isDevMode = null) + bool? isDevMode = null, + DeserializationVariableReplacementSettings? replacementSettings = null) { if (_fileSystem.File.Exists(path)) { @@ -226,7 +225,15 @@ public bool TryLoadConfig( } } - if (!string.IsNullOrEmpty(json) && TryParseConfig(json, out RuntimeConfig, connectionString: _connectionString, replaceEnvVar: replaceEnvVar)) + // Use default replacement settings if none provided + replacementSettings ??= new DeserializationVariableReplacementSettings(); + + if (!string.IsNullOrEmpty(json) && TryParseConfig( + json, + out RuntimeConfig, + replacementSettings, + logger: null, + connectionString: _connectionString)) { if (TrySetupConfigFileWatcher()) { @@ -292,12 +299,13 @@ public bool TryLoadConfig( /// Tries to load the config file using the filename known to the RuntimeConfigLoader and for the default environment. /// /// The loaded RuntimeConfig, or null if none was loaded. - /// Whether to replace environment variable with its - /// value or not while deserializing. + /// Settings for variable replacement during deserialization. If null, uses default settings with environment variable replacement disabled. /// True if the config was loaded, otherwise false. public override bool TryLoadKnownConfig([NotNullWhen(true)] out RuntimeConfig? config, bool replaceEnvVar = false) { - return TryLoadConfig(ConfigFilePath, out config, replaceEnvVar); + // Convert legacy replaceEnvVar parameter to replacement settings for backward compatibility + DeserializationVariableReplacementSettings? replacementSettings = new(azureKeyVaultOptions: null, doReplaceEnvVar: replaceEnvVar, doReplaceAkvVar: replaceEnvVar); + return TryLoadConfig(ConfigFilePath, out config, replacementSettings: replacementSettings); } /// @@ -307,7 +315,11 @@ public override bool TryLoadKnownConfig([NotNullWhen(true)] out RuntimeConfig? c private void HotReloadConfig(bool isDevMode, ILogger? logger = null) { logger?.LogInformation(message: "Starting hot-reload process for config: {ConfigFilePath}", ConfigFilePath); - if (!TryLoadConfig(ConfigFilePath, out _, replaceEnvVar: true, isDevMode: isDevMode)) + + // Use default replacement settings for hot reload + DeserializationVariableReplacementSettings replacementSettings = new(azureKeyVaultOptions: null, doReplaceEnvVar: true, doReplaceAkvVar: true); + + if (!TryLoadConfig(ConfigFilePath, out _, logger: logger, isDevMode: isDevMode, replacementSettings: replacementSettings)) { throw new DataApiBuilderException( message: "Deserialization of the configuration file failed.", @@ -467,7 +479,7 @@ public override string GetPublishedDraftSchemaLink() string? schemaPath = _fileSystem.Path.Combine(assemblyDirectory, "dab.draft.schema.json"); string schemaFileContent = _fileSystem.File.ReadAllText(schemaPath); - Dictionary? jsonDictionary = JsonSerializer.Deserialize>(schemaFileContent, GetSerializationOptions()); + Dictionary? jsonDictionary = JsonSerializer.Deserialize>(schemaFileContent, GetSerializationOptions(replacementSettings: null)); if (jsonDictionary is null) { diff --git a/src/Config/ObjectModel/AzureKeyVaultOptions.cs b/src/Config/ObjectModel/AzureKeyVaultOptions.cs index 27094cd16f..ebd1e909c1 100644 --- a/src/Config/ObjectModel/AzureKeyVaultOptions.cs +++ b/src/Config/ObjectModel/AzureKeyVaultOptions.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization; namespace Azure.DataApiBuilder.Config.ObjectModel; @@ -12,4 +13,40 @@ public record AzureKeyVaultOptions [JsonPropertyName("retry-policy")] public AKVRetryPolicyOptions? RetryPolicy { get; init; } + + /// + /// Flag which informs CLI and JSON serializer whether to write endpoint + /// property and value to the runtime config file. + /// When user doesn't provide the endpoint property/value, which signals DAB to use the default, + /// the DAB CLI should not write the default value to a serialized config. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.Always)] + [MemberNotNullWhen(true, nameof(Endpoint))] + public bool UserProvidedEndpoint { get; init; } = false; + + /// + /// Flag which informs CLI and JSON serializer whether to write retry-policy + /// property and value to the runtime config file. + /// When user doesn't provide the retry-policy property/value, which signals DAB to use the default, + /// the DAB CLI should not write the default value to a serialized config. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.Always)] + [MemberNotNullWhen(true, nameof(RetryPolicy))] + public bool UserProvidedRetryPolicy { get; init; } = false; + + [JsonConstructor] + public AzureKeyVaultOptions(string? endpoint = null, AKVRetryPolicyOptions? retryPolicy = null) + { + if (endpoint is not null) + { + Endpoint = endpoint; + UserProvidedEndpoint = true; + } + + if (retryPolicy is not null) + { + RetryPolicy = retryPolicy; + UserProvidedRetryPolicy = true; + } + } } diff --git a/src/Config/ObjectModel/RuntimeConfig.cs b/src/Config/ObjectModel/RuntimeConfig.cs index a450e1265c..6896d82161 100644 --- a/src/Config/ObjectModel/RuntimeConfig.cs +++ b/src/Config/ObjectModel/RuntimeConfig.cs @@ -298,7 +298,10 @@ public RuntimeConfig( foreach (string dataSourceFile in DataSourceFiles.SourceFiles) { - if (loader.TryLoadConfig(dataSourceFile, out RuntimeConfig? config, replaceEnvVar: true)) + // Use default replacement settings for environment variable replacement + DeserializationVariableReplacementSettings replacementSettings = new(azureKeyVaultOptions: null, doReplaceEnvVar: true, doReplaceAkvVar: true); + + if (loader.TryLoadConfig(dataSourceFile, out RuntimeConfig? config, replacementSettings: replacementSettings)) { try { @@ -448,7 +451,7 @@ public bool CheckDataSourceExists(string dataSourceName) public string ToJson(JsonSerializerOptions? jsonSerializerOptions = null) { // get default serializer options if none provided. - jsonSerializerOptions = jsonSerializerOptions ?? RuntimeConfigLoader.GetSerializationOptions(); + jsonSerializerOptions = jsonSerializerOptions ?? RuntimeConfigLoader.GetSerializationOptions(replacementSettings: null); return JsonSerializer.Serialize(this, jsonSerializerOptions); } diff --git a/src/Config/RuntimeConfigLoader.cs b/src/Config/RuntimeConfigLoader.cs index f78c32ebc1..bad5aa8680 100644 --- a/src/Config/RuntimeConfigLoader.cs +++ b/src/Config/RuntimeConfigLoader.cs @@ -129,25 +129,86 @@ protected void SignalConfigChanged(string message = "") /// public abstract string GetPublishedDraftSchemaLink(); + /// + /// Extracts AzureKeyVaultOptions from JSON string with configurable variable replacement. + /// + /// JSON that represents the config file. + /// Whether to enable environment variable replacement during extraction. + /// Failure mode for environment variable replacement if enabled. + /// AzureKeyVaultOptions if present, null otherwise. + private static AzureKeyVaultOptions? ExtractAzureKeyVaultOptions( + string json, + bool enableEnvReplacement, + EnvironmentVariableReplacementFailureMode replacementFailureMode = EnvironmentVariableReplacementFailureMode.Throw) + { + JsonSerializerOptions options = new() + { + PropertyNameCaseInsensitive = false, + PropertyNamingPolicy = new HyphenatedNamingPolicy(), + ReadCommentHandling = JsonCommentHandling.Skip + }; + DeserializationVariableReplacementSettings envOnlySettings = new( + azureKeyVaultOptions: null, + doReplaceEnvVar: enableEnvReplacement, + doReplaceAkvVar: false, + envFailureMode: replacementFailureMode); + options.Converters.Add(new StringJsonConverterFactory(envOnlySettings)); + options.Converters.Add(new EnumMemberJsonEnumConverterFactory()); + options.Converters.Add(new AzureKeyVaultOptionsConverterFactory(replacementSettings: envOnlySettings)); + options.Converters.Add(new AKVRetryPolicyOptionsConverterFactory(replacementSettings: envOnlySettings)); + + try + { + using JsonDocument doc = JsonDocument.Parse(json); + if (doc.RootElement.TryGetProperty("azure-key-vault", out JsonElement akvElement)) + { + return JsonSerializer.Deserialize(akvElement.GetRawText(), options); + } + } + catch + { + // If we can't extract AKV options, return null and proceed without AKV variable replacement + return null; + } + + return null; + } + /// /// Parses a JSON string into a RuntimeConfig object for single database scenario. /// /// JSON that represents the config file. /// The parsed config, or null if it parsed unsuccessfully. - /// True if the config was parsed, otherwise false. + /// Settings for variable replacement during deserialization. If null, no variable replacement will be performed. /// logger to log messages /// connectionString to add to config if specified - /// Whether to replace environment variable with its - /// value or not while deserializing. By default, no replacement happens. - /// Determines failure mode for env variable replacement. + /// True if the config was parsed, otherwise false. public static bool TryParseConfig(string json, [NotNullWhen(true)] out RuntimeConfig? config, + DeserializationVariableReplacementSettings? replacementSettings = null, ILogger? logger = null, - string? connectionString = null, - bool replaceEnvVar = false, - EnvironmentVariableReplacementFailureMode replacementFailureMode = EnvironmentVariableReplacementFailureMode.Throw) + string? connectionString = null) { - JsonSerializerOptions options = GetSerializationOptions(replaceEnvVar, replacementFailureMode); + // First pass: extract AzureKeyVault options if AKV replacement is requested + if (replacementSettings?.DoReplaceAkvVar is true) + { + AzureKeyVaultOptions? azureKeyVaultOptions = ExtractAzureKeyVaultOptions( + json: json, + enableEnvReplacement: replacementSettings.DoReplaceEnvVar, + replacementFailureMode: replacementSettings.EnvFailureMode); + + // Update replacement settings with the extracted AKV options + if (azureKeyVaultOptions is not null) + { + replacementSettings = new DeserializationVariableReplacementSettings( + azureKeyVaultOptions: azureKeyVaultOptions, + doReplaceEnvVar: replacementSettings.DoReplaceEnvVar, + doReplaceAkvVar: replacementSettings.DoReplaceAkvVar, + envFailureMode: replacementSettings.EnvFailureMode); + } + } + + JsonSerializerOptions options = GetSerializationOptions(replacementSettings); try { @@ -180,11 +241,11 @@ public static bool TryParseConfig(string json, DataSource ds = config.GetDataSourceFromDataSourceName(dataSourceKey); // Add Application Name for telemetry for MsSQL or PgSql - if (ds.DatabaseType is DatabaseType.MSSQL && replaceEnvVar) + if (ds.DatabaseType is DatabaseType.MSSQL && replacementSettings?.DoReplaceEnvVar == true) { updatedConnection = GetConnectionStringWithApplicationName(connectionValue); } - else if (ds.DatabaseType is DatabaseType.PostgreSQL && replaceEnvVar) + else if (ds.DatabaseType is DatabaseType.PostgreSQL && replacementSettings?.DoReplaceEnvVar == true) { updatedConnection = GetPgSqlConnectionStringWithApplicationName(connectionValue); } @@ -225,11 +286,10 @@ ex is JsonException || /// /// Get Serializer options for the config file. /// - /// Whether to replace environment variable with value or not while deserializing. - /// By default, no replacement happens. + /// Settings for variable replacement during deserialization. + /// If null, no variable replacement will be performed. public static JsonSerializerOptions GetSerializationOptions( - bool replaceEnvVar = false, - EnvironmentVariableReplacementFailureMode replacementFailureMode = EnvironmentVariableReplacementFailureMode.Throw) + DeserializationVariableReplacementSettings? replacementSettings = null) { JsonSerializerOptions options = new() { @@ -241,33 +301,37 @@ public static JsonSerializerOptions GetSerializationOptions( Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping }; options.Converters.Add(new EnumMemberJsonEnumConverterFactory()); - options.Converters.Add(new RuntimeHealthOptionsConvertorFactory(replaceEnvVar)); - options.Converters.Add(new DataSourceHealthOptionsConvertorFactory(replaceEnvVar)); + options.Converters.Add(new RuntimeHealthOptionsConvertorFactory(replacementSettings)); + options.Converters.Add(new DataSourceHealthOptionsConvertorFactory(replacementSettings)); options.Converters.Add(new EntityHealthOptionsConvertorFactory()); options.Converters.Add(new RestRuntimeOptionsConverterFactory()); - options.Converters.Add(new GraphQLRuntimeOptionsConverterFactory(replaceEnvVar)); - options.Converters.Add(new McpRuntimeOptionsConverterFactory(replaceEnvVar)); + options.Converters.Add(new GraphQLRuntimeOptionsConverterFactory(replacementSettings)); + options.Converters.Add(new McpRuntimeOptionsConverterFactory(replacementSettings)); options.Converters.Add(new DmlToolsConfigConverter()); - options.Converters.Add(new EntitySourceConverterFactory(replaceEnvVar)); - options.Converters.Add(new EntityGraphQLOptionsConverterFactory(replaceEnvVar)); - options.Converters.Add(new EntityRestOptionsConverterFactory(replaceEnvVar)); + options.Converters.Add(new EntitySourceConverterFactory(replacementSettings)); + options.Converters.Add(new EntityGraphQLOptionsConverterFactory(replacementSettings)); + options.Converters.Add(new EntityRestOptionsConverterFactory(replacementSettings)); options.Converters.Add(new EntityActionConverterFactory()); options.Converters.Add(new DataSourceFilesConverter()); - options.Converters.Add(new EntityCacheOptionsConverterFactory(replaceEnvVar)); + options.Converters.Add(new EntityCacheOptionsConverterFactory(replacementSettings)); options.Converters.Add(new RuntimeCacheOptionsConverterFactory()); options.Converters.Add(new RuntimeCacheLevel2OptionsConverterFactory()); options.Converters.Add(new MultipleCreateOptionsConverter()); options.Converters.Add(new MultipleMutationOptionsConverter(options)); - options.Converters.Add(new DataSourceConverterFactory(replaceEnvVar)); + options.Converters.Add(new DataSourceConverterFactory(replacementSettings)); options.Converters.Add(new HostOptionsConvertorFactory()); - options.Converters.Add(new AKVRetryPolicyOptionsConverterFactory(replaceEnvVar)); - options.Converters.Add(new AzureLogAnalyticsOptionsConverterFactory(replaceEnvVar)); - options.Converters.Add(new AzureLogAnalyticsAuthOptionsConverter(replaceEnvVar)); - options.Converters.Add(new FileSinkConverter(replaceEnvVar)); + options.Converters.Add(new AKVRetryPolicyOptionsConverterFactory(replacementSettings)); + options.Converters.Add(new AzureLogAnalyticsOptionsConverterFactory(replacementSettings)); + options.Converters.Add(new AzureLogAnalyticsAuthOptionsConverter(replacementSettings)); + options.Converters.Add(new FileSinkConverter(replacementSettings)); + + // Add AzureKeyVaultOptionsConverterFactory to ensure AKV config is deserialized properly + options.Converters.Add(new AzureKeyVaultOptionsConverterFactory(replacementSettings)); - if (replaceEnvVar) + // Only add the extensible string converter if we have replacement settings + if (replacementSettings is not null) { - options.Converters.Add(new StringJsonConverterFactory(replacementFailureMode)); + options.Converters.Add(new StringJsonConverterFactory(replacementSettings)); } return options; diff --git a/src/Core/Configurations/RuntimeConfigProvider.cs b/src/Core/Configurations/RuntimeConfigProvider.cs index faeb2b94d0..b46a716f48 100644 --- a/src/Core/Configurations/RuntimeConfigProvider.cs +++ b/src/Core/Configurations/RuntimeConfigProvider.cs @@ -6,7 +6,6 @@ using System.IO.Abstractions; using System.Net; using Azure.DataApiBuilder.Config; -using Azure.DataApiBuilder.Config.Converters; using Azure.DataApiBuilder.Config.NamingPolicies; using Azure.DataApiBuilder.Config.ObjectModel; using Azure.DataApiBuilder.Service.Exceptions; @@ -189,8 +188,7 @@ public async Task Initialize( if (RuntimeConfigLoader.TryParseConfig( configuration, out RuntimeConfig? runtimeConfig, - replaceEnvVar: false, - replacementFailureMode: EnvironmentVariableReplacementFailureMode.Ignore)) + replacementSettings: null)) { _configLoader.RuntimeConfig = runtimeConfig; @@ -257,8 +255,7 @@ public async Task Initialize( string? graphQLSchema, string connectionString, string? accessToken, - bool replaceEnvVar = true, - EnvironmentVariableReplacementFailureMode replacementFailureMode = EnvironmentVariableReplacementFailureMode.Throw) + DeserializationVariableReplacementSettings? replacementSettings) { if (string.IsNullOrEmpty(connectionString)) { @@ -272,7 +269,7 @@ public async Task Initialize( IsLateConfigured = true; - if (RuntimeConfigLoader.TryParseConfig(jsonConfig, out RuntimeConfig? runtimeConfig, replaceEnvVar: replaceEnvVar, replacementFailureMode: replacementFailureMode)) + if (RuntimeConfigLoader.TryParseConfig(jsonConfig, out RuntimeConfig? runtimeConfig, replacementSettings)) { _configLoader.RuntimeConfig = runtimeConfig.DataSource.DatabaseType switch { diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 658e489bac..ccd69b9600 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -9,6 +9,7 @@ + diff --git a/src/Service.Tests/Caching/CachingConfigProcessingTests.cs b/src/Service.Tests/Caching/CachingConfigProcessingTests.cs index 2780af63c5..a6daebf3e4 100644 --- a/src/Service.Tests/Caching/CachingConfigProcessingTests.cs +++ b/src/Service.Tests/Caching/CachingConfigProcessingTests.cs @@ -5,7 +5,6 @@ using System.Text; using System.Text.Json; using Azure.DataApiBuilder.Config; -using Azure.DataApiBuilder.Config.Converters; using Azure.DataApiBuilder.Config.ObjectModel; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -56,10 +55,7 @@ public void EntityCacheOptionsDeserialization_ValidJson( RuntimeConfigLoader.TryParseConfig( json: fullConfig, out RuntimeConfig? config, - logger: null, - connectionString: null, - replaceEnvVar: false, - replacementFailureMode: EnvironmentVariableReplacementFailureMode.Throw); + replacementSettings: null); // Assert Assert.IsNotNull(config, message: "Config must not be null, runtime config JSON deserialization failed."); @@ -103,10 +99,7 @@ public void EntityCacheOptionsDeserialization_InvalidValues(string entityCacheCo bool isParsingSuccessful = RuntimeConfigLoader.TryParseConfig( json: fullConfig, out _, - logger: null, - connectionString: null, - replaceEnvVar: false, - replacementFailureMode: EnvironmentVariableReplacementFailureMode.Throw); + replacementSettings: null); // Assert Assert.IsFalse(isParsingSuccessful, message: "Expected JSON parsing to fail."); @@ -141,10 +134,7 @@ public void GlobalCacheOptionsDeserialization_ValidValues( RuntimeConfigLoader.TryParseConfig( json: fullConfig, out RuntimeConfig? config, - logger: null, - connectionString: null, - replaceEnvVar: false, - replacementFailureMode: EnvironmentVariableReplacementFailureMode.Throw); + replacementSettings: null); // Assert Assert.IsNotNull(config, message: "Config must not be null, runtime config JSON deserialization failed."); @@ -187,10 +177,7 @@ public void GlobalCacheOptionsDeserialization_InvalidValues(string globalCacheCo bool parsingSuccessful = RuntimeConfigLoader.TryParseConfig( json: fullConfig, out _, - logger: null, - connectionString: null, - replaceEnvVar: false, - replacementFailureMode: EnvironmentVariableReplacementFailureMode.Throw); + replacementSettings: null); // Assert Assert.IsFalse(parsingSuccessful, message: "Expected JSON parsing to fail."); @@ -216,10 +203,7 @@ public void GlobalCacheOptionsOverridesEntityCacheOptions(string globalCacheConf RuntimeConfigLoader.TryParseConfig( json: fullConfig, out RuntimeConfig? config, - logger: null, - connectionString: null, - replaceEnvVar: false, - replacementFailureMode: EnvironmentVariableReplacementFailureMode.Throw); + replacementSettings: null); // Assert Assert.IsNotNull(config, message: "Config must not be null, runtime config JSON deserialization failed."); @@ -252,10 +236,7 @@ public void UserDefinedTtlWrittenToSerializedJsonConfigFile(bool expectIsUserDef RuntimeConfigLoader.TryParseConfig( json: fullConfig, out RuntimeConfig? config, - logger: null, - connectionString: null, - replaceEnvVar: false, - replacementFailureMode: EnvironmentVariableReplacementFailureMode.Throw); + replacementSettings: null); Assert.IsNotNull(config, message: "Test setup failure. Config must not be null, runtime config JSON deserialization failed."); // Act @@ -300,10 +281,7 @@ public void CachePropertyNotWrittenToSerializedJsonConfigFile(string cacheConfig RuntimeConfigLoader.TryParseConfig( json: fullConfig, out RuntimeConfig? config, - logger: null, - connectionString: null, - replaceEnvVar: false, - replacementFailureMode: EnvironmentVariableReplacementFailureMode.Throw); + replacementSettings: null); Assert.IsNotNull(config, message: "Test setup failure. Config must not be null, runtime config JSON deserialization failed."); // Act @@ -342,10 +320,7 @@ public void DefaultTtlNotWrittenToSerializedJsonConfigFile(string cacheConfig) RuntimeConfigLoader.TryParseConfig( json: fullConfig, out RuntimeConfig? config, - logger: null, - connectionString: null, - replaceEnvVar: false, - replacementFailureMode: EnvironmentVariableReplacementFailureMode.Throw); + replacementSettings: null); Assert.IsNotNull(config, message: "Test setup failure. Config must not be null, runtime config JSON deserialization failed."); // Act diff --git a/src/Service.Tests/Configuration/ConfigurationTests.cs b/src/Service.Tests/Configuration/ConfigurationTests.cs index 65f6e6643b..0614e7688f 100644 --- a/src/Service.Tests/Configuration/ConfigurationTests.cs +++ b/src/Service.Tests/Configuration/ConfigurationTests.cs @@ -838,9 +838,9 @@ public void MsSqlConnStringSupplementedWithAppNameProperty( // Act bool configParsed = RuntimeConfigLoader.TryParseConfig( - runtimeConfig.ToJson(), - out RuntimeConfig updatedRuntimeConfig, - replaceEnvVar: true); + json: runtimeConfig.ToJson(), + config: out RuntimeConfig updatedRuntimeConfig, + replacementSettings: new(doReplaceEnvVar: true)); // Assert Assert.AreEqual( @@ -891,9 +891,9 @@ public void PgSqlConnStringSupplementedWithAppNameProperty( // Act bool configParsed = RuntimeConfigLoader.TryParseConfig( - runtimeConfig.ToJson(), - out RuntimeConfig updatedRuntimeConfig, - replaceEnvVar: true); + json: runtimeConfig.ToJson(), + config: out RuntimeConfig updatedRuntimeConfig, + replacementSettings: new(doReplaceEnvVar: true)); // Assert Assert.AreEqual( @@ -956,9 +956,9 @@ public void TestConnectionStringIsCorrectlyUpdatedWithApplicationName( // Act bool configParsed = RuntimeConfigLoader.TryParseConfig( - runtimeConfig.ToJson(), - out RuntimeConfig updatedRuntimeConfig, - replaceEnvVar: true); + json: runtimeConfig.ToJson(), + config: out RuntimeConfig updatedRuntimeConfig, + replacementSettings: new(doReplaceEnvVar: true)); // Assert Assert.AreEqual( @@ -2346,7 +2346,12 @@ public async Task TestSPRestDefaultsForManuallyConstructedConfigs( HttpStatusCode expectedResponseStatusCode) { string configJson = TestHelper.AddPropertiesToJson(TestHelper.BASE_CONFIG, entityJson); - RuntimeConfigLoader.TryParseConfig(configJson, out RuntimeConfig deserializedConfig, logger: null, GetConnectionStringFromEnvironmentConfig(environment: TestCategory.MSSQL)); + RuntimeConfigLoader.TryParseConfig( + configJson, + out RuntimeConfig deserializedConfig, + replacementSettings: new(), + logger: null, + GetConnectionStringFromEnvironmentConfig(environment: TestCategory.MSSQL)); string configFileName = "custom-config.json"; File.WriteAllText(configFileName, deserializedConfig.ToJson()); string[] args = new[] @@ -2429,7 +2434,12 @@ public async Task SanityTestForRestAndGQLRequestsWithoutMultipleMutationFeatureF // The configuration file is constructed by merging hard-coded JSON strings to simulate the scenario where users manually edit the // configuration file (instead of using CLI). string configJson = TestHelper.AddPropertiesToJson(TestHelper.BASE_CONFIG, BOOK_ENTITY_JSON); - Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(configJson, out RuntimeConfig deserializedConfig, logger: null, GetConnectionStringFromEnvironmentConfig(environment: TestCategory.MSSQL))); + Assert.IsTrue(RuntimeConfigLoader.TryParseConfig( + configJson, + out RuntimeConfig deserializedConfig, + replacementSettings: new(), + logger: null, + GetConnectionStringFromEnvironmentConfig(environment: TestCategory.MSSQL))); string configFileName = "custom-config.json"; File.WriteAllText(configFileName, deserializedConfig.ToJson()); string[] args = new[] @@ -3290,7 +3300,12 @@ public async Task ValidateStrictModeAsDefaultForRestRequestBody(bool includeExtr // The BASE_CONFIG omits the rest.request-body-strict option in the runtime section. string configJson = TestHelper.AddPropertiesToJson(TestHelper.BASE_CONFIG, entityJson); - RuntimeConfigLoader.TryParseConfig(configJson, out RuntimeConfig deserializedConfig, logger: null, GetConnectionStringFromEnvironmentConfig(environment: TestCategory.MSSQL)); + RuntimeConfigLoader.TryParseConfig( + configJson, + out RuntimeConfig deserializedConfig, + replacementSettings: new(), + logger: null, + GetConnectionStringFromEnvironmentConfig(environment: TestCategory.MSSQL)); const string CUSTOM_CONFIG = "custom-config.json"; File.WriteAllText(CUSTOM_CONFIG, deserializedConfig.ToJson()); string[] args = new[] @@ -5494,7 +5509,7 @@ public static string GetConnectionStringFromEnvironmentConfig(string environment string sqlFile = new FileSystemRuntimeConfigLoader(fileSystem).GetFileNameForEnvironment(environment, considerOverrides: true); string configPayload = File.ReadAllText(sqlFile); - RuntimeConfigLoader.TryParseConfig(configPayload, out RuntimeConfig runtimeConfig, replaceEnvVar: true); + RuntimeConfigLoader.TryParseConfig(configPayload, out RuntimeConfig runtimeConfig, replacementSettings: new()); return runtimeConfig.DataSource.ConnectionString; } diff --git a/src/Service.Tests/UnitTests/MySqlQueryExecutorUnitTests.cs b/src/Service.Tests/UnitTests/MySqlQueryExecutorUnitTests.cs index cbfef36664..63deed78d3 100644 --- a/src/Service.Tests/UnitTests/MySqlQueryExecutorUnitTests.cs +++ b/src/Service.Tests/UnitTests/MySqlQueryExecutorUnitTests.cs @@ -81,7 +81,8 @@ await provider.Initialize( provider.GetConfig().ToJson(), graphQLSchema: null, connectionString: connectionString, - accessToken: CONFIG_TOKEN); + accessToken: CONFIG_TOKEN, + replacementSettings: new()); mySqlQueryExecutor = new(provider, dbExceptionParser.Object, queryExecutorLogger.Object, httpContextAccessor.Object); } } diff --git a/src/Service.Tests/UnitTests/PostgreSqlQueryExecutorUnitTests.cs b/src/Service.Tests/UnitTests/PostgreSqlQueryExecutorUnitTests.cs index ccaa90b353..6039c46a72 100644 --- a/src/Service.Tests/UnitTests/PostgreSqlQueryExecutorUnitTests.cs +++ b/src/Service.Tests/UnitTests/PostgreSqlQueryExecutorUnitTests.cs @@ -89,7 +89,8 @@ await provider.Initialize( provider.GetConfig().ToJson(), graphQLSchema: null, connectionString: connectionString, - accessToken: CONFIG_TOKEN); + accessToken: CONFIG_TOKEN, + replacementSettings: new()); postgreSqlQueryExecutor = new(provider, dbExceptionParser.Object, queryExecutorLogger.Object, httpContextAccessor.Object); } } diff --git a/src/Service.Tests/UnitTests/RuntimeConfigLoaderJsonDeserializerTests.cs b/src/Service.Tests/UnitTests/RuntimeConfigLoaderJsonDeserializerTests.cs index b98de993e2..47629ca4c8 100644 --- a/src/Service.Tests/UnitTests/RuntimeConfigLoaderJsonDeserializerTests.cs +++ b/src/Service.Tests/UnitTests/RuntimeConfigLoaderJsonDeserializerTests.cs @@ -79,18 +79,18 @@ public void CheckConfigEnvParsingTest( if (replaceEnvVar) { Assert.IsTrue(RuntimeConfigLoader.TryParseConfig( - GetModifiedJsonString(repValues, @"""postgresql"""), out expectedConfig, replaceEnvVar: replaceEnvVar), + GetModifiedJsonString(repValues, @"""postgresql"""), out expectedConfig, replacementSettings: new DeserializationVariableReplacementSettings(azureKeyVaultOptions: null, doReplaceEnvVar: replaceEnvVar, doReplaceAkvVar: false)), "Should read the expected config"); } else { Assert.IsTrue(RuntimeConfigLoader.TryParseConfig( - GetModifiedJsonString(repKeys, @"""postgresql"""), out expectedConfig, replaceEnvVar: replaceEnvVar), + GetModifiedJsonString(repKeys, @"""postgresql"""), out expectedConfig, replacementSettings: new DeserializationVariableReplacementSettings(azureKeyVaultOptions: null, doReplaceEnvVar: replaceEnvVar, doReplaceAkvVar: false)), "Should read the expected config"); } Assert.IsTrue(RuntimeConfigLoader.TryParseConfig( - GetModifiedJsonString(repKeys, @"""@env('enumVarName')"""), out RuntimeConfig actualConfig, replaceEnvVar: replaceEnvVar), + GetModifiedJsonString(repKeys, @"""@env('enumVarName')"""), out RuntimeConfig actualConfig, replacementSettings: new DeserializationVariableReplacementSettings(azureKeyVaultOptions: null, doReplaceEnvVar: replaceEnvVar, doReplaceAkvVar: false)), "Should read actual config"); Assert.AreEqual(expectedConfig.ToJson(), actualConfig.ToJson()); } @@ -130,7 +130,7 @@ public void TestConfigParsingWithEnvVarReplacement(bool replaceEnvVar, string da string configWithEnvVar = _configWithVariableDataSource.Replace("{0}", GetDataSourceConfigForGivenDatabase(databaseType)); bool isParsingSuccessful = RuntimeConfigLoader.TryParseConfig( - configWithEnvVar, out RuntimeConfig runtimeConfig, replaceEnvVar: replaceEnvVar); + configWithEnvVar, out RuntimeConfig runtimeConfig, replacementSettings: new DeserializationVariableReplacementSettings(azureKeyVaultOptions: null, doReplaceEnvVar: replaceEnvVar, doReplaceAkvVar: true)); // Assert Assert.IsTrue(isParsingSuccessful); @@ -178,7 +178,7 @@ public void TestConfigParsingWhenDataSourceOptionsForCosmosDBContainsInvalidValu string configWithEnvVar = _configWithVariableDataSource.Replace("{0}", GetDataSourceOptionsForCosmosDBWithInvalidValues()); bool isParsingSuccessful = RuntimeConfigLoader.TryParseConfig( - configWithEnvVar, out RuntimeConfig runtimeConfig, replaceEnvVar: true); + configWithEnvVar, out RuntimeConfig runtimeConfig, replacementSettings: new DeserializationVariableReplacementSettings(azureKeyVaultOptions: null, doReplaceEnvVar: true, doReplaceAkvVar: true)); // Assert Assert.IsTrue(isParsingSuccessful); @@ -302,7 +302,7 @@ public void CheckConfigEnvParsingThrowExceptions(string invalidEnvVarName) { string json = @"{ ""foo"" : ""@env('envVarName'), @env('" + invalidEnvVarName + @"')"" }"; SetEnvVariables(); - StringJsonConverterFactory stringConverterFactory = new(EnvironmentVariableReplacementFailureMode.Throw); + StringJsonConverterFactory stringConverterFactory = new(new(doReplaceEnvVar: true, envFailureMode: EnvironmentVariableReplacementFailureMode.Throw)); JsonSerializerOptions options = new() { PropertyNameCaseInsensitive = true }; options.Converters.Add(stringConverterFactory); Assert.ThrowsException(() => JsonSerializer.Deserialize(json, options)); @@ -324,7 +324,7 @@ public void TestDataSourceDeserializationFailures(string dbType, string connecti ""entities"":{ } }"; // replaceEnvVar: true is needed to make sure we do post-processing for the connection string case - Assert.IsFalse(RuntimeConfigLoader.TryParseConfig(configJson, out RuntimeConfig deserializedConfig, replaceEnvVar: true)); + Assert.IsFalse(RuntimeConfigLoader.TryParseConfig(configJson, out RuntimeConfig deserializedConfig, replacementSettings: new DeserializationVariableReplacementSettings(azureKeyVaultOptions: null, doReplaceEnvVar: true, doReplaceAkvVar: true))); Assert.IsNull(deserializedConfig); } @@ -343,7 +343,8 @@ public void TestLoadRuntimeConfigFailures( MockFileSystem fileSystem = new(); FileSystemRuntimeConfigLoader loader = new(fileSystem); - Assert.IsFalse(loader.TryLoadConfig(configFileName, out RuntimeConfig _)); + // Use null replacement settings for this test + Assert.IsFalse(loader.TryLoadConfig(configFileName, out RuntimeConfig _, replacementSettings: null)); } /// diff --git a/src/Service.Tests/UnitTests/SerializationDeserializationTests.cs b/src/Service.Tests/UnitTests/SerializationDeserializationTests.cs index 44978cd6aa..74d548fef4 100644 --- a/src/Service.Tests/UnitTests/SerializationDeserializationTests.cs +++ b/src/Service.Tests/UnitTests/SerializationDeserializationTests.cs @@ -8,6 +8,7 @@ using System.Reflection; using System.Text.Json; using System.Text.Json.Serialization; +using Azure.DataApiBuilder.Config; using Azure.DataApiBuilder.Config.DatabasePrimitives; using Azure.DataApiBuilder.Config.ObjectModel; using Azure.DataApiBuilder.Core.Services.MetadataProviders.Converters; @@ -187,7 +188,7 @@ public void TestSourceDefinitionCyclicObjectsSerializationDeserialization() _sourceDefinition.SourceEntityRelationshipMap.Add("persons", metadata); - // In serialization options we need ReferenceHandler = ReferenceHandler.Preserve, or else it doesnot seialize objects with cycle references + // In serialization options we need ReferenceHandler = ReferenceHandler.Preserve, or else it does not serialize objects with cycle references // SourceDefinition -> RelationShipMetadata -> ForeignKeyDefinition RelationshipPair ->DatabaseTable -> SourceDefinition Assert.ThrowsException(() => { @@ -489,5 +490,68 @@ private RelationShipPair GetRelationShipPair() }; return new(_databaseTable, table2); } + + /// + /// Verifies that when merging multiple runtime configs, if the child config omits + /// the azure-key-vault section, the merged result still contains the AzureKeyVaultOptions (including retry-policy) + /// inherited from the parent config. + /// + [TestMethod] + public void TestMergedConfigInheritsAzureKeyVaultOptions() + { + // Arrange + + // Parent config with AKV section. + string parentConfig = @"{ + ""data-source"": { ""database-type"": ""mssql"", ""connection-string"": ""Server=.;Database=Parent;Trusted_Connection=True;"" }, + ""runtime"": { ""rest"": { ""enabled"": true }, ""graphql"": { ""enabled"": true } }, + ""entities"": {}, + ""azure-key-vault"": { + ""endpoint"": ""https://myvault.vault.azure.net/"", + ""retry-policy"": { + ""mode"": ""fixed"", + ""max-count"": 7, + ""delay-seconds"": 3, + ""max-delay-seconds"": 15, + ""network-timeout-seconds"": 20 + } + } +}"; + + // Child config overrides some properties but omits azure-key-vault entirely. + string childConfig = @"{ + ""data-source"": { ""database-type"": ""mssql"", ""connection-string"": ""Server=.;Database=Child;Trusted_Connection=True;"" }, + ""runtime"": { ""rest"": { ""enabled"": true }, ""graphql"": { ""enabled"": true } }, + ""entities"": {} +}"; + // Act + + // Merge child over parent. + string mergedJson = MergeJsonProvider.Merge(parentConfig, childConfig); + + // Parse with AKV replacement enabled so extraction path executes. + DeserializationVariableReplacementSettings replacementSettings = new( + azureKeyVaultOptions: null, + doReplaceEnvVar: false, + doReplaceAkvVar: true); + + // Assert + + Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(mergedJson, out RuntimeConfig mergedConfig, replacementSettings: replacementSettings), "Merged runtime config failed to parse."); + Assert.IsNotNull(mergedConfig, "Merged runtime config is null."); + + // Validate AKV inheritance. + Assert.IsNotNull(mergedConfig.AzureKeyVault, "AzureKeyVaultOptions should be inherited from base config."); + Assert.AreEqual("https://myvault.vault.azure.net/", mergedConfig.AzureKeyVault!.Endpoint, "Inherited AKV endpoint mismatch."); + Assert.IsNotNull(mergedConfig.AzureKeyVault.RetryPolicy, "RetryPolicy should be inherited."); + Assert.AreEqual(AKVRetryPolicyMode.Fixed, mergedConfig.AzureKeyVault.RetryPolicy!.Mode, "Inherited retry-policy mode mismatch."); + Assert.AreEqual(7, mergedConfig.AzureKeyVault.RetryPolicy.MaxCount, "Inherited retry-policy max-count mismatch."); + Assert.AreEqual(3, mergedConfig.AzureKeyVault.RetryPolicy.DelaySeconds, "Inherited retry-policy delay-seconds mismatch."); + Assert.AreEqual(15, mergedConfig.AzureKeyVault.RetryPolicy.MaxDelaySeconds, "Inherited retry-policy max-delay-seconds mismatch."); + Assert.AreEqual(20, mergedConfig.AzureKeyVault.RetryPolicy.NetworkTimeoutSeconds, "Inherited retry-policy network-timeout-seconds mismatch."); + + // Ensure child override for connection-string applied while AKV remained from base. + Assert.AreEqual("Server=.;Database=Child;Trusted_Connection=True;", mergedConfig.DataSource.ConnectionString, "Child connection-string override not applied."); + } } } diff --git a/src/Service.Tests/UnitTests/SqlQueryExecutorUnitTests.cs b/src/Service.Tests/UnitTests/SqlQueryExecutorUnitTests.cs index 2b62c6b444..b3782950f9 100644 --- a/src/Service.Tests/UnitTests/SqlQueryExecutorUnitTests.cs +++ b/src/Service.Tests/UnitTests/SqlQueryExecutorUnitTests.cs @@ -115,7 +115,8 @@ await provider.Initialize( provider.GetConfig().ToJson(), graphQLSchema: null, connectionString: connectionString, - accessToken: CONFIG_TOKEN); + accessToken: CONFIG_TOKEN, + replacementSettings: new()); msSqlQueryExecutor = new(provider, dbExceptionParser.Object, queryExecutorLogger.Object, httpContextAccessor.Object); } } diff --git a/src/Service/Controllers/ConfigurationController.cs b/src/Service/Controllers/ConfigurationController.cs index be3f9bd727..4ad8fb40f4 100644 --- a/src/Service/Controllers/ConfigurationController.cs +++ b/src/Service/Controllers/ConfigurationController.cs @@ -91,8 +91,8 @@ public async Task Index([FromBody] ConfigurationPostParameters con configuration.Schema, configuration.ConnectionString, configuration.AccessToken, - replaceEnvVar: false, - replacementFailureMode: Config.Converters.EnvironmentVariableReplacementFailureMode.Ignore); + replacementSettings: new(azureKeyVaultOptions: null, doReplaceEnvVar: false, doReplaceAkvVar: false, envFailureMode: Config.Converters.EnvironmentVariableReplacementFailureMode.Ignore) + ); if (initResult && _configurationProvider.TryGetConfig(out _)) { From 12e5ccf3c53a602594db04365ab152ef0fb87464 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Sun, 23 Nov 2025 18:15:02 -0800 Subject: [PATCH 11/36] Skip PR validation for non-code paths (samples, docs, schemas, templates) (#2988) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Why make this change? Closes #2977. Running full integration test suites on PRs that only modify samples, documentation, or configuration files wastes CI resources and slows down contribution velocity. ## What is this change? Added `pr:` trigger sections with path exclusions to all 7 Azure Pipeline definitions in `.pipelines/`: **Excluded paths:** - `samples/**` - Sample configurations and code - `docs/**` - Documentation - `*.md` - Markdown files (README, CONTRIBUTING, etc.) - `schemas/**` - JSON schema definitions - `templates/**` - Project templates **Modified pipelines:** - `azure-pipelines.yml` - Static analysis - `cosmos-pipelines.yml` - CosmosDB integration tests - `dwsql-pipelines.yml` - Data Warehouse SQL tests - `mssql-pipelines.yml` - SQL Server tests - `mysql-pipelines.yml` - MySQL tests - `pg-pipelines.yml` - PostgreSQL tests - `unittest-pipelines.yml` - Unit test suite PRs touching only excluded paths will skip pipeline execution. PRs with mixed changes (code + excluded paths) still run all validations. ## How was this tested? - [x] YAML syntax validation - [ ] Integration Tests - N/A (configuration change) - [ ] Unit Tests - N/A (configuration change) ## Sample Request(s) N/A - Pipeline configuration change only. Validation occurs automatically on PR creation.
Original prompt > > ---- > > *This section details on the original issue you should resolve* > > Skip Pull Request validation for certain paths > We really need to add this to our tests in order to avoid requiring the full suite of integration tests when /samples is updated. In fact, there are many paths in this repo that should be set up to allow pull requests to skip integration tests. Please consider. > > ```yaml > on: > pull_request: > paths-ignore: > - 'samples/**' > ``` > > _Originally posted by @JerryNixon in https://github.com/Azure/data-api-builder/issues/2977#issuecomment-3536055197_ > > > Modify the yml files in the .pipelines folder which do Pull Request validation to skip paths like samples. This will help checkin PRs on those paths quickly. > > ## Comments on the Issue (you are @copilot in this section) > > > >
- Fixes Azure/data-api-builder#2987 --- 💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more [Copilot coding agent tips](https://gh.io/copilot-coding-agent-tips) in the docs. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Aniruddh25 <3513779+Aniruddh25@users.noreply.github.com> --- .pipelines/azure-pipelines.yml | 12 ++++++++++++ .pipelines/cosmos-pipelines.yml | 12 ++++++++++++ .pipelines/dwsql-pipelines.yml | 12 ++++++++++++ .pipelines/mssql-pipelines.yml | 12 ++++++++++++ .pipelines/mysql-pipelines.yml | 12 ++++++++++++ .pipelines/pg-pipelines.yml | 12 ++++++++++++ .pipelines/unittest-pipelines.yml | 20 ++++++++++++++++++++ 7 files changed, 92 insertions(+) diff --git a/.pipelines/azure-pipelines.yml b/.pipelines/azure-pipelines.yml index a29cd29de2..88ce6bb186 100644 --- a/.pipelines/azure-pipelines.yml +++ b/.pipelines/azure-pipelines.yml @@ -9,6 +9,18 @@ trigger: - gh-readonly-queue/main - release/* +pr: + branches: + include: + - main + - release/* + paths: + exclude: + - samples/** + - docs/** + - '*.md' + - templates/** + variables: # DebugArguments: ' --filter "TestCategory!=ABC" --verbosity normal ' # ReleaseArguments: ' --filter "TestCategory!=ABC" --verbosity normal ' diff --git a/.pipelines/cosmos-pipelines.yml b/.pipelines/cosmos-pipelines.yml index 2f95f45211..61d066aac7 100644 --- a/.pipelines/cosmos-pipelines.yml +++ b/.pipelines/cosmos-pipelines.yml @@ -13,6 +13,18 @@ trigger: - gh-readonly-queue/main - release/* +pr: + branches: + include: + - main + - release/* + paths: + exclude: + - samples/** + - docs/** + - '*.md' + - templates/** + strategy: matrix: windows: diff --git a/.pipelines/dwsql-pipelines.yml b/.pipelines/dwsql-pipelines.yml index 2ae2730747..767c6c7df9 100644 --- a/.pipelines/dwsql-pipelines.yml +++ b/.pipelines/dwsql-pipelines.yml @@ -16,6 +16,18 @@ trigger: exclude: - docs +pr: + branches: + include: + - main + - release/* + paths: + exclude: + - samples/** + - docs/** + - '*.md' + - templates/** + jobs: - job: linux pool: diff --git a/.pipelines/mssql-pipelines.yml b/.pipelines/mssql-pipelines.yml index 81955bd889..8ddbbbd88b 100644 --- a/.pipelines/mssql-pipelines.yml +++ b/.pipelines/mssql-pipelines.yml @@ -16,6 +16,18 @@ trigger: exclude: - docs +pr: + branches: + include: + - main + - release/* + paths: + exclude: + - samples/** + - docs/** + - '*.md' + - templates/** + jobs: - job: linux condition: false # Disable until we resolve flakey pipeline issue. https://github.com/Azure/data-api-builder/issues/2010 diff --git a/.pipelines/mysql-pipelines.yml b/.pipelines/mysql-pipelines.yml index 2623415a70..7377d5b69c 100644 --- a/.pipelines/mysql-pipelines.yml +++ b/.pipelines/mysql-pipelines.yml @@ -14,6 +14,18 @@ trigger: - gh-readonly-queue/main - release/* +pr: + branches: + include: + - main + - release/* + paths: + exclude: + - samples/** + - docs/** + - '*.md' + - templates/** + jobs: - job: linux pool: diff --git a/.pipelines/pg-pipelines.yml b/.pipelines/pg-pipelines.yml index 6618dab734..d6b7600e1f 100644 --- a/.pipelines/pg-pipelines.yml +++ b/.pipelines/pg-pipelines.yml @@ -9,6 +9,18 @@ trigger: - gh-readonly-queue/main - release/* +pr: + branches: + include: + - main + - release/* + paths: + exclude: + - samples/** + - docs/** + - '*.md' + - templates/** + jobs: - job: linux pool: diff --git a/.pipelines/unittest-pipelines.yml b/.pipelines/unittest-pipelines.yml index b82562f780..56357a181c 100644 --- a/.pipelines/unittest-pipelines.yml +++ b/.pipelines/unittest-pipelines.yml @@ -1,6 +1,26 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. +trigger: + batch: true + branches: + include: + - main + - gh-readonly-queue/main + - release/* + +pr: + branches: + include: + - main + - release/* + paths: + exclude: + - samples/** + - docs/** + - '*.md' + - templates/** + pool: vmImage: 'ubuntu-latest' # examples of other options: 'macOS-10.15', 'windows-2019' From 08ca05ff82ee69678cc5dfd8f7b007eb12b6fb14 Mon Sep 17 00:00:00 2001 From: Jerry Nixon <1749983+JerryNixon@users.noreply.github.com> Date: Mon, 24 Nov 2025 20:16:09 -0700 Subject: [PATCH 12/36] Added aspire sample (#2977) This is a simple sample that demonstrates how to use DAB in Aspire. I will add it to our docs/quick-starts when this is merged. Please note there is no /src code change, just /samples. --------- Co-authored-by: Jerry Nixon Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Aniruddh Munde Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> Co-authored-by: Aniruddh25 <3513779+Aniruddh25@users.noreply.github.com> --- samples/aspire/.aspire/settings.json | 5 + samples/aspire/.gitignore | 5 + samples/aspire/apphost.cs | 73 ++++ samples/aspire/dab-config.cmd | 66 ++++ samples/aspire/dab-config.json | 534 +++++++++++++++++++++++++++ samples/aspire/database.sql | 465 +++++++++++++++++++++++ samples/aspire/global.json | 5 + samples/aspire/readme.md | 66 ++++ 8 files changed, 1219 insertions(+) create mode 100644 samples/aspire/.aspire/settings.json create mode 100644 samples/aspire/.gitignore create mode 100644 samples/aspire/apphost.cs create mode 100644 samples/aspire/dab-config.cmd create mode 100644 samples/aspire/dab-config.json create mode 100644 samples/aspire/database.sql create mode 100644 samples/aspire/global.json create mode 100644 samples/aspire/readme.md diff --git a/samples/aspire/.aspire/settings.json b/samples/aspire/.aspire/settings.json new file mode 100644 index 0000000000..bbfbabae61 --- /dev/null +++ b/samples/aspire/.aspire/settings.json @@ -0,0 +1,5 @@ +{ + "appHostPath": "../apphost.cs", + "features.minimumSdkCheckEnabled": true, + "features.singleFileAppHostEnabled": true +} \ No newline at end of file diff --git a/samples/aspire/.gitignore b/samples/aspire/.gitignore new file mode 100644 index 0000000000..f7ed5b1225 --- /dev/null +++ b/samples/aspire/.gitignore @@ -0,0 +1,5 @@ +# Do not ignore anything in this folder +!* + +# Explicitly include dab-config.json +!dab-config.json diff --git a/samples/aspire/apphost.cs b/samples/aspire/apphost.cs new file mode 100644 index 0000000000..0e44c700a8 --- /dev/null +++ b/samples/aspire/apphost.cs @@ -0,0 +1,73 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#:sdk Aspire.AppHost.Sdk@13.0.0 +#:package Aspire.Hosting.SqlServer@13.0.0 +#:package CommunityToolkit.Aspire.Hosting.SqlDatabaseProjects@9.8.1-beta.420 +#:package CommunityToolkit.Aspire.Hosting.Azure.DataApiBuilder@9.8.1-beta.420 +#:package CommunityToolkit.Aspire.Hosting.McpInspector@9.8.0 + +var builder = DistributedApplication.CreateBuilder(args); + +var options = new +{ + SqlScript = "database.sql", + SqlDatabase = "StarTrek", + DabConfig = "dab-config.json", + DabImage = "1.7.81-rc", + SqlCmdrImage = "latest" +}; + +var sqlServer = builder + .AddSqlServer("sql-server") + .WithDataVolume("sql-data-volume") + .WithLifetime(ContainerLifetime.Persistent); + +var sqlDatabase = sqlServer + .AddDatabase("sql-database", options.SqlDatabase) + .WithCreationScript(File.ReadAllText(new FileInfo(options.SqlScript).FullName)); + +var dabServer = builder + .AddContainer("data-api", image: "azure-databases/data-api-builder", tag: options.DabImage) + .WithImageRegistry("mcr.microsoft.com") + .WithBindMount(source: new FileInfo(options.DabConfig).FullName, target: "/App/dab-config.json", isReadOnly: true) + .WithHttpEndpoint(targetPort: 5000, name: "http") + .WithEnvironment("MSSQL_CONNECTION_STRING", sqlDatabase) + .WithUrls(context => + { + context.Urls.Clear(); + context.Urls.Add(new() { Url = "/graphql", DisplayText = "Nitro", Endpoint = context.GetEndpoint("http") }); + context.Urls.Add(new() { Url = "/swagger", DisplayText = "Swagger", Endpoint = context.GetEndpoint("http") }); + context.Urls.Add(new() { Url = "/health", DisplayText = "Health", Endpoint = context.GetEndpoint("http") }); + }) + .WithOtlpExporter() + .WithParentRelationship(sqlDatabase) + .WithHttpHealthCheck("/health") + .WaitFor(sqlDatabase); + +var sqlCommander = builder + .AddContainer("sql-cmdr", "jerrynixon/sql-commander", options.SqlCmdrImage) + .WithImageRegistry("docker.io") + .WithHttpEndpoint(targetPort: 8080, name: "http") + .WithEnvironment("ConnectionStrings__db", sqlDatabase) + .WithUrls(context => + { + context.Urls.Clear(); + context.Urls.Add(new() { Url = "/", DisplayText = "Commander", Endpoint = context.GetEndpoint("http") }); + }) + .WithParentRelationship(sqlDatabase) + .WithHttpHealthCheck("/health") + .WaitFor(sqlDatabase); + +var mcpInspector = builder + .AddMcpInspector("mcp-inspector") + .WithMcpServer(dabServer) + .WithParentRelationship(dabServer) + .WithEnvironment("NODE_TLS_REJECT_UNAUTHORIZED", "0") + .WaitFor(dabServer) + .WithUrls(context => + { + context.Urls.First().DisplayText = "Inspector"; + }); + +await builder.Build().RunAsync(); \ No newline at end of file diff --git a/samples/aspire/dab-config.cmd b/samples/aspire/dab-config.cmd new file mode 100644 index 0000000000..6cc89703cc --- /dev/null +++ b/samples/aspire/dab-config.cmd @@ -0,0 +1,66 @@ +@echo off + +del /q dab-config.json 2>nul + +set MSSQL_CONNECTION_STRING=%MSSQL_CONNECTION_STRING% + +dab init --database-type mssql --connection-string "@env('MSSQL_CONNECTION_STRING')" --config dab-config.json --host-mode development + +dab add Series --source dbo.Series --source.type table --permissions "anonymous:*" --description "Star Trek series" +dab update Series --fields.name Id --fields.description "Primary key" --fields.primary-key true +dab update Series --fields.name Name --fields.description "Series name" --fields.primary-key false + +dab add Actor --source dbo.Actor --source.type table --permissions "anonymous:*" --description "An actor in the franchise" +dab update Actor --fields.name Id --fields.description "Primary key" --fields.primary-key true +dab update Actor --fields.name FirstName --fields.description "Given name" --fields.primary-key false +dab update Actor --fields.name LastName --fields.description "Family name" --fields.primary-key false +dab update Actor --fields.name BirthYear --fields.description "Year of birth" --fields.primary-key false +dab update Actor --fields.name FullName --fields.description "Computed full name" --fields.primary-key false + +dab add Species --source dbo.Species --source.type table --permissions "anonymous:*" --description "Alien species in Star Trek" +dab update Species --fields.name Id --fields.description "Primary key" --fields.primary-key true +dab update Species --fields.name Name --fields.description "Species name" --fields.primary-key false + +dab add Character --source dbo.Character --source.type table --permissions "anonymous:*" --description "A fictional character portrayed by an actor" +dab update Character --fields.name Id --fields.description "Primary key" --fields.primary-key true +dab update Character --fields.name Name --fields.description "Character name" --fields.primary-key false +dab update Character --fields.name ActorId --fields.description "Foreign key to Actor" --fields.primary-key false +dab update Character --fields.name Stardate --fields.description "Birth stardate" --fields.primary-key false + +dab add Series_Character --source dbo.Series_Character --source.type table --permissions "anonymous:*" --description "Characters appearing in series" +dab update Series_Character --fields.name SeriesId --fields.description "Foreign key to Series" --fields.primary-key true +dab update Series_Character --fields.name CharacterId --fields.description "Foreign key to Character" --fields.primary-key true +dab update Series_Character --fields.name Role --fields.description "Character role in series" --fields.primary-key false + +dab add Character_Species --source dbo.Character_Species --source.type table --permissions "anonymous:*" --description "Species composition of characters" +dab update Character_Species --fields.name CharacterId --fields.description "Foreign key to Character" --fields.primary-key true +dab update Character_Species --fields.name SpeciesId --fields.description "Foreign key to Species" --fields.primary-key true + +dab update Character --relationship Character_Actor --target.entity Actor --cardinality one --relationship.fields "ActorId:Id" +dab update Character --relationship Character_Series --cardinality many --target.entity Series_Character --relationship.fields "Id:CharacterId" +dab update Character --relationship Character_Species --cardinality many --target.entity Character_Species --relationship.fields "Id:CharacterId" + +dab update Actor --relationship Actor_Characters --cardinality many --target.entity Character --relationship.fields "Id:ActorId" + +dab update Species --relationship Species_Characters --cardinality many --target.entity Character_Species --relationship.fields "Id:SpeciesId" + +dab update Series --relationship Series_Characters --cardinality many --target.entity Series_Character --relationship.fields "Id:SeriesId" + +dab update Series_Character --relationship SeriesCharacter_Series --cardinality one --target.entity Series --relationship.fields "SeriesId:Id" +dab update Series_Character --relationship SeriesCharacter_Character --cardinality one --target.entity Character --relationship.fields "CharacterId:Id" + +dab update Character_Species --relationship CharacterSpecies_Character --cardinality one --target.entity Character --relationship.fields "CharacterId:Id" +dab update Character_Species --relationship CharacterSpecies_Species --cardinality one --target.entity Species --relationship.fields "SpeciesId:Id" + +dab add SeriesActors --source dbo.SeriesActors --source.type view --source.key-fields "Id,SeriesId" --permissions "anonymous:*" --description "Actors appearing in each series" +dab update SeriesActors --fields.name Id --fields.description "Actor id" --fields.primary-key true +dab update SeriesActors --fields.name SeriesId --fields.description "Series id" --fields.primary-key true +dab update SeriesActors --fields.name Actor --fields.description "Actor name" --fields.primary-key false +dab update SeriesActors --fields.name BirthYear --fields.description "Year of birth" --fields.primary-key false +dab update SeriesActors --fields.name Series --fields.description "Series name" --fields.primary-key false + +dab add GetSeriesActors --source dbo.GetSeriesActors --source.type stored-procedure --permissions "anonymous:*" --description "Return actors in a series" +dab update GetSeriesActors --parameters.name seriesId --parameters.description "Series identifier" --parameters.required false --parameters.default 1 +dab update GetSeriesActors --parameters.name top --parameters.description "Limit rows" --parameters.required false --parameters.default 5 + +echo DAB configuration complete! diff --git a/samples/aspire/dab-config.json b/samples/aspire/dab-config.json new file mode 100644 index 0000000000..0d3ea6f48b --- /dev/null +++ b/samples/aspire/dab-config.json @@ -0,0 +1,534 @@ +{ + "$schema": "https://github.com/Azure/data-api-builder/releases/download/v1.7.81/dab.draft.schema.json", + "data-source": { + "database-type": "mssql", + "connection-string": "@env('MSSQL_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 + }, + "mcp": { + "enabled": true, + "path": "/mcp" + }, + "host": { + "cors": { + "origins": [], + "allow-credentials": false + }, + "authentication": { + "provider": "StaticWebApps" + }, + "mode": "development" + } + }, + "entities": { + "Series": { + "description": "Star Trek series", + "source": { + "object": "dbo.Series", + "type": "table" + }, + "fields": [ + { + "name": "Id", + "description": "Primary key", + "primary-key": true + }, + { + "name": "Name", + "description": "Series name", + "primary-key": false + } + ], + "graphql": { + "enabled": true, + "type": { + "singular": "Series", + "plural": "Series" + } + }, + "rest": { + "enabled": true + }, + "permissions": [ + { + "role": "anonymous", + "actions": [ + { + "action": "*" + } + ] + } + ], + "relationships": { + "Series_Characters": { + "cardinality": "many", + "target.entity": "Series_Character", + "source.fields": [ + "Id" + ], + "target.fields": [ + "SeriesId" + ], + "linking.source.fields": [], + "linking.target.fields": [] + } + } + }, + "Actor": { + "description": "An actor in the franchise", + "source": { + "object": "dbo.Actor", + "type": "table" + }, + "fields": [ + { + "name": "Id", + "description": "Primary key", + "primary-key": true + }, + { + "name": "FirstName", + "description": "Given name", + "primary-key": false + }, + { + "name": "LastName", + "description": "Family name", + "primary-key": false + }, + { + "name": "BirthYear", + "description": "Year of birth", + "primary-key": false + }, + { + "name": "FullName", + "description": "Computed full name", + "primary-key": false + } + ], + "graphql": { + "enabled": true, + "type": { + "singular": "Actor", + "plural": "Actors" + } + }, + "rest": { + "enabled": true + }, + "permissions": [ + { + "role": "anonymous", + "actions": [ + { + "action": "*" + } + ] + } + ], + "relationships": { + "Actor_Characters": { + "cardinality": "many", + "target.entity": "Character", + "source.fields": [ + "Id" + ], + "target.fields": [ + "ActorId" + ], + "linking.source.fields": [], + "linking.target.fields": [] + } + } + }, + "Species": { + "description": "Alien species in Star Trek", + "source": { + "object": "dbo.Species", + "type": "table" + }, + "fields": [ + { + "name": "Id", + "description": "Primary key", + "primary-key": true + }, + { + "name": "Name", + "description": "Species name", + "primary-key": false + } + ], + "graphql": { + "enabled": true, + "type": { + "singular": "Species", + "plural": "Species" + } + }, + "rest": { + "enabled": true + }, + "permissions": [ + { + "role": "anonymous", + "actions": [ + { + "action": "*" + } + ] + } + ], + "relationships": { + "Species_Characters": { + "cardinality": "many", + "target.entity": "Character_Species", + "source.fields": [ + "Id" + ], + "target.fields": [ + "SpeciesId" + ], + "linking.source.fields": [], + "linking.target.fields": [] + } + } + }, + "Character": { + "description": "A fictional character portrayed by an actor", + "source": { + "object": "dbo.Character", + "type": "table" + }, + "fields": [ + { + "name": "Id", + "description": "Primary key", + "primary-key": true + }, + { + "name": "Name", + "description": "Character name", + "primary-key": false + }, + { + "name": "ActorId", + "description": "Foreign key to Actor", + "primary-key": false + }, + { + "name": "Stardate", + "description": "Birth stardate", + "primary-key": false + } + ], + "graphql": { + "enabled": true, + "type": { + "singular": "Character", + "plural": "Characters" + } + }, + "rest": { + "enabled": true + }, + "permissions": [ + { + "role": "anonymous", + "actions": [ + { + "action": "*" + } + ] + } + ], + "relationships": { + "Character_Actor": { + "cardinality": "one", + "target.entity": "Actor", + "source.fields": [ + "ActorId" + ], + "target.fields": [ + "Id" + ], + "linking.source.fields": [], + "linking.target.fields": [] + }, + "Character_Series": { + "cardinality": "many", + "target.entity": "Series_Character", + "source.fields": [ + "Id" + ], + "target.fields": [ + "CharacterId" + ], + "linking.source.fields": [], + "linking.target.fields": [] + }, + "Character_Species": { + "cardinality": "many", + "target.entity": "Character_Species", + "source.fields": [ + "Id" + ], + "target.fields": [ + "CharacterId" + ], + "linking.source.fields": [], + "linking.target.fields": [] + } + } + }, + "Series_Character": { + "description": "Characters appearing in series", + "source": { + "object": "dbo.Series_Character", + "type": "table" + }, + "fields": [ + { + "name": "SeriesId", + "description": "Foreign key to Series", + "primary-key": true + }, + { + "name": "CharacterId", + "description": "Foreign key to Character", + "primary-key": true + }, + { + "name": "Role", + "description": "Character role in series", + "primary-key": false + } + ], + "graphql": { + "enabled": true, + "type": { + "singular": "Series_Character", + "plural": "Series_Characters" + } + }, + "rest": { + "enabled": true + }, + "permissions": [ + { + "role": "anonymous", + "actions": [ + { + "action": "*" + } + ] + } + ], + "relationships": { + "SeriesCharacter_Series": { + "cardinality": "one", + "target.entity": "Series", + "source.fields": [ + "SeriesId" + ], + "target.fields": [ + "Id" + ], + "linking.source.fields": [], + "linking.target.fields": [] + }, + "SeriesCharacter_Character": { + "cardinality": "one", + "target.entity": "Character", + "source.fields": [ + "CharacterId" + ], + "target.fields": [ + "Id" + ], + "linking.source.fields": [], + "linking.target.fields": [] + } + } + }, + "Character_Species": { + "description": "Species composition of characters", + "source": { + "object": "dbo.Character_Species", + "type": "table" + }, + "fields": [ + { + "name": "CharacterId", + "description": "Foreign key to Character", + "primary-key": true + }, + { + "name": "SpeciesId", + "description": "Foreign key to Species", + "primary-key": true + } + ], + "graphql": { + "enabled": true, + "type": { + "singular": "Character_Species", + "plural": "Character_Species" + } + }, + "rest": { + "enabled": true + }, + "permissions": [ + { + "role": "anonymous", + "actions": [ + { + "action": "*" + } + ] + } + ], + "relationships": { + "CharacterSpecies_Character": { + "cardinality": "one", + "target.entity": "Character", + "source.fields": [ + "CharacterId" + ], + "target.fields": [ + "Id" + ], + "linking.source.fields": [], + "linking.target.fields": [] + }, + "CharacterSpecies_Species": { + "cardinality": "one", + "target.entity": "Species", + "source.fields": [ + "SpeciesId" + ], + "target.fields": [ + "Id" + ], + "linking.source.fields": [], + "linking.target.fields": [] + } + } + }, + "SeriesActors": { + "description": "Actors appearing in each series", + "source": { + "object": "dbo.SeriesActors", + "type": "view" + }, + "fields": [ + { + "name": "Id", + "description": "Actor id", + "primary-key": true + }, + { + "name": "SeriesId", + "description": "Series id", + "primary-key": true + }, + { + "name": "Actor", + "description": "Actor name", + "primary-key": false + }, + { + "name": "BirthYear", + "description": "Year of birth", + "primary-key": false + }, + { + "name": "Series", + "description": "Series name", + "primary-key": false + } + ], + "graphql": { + "enabled": true, + "type": { + "singular": "SeriesActors", + "plural": "SeriesActors" + } + }, + "rest": { + "enabled": true + }, + "permissions": [ + { + "role": "anonymous", + "actions": [ + { + "action": "*" + } + ] + } + ] + }, + "GetSeriesActors": { + "description": "Return actors in a series", + "source": { + "object": "dbo.GetSeriesActors", + "type": "stored-procedure", + "parameters": [ + { + "name": "top", + "description": "Limit rows", + "required": false, + "default": "5" + }, + { + "name": "seriesId", + "description": "Series identifier", + "required": false, + "default": "1" + } + ] + }, + "fields": [], + "graphql": { + "enabled": true, + "operation": "mutation", + "type": { + "singular": "GetSeriesActors", + "plural": "GetSeriesActors" + } + }, + "rest": { + "enabled": true, + "methods": [ + "post" + ] + }, + "permissions": [ + { + "role": "anonymous", + "actions": [ + { + "action": "*" + } + ] + } + ] + } + } +} \ No newline at end of file diff --git a/samples/aspire/database.sql b/samples/aspire/database.sql new file mode 100644 index 0000000000..ca17dadb99 --- /dev/null +++ b/samples/aspire/database.sql @@ -0,0 +1,465 @@ +USE [Master] + +GO +IF NOT EXISTS (SELECT * FROM sys.databases WHERE name = 'StarTrek') +BEGIN + CREATE DATABASE StarTrek; +END + +GO +USE [StarTrek] + +GO +IF NOT EXISTS (SELECT * FROM sys.tables WHERE name = 'Actor') +BEGIN + RETURN; -- assume all tables exist +END + +GO +-- Drop tables in reverse order of creation due to foreign key dependencies +DROP TABLE IF EXISTS Character_Species; +DROP TABLE IF EXISTS Series_Character; +DROP TABLE IF EXISTS Character; +DROP TABLE IF EXISTS Species; +DROP TABLE IF EXISTS Actor; +DROP TABLE IF EXISTS Series; +DROP VIEW IF EXISTS SeriesActors; +DROP PROC IF EXISTS GetSeriesActors; + +GO +-- create tables +CREATE TABLE Series ( + Id INT PRIMARY KEY, + Name NVARCHAR(255) NOT NULL +); + +GO +CREATE TABLE Actor ( + Id INT PRIMARY KEY, + FirstName NVARCHAR(100) NOT NULL, + LastName NVARCHAR(100) NOT NULL, + [BirthYear] INT NOT NULL, + FullName AS (RTRIM(LTRIM(FirstName + ' ' + LastName))) PERSISTED +); + +GO +CREATE TABLE Species ( + Id INT PRIMARY KEY, + Name NVARCHAR(255) NOT NULL +); + +GO +CREATE TABLE Character ( + Id INT PRIMARY KEY, + Name NVARCHAR(255) NOT NULL, + ActorId INT NOT NULL, + Stardate DECIMAL(10, 2), + FOREIGN KEY (ActorId) REFERENCES Actor(Id) +); + +GO +CREATE TABLE Series_Character ( + SeriesId INT, + CharacterId INT, + Role VARCHAR(500), + FOREIGN KEY (SeriesId) REFERENCES Series(Id), + FOREIGN KEY (CharacterId) REFERENCES Character(Id), + PRIMARY KEY (SeriesId, CharacterId) +); + +GO +CREATE TABLE Character_Species ( + CharacterId INT, + SpeciesId INT, + FOREIGN KEY (CharacterId) REFERENCES Character(Id), + FOREIGN KEY (SpeciesId) REFERENCES Species(Id), + PRIMARY KEY (CharacterId, SpeciesId) +); + +GO +-- create data +INSERT INTO Series (Id, Name) VALUES + (1, 'Star Trek'), + (2, 'Star Trek: The Next Generation'), + (3, 'Star Trek: Voyager'), + (4, 'Star Trek: Deep Space Nine'), + (5, 'Star Trek: Enterprise'), + (6, 'Star Trek: Discovery'), + (7, 'Star Trek: Strange New Worlds'), + (8, 'Star Trek: Lower Decks'), + (9, 'Star Trek: Prodigy'), + (10, 'Star Trek: Starfleet Academy'), + (11, 'Star Trek: Section 31'); + +GO +INSERT INTO Species (Id, Name) VALUES + (1, 'Human'), + (2, 'Vulcan'), + (3, 'Android'), + (4, 'Klingon'), + (5, 'Betazoid'), + (6, 'Hologram'), + (7, 'Bajoran'), + (8, 'Changeling'), + (9, 'Trill'), + (10, 'Ferengi'), + (11, 'Denobulan'), + (12, 'Borg'), + (13, 'Kelpien'), + (14, 'Illyrian'), + (15, 'Brikar'), + (16, 'Medusan'), + (17, 'Vau N''Akat'), + (18, 'Deltan'), + (19, 'Chameloid'), + (20, 'Nanokin'); + +GO +INSERT INTO Actor (Id, FirstName, LastName, [BirthYear]) VALUES + (1, 'William', 'Shatner', 1931), + (2, 'Leonard', 'Nimoy', 1931), + (3, 'DeForest', 'Kelley', 1920), + (4, 'James', 'Doohan', 1920), + (5, 'Nichelle', 'Nichols', 1932), + (6, 'George', 'Takei', 1937), + (7, 'Walter', 'Koenig', 1936), + (8, 'Patrick', 'Stewart', 1940), + (9, 'Jonathan', 'Frakes', 1952), + (10, 'Brent', 'Spiner', 1949), + (11, 'Michael', 'Dorn', 1952), + (12, 'Gates', 'McFadden', 1949), + (13, 'Marina', 'Sirtis', 1955), + (14, 'LeVar', 'Burton', 1957), + (15, 'Kate', 'Mulgrew', 1955), + (16, 'Robert', 'Beltran', 1953), + (17, 'Tim', 'Russ', 1956), + (18, 'Roxann', 'Dawson', 1958), + (19, 'Robert', 'Duncan McNeill', 1964), + (20, 'Garrett', 'Wang', 1968), + (21, 'Robert', 'Picardo', 1953), + (22, 'Jeri', 'Ryan', 1968), + (23, 'Avery', 'Brooks', 1948), + (24, 'Nana', 'Visitor', 1957), + (25, 'Rene', 'Auberjonois', 1940), + (26, 'Terry', 'Farrell', 1963), + (27, 'Alexander', 'Siddig', 1965), + (28, 'Armin', 'Shimerman', 1949), + (29, 'Cirroc', 'Lofton', 1978), + (30, 'Scott', 'Bakula', 1954), + (31, 'Jolene', 'Blalock', 1975), + (32, 'John', 'Billingsley', 1960), + (33, 'Connor', 'Trinneer', 1969), + (34, 'Dominic', 'Keating', 1962), + (35, 'Linda', 'Park', 1978), + (36, 'Anthony', 'Montgomery', 1971), + (37, 'Sonequa', 'Martin-Green', 1985), + (38, 'Doug', 'Jones', 1960), + (39, 'Anthony', 'Rapp', 1971), + (40, 'Mary', 'Wiseman', 1985), + (41, 'Wilson', 'Cruz', 1974), + -- Strange New Worlds cast + (42, 'Anson', 'Mount', 1973), + (43, 'Ethan', 'Peck', 1986), + (44, 'Rebecca', 'Romijn', 1972), + (45, 'Celia', 'Rose Gooding', 1999), + (46, 'Jess', 'Bush', 1991), + -- Lower Decks cast + (47, 'Tawny', 'Newsome', 1983), + (48, 'Jack', 'Quaid', 1992), + (49, 'Noel', 'Wells', 1986), + (50, 'Eugene', 'Cordero', 1986), + (51, 'Dawnn', 'Lewis', 1961), + -- Prodigy cast + (52, 'Brett', 'Gray', 1996), + (53, 'Ella', 'Purnell', 1996), + (54, 'Jason', 'Mantzoukas', 1972), + (55, 'Angus', 'Imrie', 1994), + (56, 'Dee Bradley', 'Baker', 1962), + -- Starfleet Academy cast + (57, 'Holly', 'Hunter', 1958), + (58, 'Paul', 'Giamatti', 1967), + (59, 'Kerrice', 'Brooks', 1990), + (60, 'Bella', 'Shepard', 2005), + (61, 'George', 'Hawkins', 1999), + -- Section 31 cast + (62, 'Michelle', 'Yeoh', 1962), + (63, 'Omari', 'Hardwick', 1974), + (64, 'Sam', 'Richardson', 1984), + (65, 'Robert', 'Kazinsky', 1983), + (66, 'Kacey', 'Rohl', 1991); + +INSERT INTO Character (Id, Name, ActorId, Stardate) VALUES + (1, 'James T. Kirk', 1, 2233.04), + (2, 'Spock', 2, 2230.06), + (3, 'Leonard McCoy', 3, 2227.00), + (4, 'Montgomery Scott', 4, 2222.00), + (5, 'Uhura', 5, 2233.00), + (6, 'Hikaru Sulu', 6, 2237.00), + (7, 'Pavel Chekov', 7, 2245.00), + (8, 'Jean-Luc Picard', 8, 2305.07), + (9, 'William Riker', 9, 2335.08), + (10, 'Data', 10, 2336.00), + (11, 'Worf', 11, 2340.00), + (12, 'Beverly Crusher', 12, 2324.00), + (13, 'Deanna Troi', 13, 2336.00), + (14, 'Geordi La Forge', 14, 2335.02), + (15, 'Kathryn Janeway', 15, 2336.05), + (16, 'Chakotay', 16, 2329.00), + (17, 'Tuvok', 17, 2264.00), + (18, 'B''Elanna Torres', 18, 2349.00), + (19, 'Tom Paris', 19, 2346.00), + (20, 'Harry Kim', 20, 2349.00), + (21, 'The Doctor', 21, 2371.00), + (22, 'Seven of Nine', 22, 2348.00), + (23, 'Benjamin Sisko', 23, 2332.00), + (24, 'Kira Nerys', 24, 2343.00), + (25, 'Odo', 25, 2337.00), + (26, 'Ezri Dax', 26, 2368.00), + (27, 'Jadzia Dax', 26, 2341.00), + (28, 'Julian Bashir', 27, 2341.00), + (29, 'Quark', 28, 2333.00), + (30, 'Jake Sisko', 29, 2355.00), + (31, 'Jonathan Archer', 30, 2112.00), + (32, 'T''Pol', 31, 2088.00), + (33, 'Phlox', 32, 2102.00), + (34, 'Charles "Trip" Tucker III', 33, 2121.00), + (35, 'Malcolm Reed', 34, 2117.00), + (36, 'Hoshi Sato', 35, 2129.00), + (37, 'Travis Mayweather', 36, 2126.00), + (38, 'Michael Burnham', 37, 2226.00), + (39, 'Saru', 38, 2237.00), + (40, 'Paul Stamets', 39, 2246.00), + (41, 'Sylvia Tilly', 40, 2249.00), + (42, 'Hugh Culber', 41, 2246.00), + -- Strange New Worlds characters + (43, 'Christopher Pike', 42, 2219.00), + (44, 'Spock (SNW)', 43, 2230.06), + (45, 'Una Chin-Riley', 44, 2220.00), + (46, 'Nyota Uhura (SNW)', 45, 2233.00), + (47, 'Christine Chapel', 46, 2230.00), + -- Lower Decks characters + (48, 'Beckett Mariner', 47, 2357.00), + (49, 'Brad Boimler', 48, 2361.00), + (50, 'D''Vana Tendi', 49, 2368.00), + (51, 'Sam Rutherford', 50, 2359.00), + (52, 'Carol Freeman', 51, 2337.00), + -- Prodigy characters + (53, 'Dal R''El', 52, 2369.00), + (54, 'Gwyndala', 53, 2369.00), + (55, 'Jankom Pog', 54, 2368.00), + (56, 'Zero', 55, 2370.00), + (57, 'Murf', 56, 2371.00), + -- Starfleet Academy characters + (58, 'Admiral Grace', 57, 2328.00), + (59, 'Chancellor Vix', 58, 2335.00), + (60, 'Nyota Uhura (Academy)', 59, 2233.00), + (61, 'Cadet Sato', 60, 2377.00), + (62, 'Cadet Thira Sidhu', 61, 2378.00), + -- Section 31 characters + (63, 'Philippa Georgiou (Section 31)', 62, 2202.00), + (64, 'Alok Sahar', 63, 1970.00), + (65, 'Quasi', 64, 2335.00), + (66, 'Zeph', 65, 2330.00), + (67, 'Rachel Garrett (Young)', 66, 2305.00); + +INSERT INTO Series_Character (SeriesId, CharacterId, Role) VALUES + -- Star Trek (Original Series) + (1, 1, 'Captain'), + (1, 2, 'First Officer/Science Officer'), + (1, 3, 'Chief Medical Officer'), + (1, 4, 'Chief Engineer'), + (1, 5, 'Communications Officer'), + (1, 6, 'Helmsman'), + (1, 7, 'Navigator'), + -- Star Trek: The Next Generation + (2, 8, 'Captain'), + (2, 9, 'First Officer'), + (2, 10, 'Operations Officer'), + (2, 11, 'Chief of Security/Tactical Officer'), + (2, 12, 'Chief Medical Officer'), + (2, 13, 'Ship''s Counselor'), + (2, 14, 'Chief Engineer'), + -- Star Trek: Voyager + (3, 15, 'Captain'), + (3, 16, 'First Officer'), + (3, 17, 'Chief of Security/Tactical Officer'), + (3, 18, 'Chief Engineer'), + (3, 19, 'Helmsman'), + (3, 20, 'Operations Officer'), + (3, 21, 'Chief Medical Officer'), + (3, 22, 'Astrometrics Officer'), + -- Star Trek: Deep Space Nine + (4, 23, 'Commanding Officer'), + (4, 24, 'First Officer'), + (4, 25, 'Chief of Security'), + (4, 26, 'Science Officer'), + (4, 27, 'Science Officer'), + (4, 28, 'Chief Medical Officer'), + (4, 29, 'Bar Owner'), + (4, 30, 'Civilian'), + (4, 11, 'Strategic Operations Officer'), -- Worf also served on DS9 + -- Star Trek: Enterprise + (5, 31, 'Captain'), + (5, 32, 'Science Officer/First Officer'), + (5, 33, 'Chief Medical Officer'), + (5, 34, 'Chief Engineer'), + (5, 35, 'Armory Officer'), + (5, 36, 'Communications Officer'), + (5, 37, 'Helmsman'), + -- Star Trek: Discovery + (6, 38, 'Science Specialist/First Officer'), + (6, 39, 'First Officer/Captain'), + (6, 40, 'Chief Engineer'), + (6, 41, 'Ensign/Cadet'), + (6, 42, 'Chief Medical Officer'), + -- Star Trek: Strange New Worlds + (7, 43, 'Captain'), + (7, 44, 'Science Officer'), + (7, 45, 'First Officer'), + (7, 46, 'Communications Officer'), + (7, 47, 'Nurse'), + -- Star Trek: Lower Decks + (8, 48, 'Ensign'), + (8, 49, 'Ensign'), + (8, 50, 'Ensign'), + (8, 51, 'Ensign'), + (8, 52, 'Captain'), + -- Star Trek: Prodigy + (9, 53, 'Crew Member'), + (9, 54, 'Crew Member'), + (9, 55, 'Crew Member'), + (9, 56, 'Crew Member'), + (9, 57, 'Crew Member'), + -- Star Trek: Starfleet Academy + (10, 58, 'Academy Commandant'), + (10, 59, 'Academy Chancellor'), + (10, 60, 'Cadet/Communications Specialist'), + (10, 61, 'First Year Cadet'), + (10, 62, 'First Year Cadet'), + -- Star Trek: Section 31 + (11, 63, 'Emperor/Section 31 Operative'), + (11, 64, 'Section 31 Agent'), + (11, 65, 'Section 31 Agent/Shapeshifter'), + (11, 66, 'Section 31 Agent'), + (11, 67, 'Starfleet Officer/Future Captain'); + +INSERT INTO Character_Species (CharacterId, SpeciesId) VALUES + -- Original Series + (1, 1), -- Kirk: Human + (2, 2), -- Spock: Vulcan + (2, 1), -- Spock: Human (half-human) + (3, 1), -- McCoy: Human + (4, 1), -- Scott: Human + (5, 1), -- Uhura: Human + (6, 1), -- Sulu: Human + (7, 1), -- Chekov: Human + -- Next Generation + (8, 1), -- Picard: Human + (9, 1), -- Riker: Human + (10, 3), -- Data: Android + (11, 4), -- Worf: Klingon + (12, 1), -- Crusher: Human + (13, 5), -- Troi: Betazoid + (13, 1), -- Troi: Human (half-human) + (14, 1), -- La Forge: Human + -- Voyager + (15, 1), -- Janeway: Human + (16, 1), -- Chakotay: Human + (17, 2), -- Tuvok: Vulcan + (18, 4), -- Torres: Klingon + (18, 1), -- Torres: Human (half-human) + (19, 1), -- Paris: Human + (20, 1), -- Kim: Human + (21, 6), -- The Doctor: Hologram + (22, 1), -- Seven of Nine: Human + (22, 12), -- Seven of Nine: Borg (ex-Borg) + -- Deep Space Nine + (23, 1), -- Sisko: Human + (24, 7), -- Kira: Bajoran + (25, 8), -- Odo: Changeling + (26, 1), -- Ezri Dax: Human + (26, 9), -- Ezri Dax: Trill (joined) + (27, 9), -- Jadzia Dax: Trill + (28, 1), -- Bashir: Human + (29, 10), -- Quark: Ferengi + (30, 1), -- Jake Sisko: Human + -- Enterprise + (31, 1), -- Archer: Human + (32, 2), -- T'Pol: Vulcan + (33, 11), -- Phlox: Denobulan + (34, 1), -- Tucker: Human + (35, 1), -- Reed: Human + (36, 1), -- Sato: Human + (37, 1), -- Mayweather: Human + -- Discovery + (38, 1), -- Burnham: Human + (39, 13), -- Saru: Kelpien + (40, 1), -- Stamets: Human + (41, 1), -- Tilly: Human + (42, 1), -- Culber: Human + -- Strange New Worlds + (43, 1), -- Pike: Human + (44, 2), -- Spock: Vulcan + (44, 1), -- Spock: Human (half-human) + (45, 1), -- Una: Human + (45, 14), -- Una: Illyrian (augmented) + (46, 1), -- Uhura: Human + (47, 1), -- Chapel: Human + -- Lower Decks + (48, 1), -- Mariner: Human + (49, 1), -- Boimler: Human + (50, 1), -- Tendi: Human (Orion) + (51, 1), -- Rutherford: Human + (52, 1), -- Freeman: Human + -- Prodigy + (53, 1), -- Dal: Human (augment) + (54, 17), -- Gwyndala: Vau N'Akat + (55, 1), -- Jankom Pog: Human (Tellarite) + (56, 16), -- Zero: Medusan + (57, 1), -- Murf: Unknown (listed as Human for database purposes) + -- Starfleet Academy + (58, 1), -- Admiral Grace: Human + (59, 1), -- Chancellor Vix: Human + (60, 1), -- Uhura (Academy): Human + (61, 1), -- Sato: Human + (62, 1), -- Thira Sidhu: Human + -- Section 31 + (63, 1), -- Philippa Georgiou: Human + (64, 1), -- Alok Sahar: Human + (65, 19), -- Quasi: Chameloid + (66, 1), -- Zeph: Human + (67, 1); -- Rachel Garrett: Human + +GO +CREATE VIEW [dbo].[SeriesActors] +AS +SELECT + a.Id AS Id, + a.FullName AS Actor, + a.BirthYear AS BirthYear, + s.Id AS SeriesId, + s.Name AS Series +FROM Series s +JOIN Series_Character AS sc ON s.Id = sc.SeriesId +JOIN Character AS c ON sc.CharacterId = c.Id +JOIN Actor AS a ON c.ActorId = a.Id; + +GO +CREATE PROCEDURE [dbo].[GetSeriesActors] + @seriesId INT = 1, + @top INT = 5 +AS +SET NOCOUNT ON; +SELECT TOP (@top) * +FROM SeriesActors +WHERE SeriesId = @seriesId; + +GO +-- demo indexes for performance +CREATE INDEX IX_Character_ActorId ON Character(ActorId); + +GO +CREATE INDEX IX_SeriesCharacter_SeriesId ON Series_Character(SeriesId); + +GO +CREATE INDEX IX_CharacterSpecies_CharacterId ON Character_Species(CharacterId); diff --git a/samples/aspire/global.json b/samples/aspire/global.json new file mode 100644 index 0000000000..376af49c07 --- /dev/null +++ b/samples/aspire/global.json @@ -0,0 +1,5 @@ +{ + "sdk": { + "version": "10.0.100" + } +} diff --git a/samples/aspire/readme.md b/samples/aspire/readme.md new file mode 100644 index 0000000000..ff3157624f --- /dev/null +++ b/samples/aspire/readme.md @@ -0,0 +1,66 @@ +# Sample: Data API builder in Aspire + +To run this sample, install the prerequisites, then execute the following command in this directory: + +```sh +> aspire run +``` + +### Prerequisites + + - Dotnet CLI v10+ [Install](https://dotnet.microsoft.com/en-us/download/dotnet) Verify with `dotnet --version` + - Aspire CLI v13+ [Install](https://aspire.dev/get-started/install-cli/) Verify with `aspire --version` + - Docker Desktop (running) [Install](https://www.docker.com/products/docker-desktop/) + +## Resources + +```mermaid +flowchart TD + SQL["SQL Server"] + DAB["Data API Builder"] + CMD["SQL Commander"] + MCP["MCP Inspector"] + + SQL --> DAB + SQL --> CMD + DAB --> MCP +``` + +## Files + +- [`apphost.cs`](apphost.cs) - Single-page [Aspire orchestration](https://learn.microsoft.com/en-us/dotnet/aspire/app-host/configuration) +- [`database.sql`](database.sql) - Basic Star Trek schema & seed data +- [`dab-config.json`](dab-config.json) - Standard DAB configuration file +- [`dab-config.cmd`](dab-config.cmd) - [CLI commands](https://learn.microsoft.com/en-us/azure/data-api-builder/command-line/) to create `dab-config.json` (reference only) +- [`global.json`](global.json) - .NET SDK version settings + +## Data structures + +This schema demonstrates all of the relationship cardinalities. + +```mermaid +stateDiagram-v2 +direction LR + + classDef empty fill:none,stroke:none + classDef table stroke:black; + classDef phantom stroke:gray,stroke-dasharray:5 5; + + + class Actor table + class Character table + class Series table + class Species table + class Series_Character table + class Character_Species table + class Series_Character phantom + class Character_Species phantom + state Tables { + Actor --> Character + Character --> Actor + Series_Character --> Series + Character_Species --> Species + Series_Character --> Character + Character_Species --> Character + } +``` From dd724981a3114aa55ceaa121ccd375598fc3512e Mon Sep 17 00:00:00 2001 From: aaronburtle <93220300+aaronburtle@users.noreply.github.com> Date: Tue, 25 Nov 2025 21:27:03 -0800 Subject: [PATCH 13/36] Refactor response for Read and Update MCP tools to use built in utility functions (#2984) ## Why make this change? Closes https://github.com/Azure/data-api-builder/issues/2919 ## What is this change? Refactors the `Read` and `Update` built in MCP tools so that they use the common `BuildErrorResult` and `BuildSuccessResult` functiosn in the Utils, aligning their usage with the other tools. ## How was this tested? Manually tested using MCP Inspector tool, and run against normal test suite. * DESCRIBE_ENTITIES image * CREATE image * READ image * UPDATE image * DELETE image ## Sample Request(s) N/A --- .../BuiltInTools/CreateRecordTool.cs | 25 ++- .../BuiltInTools/DeleteRecordTool.cs | 39 ++-- .../BuiltInTools/DescribeEntitiesTool.cs | 9 + .../BuiltInTools/ExecuteEntityTool.cs | 47 +++-- .../BuiltInTools/ReadRecordsTool.cs | 146 +++----------- .../BuiltInTools/UpdateRecordTool.cs | 180 ++++-------------- .../Utils/McpResponseBuilder.cs | 18 ++ 7 files changed, 165 insertions(+), 299 deletions(-) diff --git a/src/Azure.DataApiBuilder.Mcp/BuiltInTools/CreateRecordTool.cs b/src/Azure.DataApiBuilder.Mcp/BuiltInTools/CreateRecordTool.cs index 68447f16f4..9d64fd4cd7 100644 --- a/src/Azure.DataApiBuilder.Mcp/BuiltInTools/CreateRecordTool.cs +++ b/src/Azure.DataApiBuilder.Mcp/BuiltInTools/CreateRecordTool.cs @@ -57,20 +57,22 @@ public async Task ExecuteAsync( CancellationToken cancellationToken = default) { ILogger? logger = serviceProvider.GetService>(); + string toolName = GetToolMetadata().Name; if (arguments == null) { - return Utils.McpResponseBuilder.BuildErrorResult("Invalid Arguments", "No arguments provided", logger); + return Utils.McpResponseBuilder.BuildErrorResult(toolName, "Invalid Arguments", "No arguments provided", logger); } RuntimeConfigProvider runtimeConfigProvider = serviceProvider.GetRequiredService(); if (!runtimeConfigProvider.TryGetConfig(out RuntimeConfig? runtimeConfig)) { - return Utils.McpResponseBuilder.BuildErrorResult("Invalid Configuration", "Runtime configuration not available", logger); + return Utils.McpResponseBuilder.BuildErrorResult(toolName, "Invalid Configuration", "Runtime configuration not available", logger); } if (runtimeConfig.McpDmlTools?.CreateRecord != true) { return Utils.McpResponseBuilder.BuildErrorResult( + toolName, "ToolDisabled", "The create_record tool is disabled in the configuration.", logger); @@ -84,13 +86,13 @@ public async Task ExecuteAsync( if (!root.TryGetProperty("entity", out JsonElement entityElement) || !root.TryGetProperty("data", out JsonElement dataElement)) { - return Utils.McpResponseBuilder.BuildErrorResult("InvalidArguments", "Missing required arguments 'entity' or 'data'", logger); + return Utils.McpResponseBuilder.BuildErrorResult(toolName, "InvalidArguments", "Missing required arguments 'entity' or 'data'", logger); } string entityName = entityElement.GetString() ?? string.Empty; if (string.IsNullOrWhiteSpace(entityName)) { - return Utils.McpResponseBuilder.BuildErrorResult("InvalidArguments", "Entity name cannot be empty", logger); + return Utils.McpResponseBuilder.BuildErrorResult(toolName, "InvalidArguments", "Entity name cannot be empty", logger); } string dataSourceName; @@ -100,7 +102,7 @@ public async Task ExecuteAsync( } catch (Exception) { - return Utils.McpResponseBuilder.BuildErrorResult("InvalidConfiguration", $"Entity '{entityName}' not found in configuration", logger); + return Utils.McpResponseBuilder.BuildErrorResult(toolName, "InvalidConfiguration", $"Entity '{entityName}' not found in configuration", logger); } IMetadataProviderFactory metadataProviderFactory = serviceProvider.GetRequiredService(); @@ -113,7 +115,7 @@ public async Task ExecuteAsync( } catch (Exception) { - return Utils.McpResponseBuilder.BuildErrorResult("InvalidConfiguration", $"Database object for entity '{entityName}' not found", logger); + return Utils.McpResponseBuilder.BuildErrorResult(toolName, "InvalidConfiguration", $"Database object for entity '{entityName}' not found", logger); } // Create an HTTP context for authorization @@ -123,13 +125,13 @@ public async Task ExecuteAsync( if (httpContext is null || !authorizationResolver.IsValidRoleContext(httpContext)) { - return Utils.McpResponseBuilder.BuildErrorResult("PermissionDenied", "Permission denied: Unable to resolve a valid role context for update operation.", logger); + return Utils.McpResponseBuilder.BuildErrorResult(toolName, "PermissionDenied", "Permission denied: Unable to resolve a valid role context for update operation.", logger); } // Validate that we have at least one role authorized for create if (!TryResolveAuthorizedRole(httpContext, authorizationResolver, entityName, out string authError)) { - return Utils.McpResponseBuilder.BuildErrorResult("PermissionDenied", authError, logger); + return Utils.McpResponseBuilder.BuildErrorResult(toolName, "PermissionDenied", authError, logger); } JsonElement insertPayloadRoot = dataElement.Clone(); @@ -150,12 +152,13 @@ public async Task ExecuteAsync( } catch (Exception ex) { - return Utils.McpResponseBuilder.BuildErrorResult("ValidationFailed", $"Request validation failed: {ex.Message}", logger); + return Utils.McpResponseBuilder.BuildErrorResult(toolName, "ValidationFailed", $"Request validation failed: {ex.Message}", logger); } } else { return Utils.McpResponseBuilder.BuildErrorResult( + toolName, "InvalidCreateTarget", "The create_record tool is only available for tables.", logger); @@ -185,6 +188,7 @@ public async Task ExecuteAsync( if (isError) { return Utils.McpResponseBuilder.BuildErrorResult( + toolName, "CreateFailed", $"Failed to create record in entity '{entityName}'. Error: {JsonSerializer.Serialize(objectResult.Value)}", logger); @@ -207,6 +211,7 @@ public async Task ExecuteAsync( if (result is null) { return Utils.McpResponseBuilder.BuildErrorResult( + toolName, "UnexpectedError", $"Mutation engine returned null result for entity '{entityName}'", logger); @@ -226,7 +231,7 @@ public async Task ExecuteAsync( } catch (Exception ex) { - return Utils.McpResponseBuilder.BuildErrorResult("Error", $"Error: {ex.Message}", logger); + return Utils.McpResponseBuilder.BuildErrorResult(toolName, "Error", $"Error: {ex.Message}", logger); } } diff --git a/src/Azure.DataApiBuilder.Mcp/BuiltInTools/DeleteRecordTool.cs b/src/Azure.DataApiBuilder.Mcp/BuiltInTools/DeleteRecordTool.cs index 7abac888c5..eb310ae364 100644 --- a/src/Azure.DataApiBuilder.Mcp/BuiltInTools/DeleteRecordTool.cs +++ b/src/Azure.DataApiBuilder.Mcp/BuiltInTools/DeleteRecordTool.cs @@ -73,6 +73,7 @@ public async Task ExecuteAsync( CancellationToken cancellationToken = default) { ILogger? logger = serviceProvider.GetService>(); + string toolName = GetToolMetadata().Name; try { @@ -87,6 +88,7 @@ public async Task ExecuteAsync( if (config.McpDmlTools?.DeleteRecord != true) { return McpResponseBuilder.BuildErrorResult( + toolName, "ToolDisabled", $"The {this.GetToolMetadata().Name} tool is disabled in the configuration.", logger); @@ -95,12 +97,12 @@ public async Task ExecuteAsync( // 3) Parsing & basic argument validation if (arguments is null) { - return McpResponseBuilder.BuildErrorResult("InvalidArguments", "No arguments provided.", logger); + return McpResponseBuilder.BuildErrorResult(toolName, "InvalidArguments", "No arguments provided.", logger); } if (!McpArgumentParser.TryParseEntityAndKeys(arguments.RootElement, out string entityName, out Dictionary keys, out string parseError)) { - return McpResponseBuilder.BuildErrorResult("InvalidArguments", parseError, logger); + return McpResponseBuilder.BuildErrorResult(toolName, "InvalidArguments", parseError, logger); } IMetadataProviderFactory metadataProviderFactory = serviceProvider.GetRequiredService(); @@ -117,18 +119,18 @@ public async Task ExecuteAsync( } catch (Exception) { - return McpResponseBuilder.BuildErrorResult("EntityNotFound", $"Entity '{entityName}' is not defined in the configuration.", logger); + return McpResponseBuilder.BuildErrorResult(toolName, "EntityNotFound", $"Entity '{entityName}' is not defined in the configuration.", logger); } if (!sqlMetadataProvider.EntityToDatabaseObject.TryGetValue(entityName, out DatabaseObject? dbObject) || dbObject is null) { - return McpResponseBuilder.BuildErrorResult("EntityNotFound", $"Entity '{entityName}' is not defined in the configuration.", logger); + return McpResponseBuilder.BuildErrorResult(toolName, "EntityNotFound", $"Entity '{entityName}' is not defined in the configuration.", logger); } // Validate it's a table or view if (dbObject.SourceType != EntitySourceType.Table && dbObject.SourceType != EntitySourceType.View) { - return McpResponseBuilder.BuildErrorResult("InvalidEntity", $"Entity '{entityName}' is not a table or view. Use 'execute-entity' for stored procedures.", logger); + return McpResponseBuilder.BuildErrorResult(toolName, "InvalidEntity", $"Entity '{entityName}' is not a table or view. Use 'execute-entity' for stored procedures.", logger); } // 5) Authorization @@ -138,7 +140,7 @@ public async Task ExecuteAsync( if (!McpAuthorizationHelper.ValidateRoleContext(httpContext, authResolver, out string roleError)) { - return McpResponseBuilder.BuildErrorResult("PermissionDenied", $"Permission denied: {roleError}", logger); + return McpResponseBuilder.BuildErrorResult(toolName, "PermissionDenied", $"Permission denied: {roleError}", logger); } if (!McpAuthorizationHelper.TryResolveAuthorizedRole( @@ -149,7 +151,7 @@ public async Task ExecuteAsync( out string? effectiveRole, out string authError)) { - return McpResponseBuilder.BuildErrorResult("PermissionDenied", $"Permission denied: {authError}", logger); + return McpResponseBuilder.BuildErrorResult(toolName, "PermissionDenied", $"Permission denied: {authError}", logger); } // 6) Build and validate Delete context @@ -164,7 +166,7 @@ public async Task ExecuteAsync( { if (kvp.Value is null) { - return McpResponseBuilder.BuildErrorResult("InvalidArguments", $"Primary key value for '{kvp.Key}' cannot be null.", logger); + return McpResponseBuilder.BuildErrorResult(toolName, "InvalidArguments", $"Primary key value for '{kvp.Key}' cannot be null.", logger); } context.PrimaryKeyValuePairs[kvp.Key] = kvp.Value; @@ -195,6 +197,7 @@ public async Task ExecuteAsync( { string keyDetails = McpJsonHelper.FormatKeyDetails(keys); return McpResponseBuilder.BuildErrorResult( + toolName, "RecordNotFound", $"No record found with the specified primary key: {keyDetails}", logger); @@ -203,6 +206,7 @@ public async Task ExecuteAsync( message.Contains("REFERENCE constraint", StringComparison.OrdinalIgnoreCase)) { return McpResponseBuilder.BuildErrorResult( + toolName, "ConstraintViolation", "Cannot delete record due to foreign key constraint. Other records depend on this record.", logger); @@ -211,6 +215,7 @@ public async Task ExecuteAsync( message.Contains("authorization", StringComparison.OrdinalIgnoreCase)) { return McpResponseBuilder.BuildErrorResult( + toolName, "PermissionDenied", "You do not have permission to delete this record.", logger); @@ -219,6 +224,7 @@ public async Task ExecuteAsync( message.Contains("type", StringComparison.OrdinalIgnoreCase)) { return McpResponseBuilder.BuildErrorResult( + toolName, "InvalidArguments", "Invalid data type for one or more key values.", logger); @@ -226,6 +232,7 @@ public async Task ExecuteAsync( // For any other DAB exceptions, return the message as-is return McpResponseBuilder.BuildErrorResult( + toolName, "DataApiBuilderError", dabEx.Message, logger); @@ -242,7 +249,7 @@ public async Task ExecuteAsync( 208 => $"Table '{dbObject.FullName}' not found in the database.", _ => $"Database error: {sqlEx.Message}" }; - return McpResponseBuilder.BuildErrorResult("DatabaseError", errorMessage, logger); + return McpResponseBuilder.BuildErrorResult(toolName, "DatabaseError", errorMessage, logger); } catch (DbException dbEx) { @@ -254,6 +261,7 @@ public async Task ExecuteAsync( if (errorMsg.Contains("foreign key") || errorMsg.Contains("constraint")) { return McpResponseBuilder.BuildErrorResult( + toolName, "ConstraintViolation", "Cannot delete record due to foreign key constraint. Other records depend on this record.", logger); @@ -261,24 +269,25 @@ public async Task ExecuteAsync( else if (errorMsg.Contains("not found") || errorMsg.Contains("does not exist")) { return McpResponseBuilder.BuildErrorResult( + toolName, "RecordNotFound", "No record found with the specified primary key.", logger); } - return McpResponseBuilder.BuildErrorResult("DatabaseError", $"Database error: {dbEx.Message}", logger); + return McpResponseBuilder.BuildErrorResult(toolName, "DatabaseError", $"Database error: {dbEx.Message}", logger); } catch (InvalidOperationException ioEx) when (ioEx.Message.Contains("connection", StringComparison.OrdinalIgnoreCase)) { // Handle connection-related issues logger?.LogError(ioEx, "Database connection error"); - return McpResponseBuilder.BuildErrorResult("ConnectionError", "Failed to connect to the database.", logger); + return McpResponseBuilder.BuildErrorResult(toolName, "ConnectionError", "Failed to connect to the database.", logger); } catch (TimeoutException timeoutEx) { // Handle query timeout logger?.LogError(timeoutEx, "Delete operation timeout for {Entity}", entityName); - return McpResponseBuilder.BuildErrorResult("TimeoutError", "The delete operation timed out.", logger); + return McpResponseBuilder.BuildErrorResult(toolName, "TimeoutError", "The delete operation timed out.", logger); } catch (Exception ex) { @@ -289,6 +298,7 @@ public async Task ExecuteAsync( { string keyDetails = McpJsonHelper.FormatKeyDetails(keys); return McpResponseBuilder.BuildErrorResult( + toolName, "RecordNotFound", $"No entity found with the given key {keyDetails}.", logger); @@ -325,11 +335,11 @@ public async Task ExecuteAsync( } catch (OperationCanceledException) { - return McpResponseBuilder.BuildErrorResult("OperationCanceled", "The delete operation was canceled.", logger); + return McpResponseBuilder.BuildErrorResult(toolName, "OperationCanceled", "The delete operation was canceled.", logger); } catch (ArgumentException argEx) { - return McpResponseBuilder.BuildErrorResult("InvalidArguments", argEx.Message, logger); + return McpResponseBuilder.BuildErrorResult(toolName, "InvalidArguments", argEx.Message, logger); } catch (Exception ex) { @@ -337,6 +347,7 @@ public async Task ExecuteAsync( innerLogger?.LogError(ex, "Unexpected error in DeleteRecordTool."); return McpResponseBuilder.BuildErrorResult( + toolName, "UnexpectedError", "An unexpected error occurred during the delete operation.", logger); diff --git a/src/Azure.DataApiBuilder.Mcp/BuiltInTools/DescribeEntitiesTool.cs b/src/Azure.DataApiBuilder.Mcp/BuiltInTools/DescribeEntitiesTool.cs index 154b37ee80..b8c7d975a2 100644 --- a/src/Azure.DataApiBuilder.Mcp/BuiltInTools/DescribeEntitiesTool.cs +++ b/src/Azure.DataApiBuilder.Mcp/BuiltInTools/DescribeEntitiesTool.cs @@ -67,6 +67,7 @@ public Task ExecuteAsync( CancellationToken cancellationToken = default) { ILogger? logger = serviceProvider.GetService>(); + string toolName = GetToolMetadata().Name; try { @@ -78,6 +79,7 @@ public Task ExecuteAsync( if (!IsToolEnabled(runtimeConfig)) { return Task.FromResult(McpResponseBuilder.BuildErrorResult( + toolName, "ToolDisabled", $"The {GetToolMetadata().Name} tool is disabled in the configuration.", logger)); @@ -158,6 +160,7 @@ public Task ExecuteAsync( if (entityFilter != null && entityFilter.Count > 0) { return Task.FromResult(McpResponseBuilder.BuildErrorResult( + toolName, "EntitiesNotFound", $"No entities found matching the filter: {string.Join(", ", entityFilter)}", logger)); @@ -165,6 +168,7 @@ public Task ExecuteAsync( else { return Task.FromResult(McpResponseBuilder.BuildErrorResult( + toolName, "NoEntitiesConfigured", "No entities are configured in the runtime configuration.", logger)); @@ -197,6 +201,7 @@ public Task ExecuteAsync( catch (OperationCanceledException) { return Task.FromResult(McpResponseBuilder.BuildErrorResult( + toolName, "OperationCanceled", "The describe operation was canceled.", logger)); @@ -205,6 +210,7 @@ public Task ExecuteAsync( { logger?.LogError(dabEx, "Data API Builder error in DescribeEntitiesTool"); return Task.FromResult(McpResponseBuilder.BuildErrorResult( + toolName, "DataApiBuilderError", dabEx.Message, logger)); @@ -212,6 +218,7 @@ public Task ExecuteAsync( catch (ArgumentException argEx) { return Task.FromResult(McpResponseBuilder.BuildErrorResult( + toolName, "InvalidArguments", argEx.Message, logger)); @@ -220,6 +227,7 @@ public Task ExecuteAsync( { logger?.LogError(ioEx, "Invalid operation in DescribeEntitiesTool"); return Task.FromResult(McpResponseBuilder.BuildErrorResult( + toolName, "InvalidOperation", "Failed to retrieve entity metadata: " + ioEx.Message, logger)); @@ -228,6 +236,7 @@ public Task ExecuteAsync( { logger?.LogError(ex, "Unexpected error in DescribeEntitiesTool"); return Task.FromResult(McpResponseBuilder.BuildErrorResult( + toolName, "UnexpectedError", "An unexpected error occurred while describing entities.", logger)); diff --git a/src/Azure.DataApiBuilder.Mcp/BuiltInTools/ExecuteEntityTool.cs b/src/Azure.DataApiBuilder.Mcp/BuiltInTools/ExecuteEntityTool.cs index be2fa7af36..6b0bc28383 100644 --- a/src/Azure.DataApiBuilder.Mcp/BuiltInTools/ExecuteEntityTool.cs +++ b/src/Azure.DataApiBuilder.Mcp/BuiltInTools/ExecuteEntityTool.cs @@ -73,6 +73,7 @@ public async Task ExecuteAsync( CancellationToken cancellationToken = default) { ILogger? logger = serviceProvider.GetService>(); + string toolName = GetToolMetadata().Name; try { @@ -87,26 +88,27 @@ public async Task ExecuteAsync( if (config.McpDmlTools?.ExecuteEntity != true) { return McpResponseBuilder.BuildErrorResult( + toolName, "ToolDisabled", - $"The {this.GetToolMetadata().Name} tool is disabled in the configuration.", + $"The {toolName} tool is disabled in the configuration.", logger); } // 3) Parsing & basic argument validation if (arguments is null) { - return McpResponseBuilder.BuildErrorResult("InvalidArguments", "No arguments provided.", logger); + return McpResponseBuilder.BuildErrorResult(toolName, "InvalidArguments", "No arguments provided.", logger); } if (!TryParseExecuteArguments(arguments.RootElement, out string entity, out Dictionary parameters, out string parseError)) { - return McpResponseBuilder.BuildErrorResult("InvalidArguments", parseError, logger); + return McpResponseBuilder.BuildErrorResult(toolName, "InvalidArguments", parseError, logger); } // Entity is required if (string.IsNullOrWhiteSpace(entity)) { - return McpResponseBuilder.BuildErrorResult("InvalidArguments", "Entity is required", logger); + return McpResponseBuilder.BuildErrorResult(toolName, "InvalidArguments", "Entity is required", logger); } IMetadataProviderFactory metadataProviderFactory = serviceProvider.GetRequiredService(); @@ -115,12 +117,12 @@ public async Task ExecuteAsync( // 4) Validate entity exists and is a stored procedure if (!config.Entities.TryGetValue(entity, out Entity? entityConfig)) { - return McpResponseBuilder.BuildErrorResult("EntityNotFound", $"Entity '{entity}' not found in configuration.", logger); + return McpResponseBuilder.BuildErrorResult(toolName, "EntityNotFound", $"Entity '{entity}' not found in configuration.", logger); } if (entityConfig.Source.Type != EntitySourceType.StoredProcedure) { - return McpResponseBuilder.BuildErrorResult("InvalidEntity", $"Entity {entity} cannot be executed.", logger); + return McpResponseBuilder.BuildErrorResult(toolName, "InvalidEntity", $"Entity {entity} cannot be executed.", logger); } // 5) Resolve metadata @@ -134,12 +136,12 @@ public async Task ExecuteAsync( } catch (Exception) { - return McpResponseBuilder.BuildErrorResult("EntityNotFound", $"Failed to resolve entity metadata for '{entity}'.", logger); + return McpResponseBuilder.BuildErrorResult(toolName, "EntityNotFound", $"Failed to resolve entity metadata for '{entity}'.", logger); } if (!sqlMetadataProvider.EntityToDatabaseObject.TryGetValue(entity, out DatabaseObject? dbObject) || dbObject is null) { - return McpResponseBuilder.BuildErrorResult("EntityNotFound", $"Failed to resolve database object for entity '{entity}'.", logger); + return McpResponseBuilder.BuildErrorResult(toolName, "EntityNotFound", $"Failed to resolve database object for entity '{entity}'.", logger); } // 6) Authorization - Never bypass permissions @@ -149,7 +151,7 @@ public async Task ExecuteAsync( if (!McpAuthorizationHelper.ValidateRoleContext(httpContext, authResolver, out string roleError)) { - return McpResponseBuilder.BuildErrorResult("PermissionDenied", roleError, logger); + return McpResponseBuilder.BuildErrorResult(toolName, "PermissionDenied", roleError, logger); } if (!McpAuthorizationHelper.TryResolveAuthorizedRole( @@ -160,7 +162,7 @@ public async Task ExecuteAsync( out string? effectiveRole, out string authError)) { - return McpResponseBuilder.BuildErrorResult("PermissionDenied", authError, logger); + return McpResponseBuilder.BuildErrorResult(toolName, "PermissionDenied", authError, logger); } // 7) Validate parameters against metadata @@ -171,7 +173,7 @@ public async Task ExecuteAsync( { if (!entityConfig.Source.Parameters.Any(p => p.Name == param.Key)) { - return McpResponseBuilder.BuildErrorResult("InvalidArguments", $"Invalid parameter: {param.Key}", logger); + return McpResponseBuilder.BuildErrorResult(toolName, "InvalidArguments", $"Invalid parameter: {param.Key}", logger); } } } @@ -241,6 +243,7 @@ public async Task ExecuteAsync( message.Contains("authorization", StringComparison.OrdinalIgnoreCase)) { return McpResponseBuilder.BuildErrorResult( + toolName, "PermissionDenied", "You do not have permission to execute this stored procedure.", logger); @@ -249,6 +252,7 @@ public async Task ExecuteAsync( message.Contains("type", StringComparison.OrdinalIgnoreCase)) { return McpResponseBuilder.BuildErrorResult( + toolName, "InvalidArguments", "Invalid data type for one or more parameters.", logger); @@ -256,6 +260,7 @@ public async Task ExecuteAsync( // For any other DAB exceptions, return the message as-is return McpResponseBuilder.BuildErrorResult( + toolName, "DataApiBuilderError", dabEx.Message, logger); @@ -273,48 +278,49 @@ public async Task ExecuteAsync( 229 or 262 => $"Permission denied to execute stored procedure '{entityConfig.Source.Object}'.", _ => $"Database error: {sqlEx.Message}" }; - return McpResponseBuilder.BuildErrorResult("DatabaseError", errorMessage, logger); + return McpResponseBuilder.BuildErrorResult(toolName, "DatabaseError", errorMessage, logger); } catch (DbException dbEx) { // Handle generic database exceptions (works for PostgreSQL, MySQL, etc.) logger?.LogError(dbEx, "Database error executing stored procedure {StoredProcedure}", entity); - return McpResponseBuilder.BuildErrorResult("DatabaseError", $"Database error: {dbEx.Message}", logger); + return McpResponseBuilder.BuildErrorResult(toolName, "DatabaseError", $"Database error: {dbEx.Message}", logger); } catch (InvalidOperationException ioEx) when (ioEx.Message.Contains("connection", StringComparison.OrdinalIgnoreCase)) { // Handle connection-related issues logger?.LogError(ioEx, "Database connection error"); - return McpResponseBuilder.BuildErrorResult("ConnectionError", "Failed to connect to the database.", logger); + return McpResponseBuilder.BuildErrorResult(toolName, "ConnectionError", "Failed to connect to the database.", logger); } catch (TimeoutException timeoutEx) { // Handle query timeout logger?.LogError(timeoutEx, "Stored procedure execution timeout for {StoredProcedure}", entity); - return McpResponseBuilder.BuildErrorResult("TimeoutError", "The stored procedure execution timed out.", logger); + return McpResponseBuilder.BuildErrorResult(toolName, "TimeoutError", "The stored procedure execution timed out.", logger); } catch (Exception ex) { // Generic database/execution errors logger?.LogError(ex, "Unexpected error executing stored procedure {StoredProcedure}", entity); - return McpResponseBuilder.BuildErrorResult("DatabaseError", "An error occurred while executing the stored procedure.", logger); + return McpResponseBuilder.BuildErrorResult(toolName, "DatabaseError", "An error occurred while executing the stored procedure.", logger); } // 11) Build response with execution result - return BuildExecuteSuccessResponse(entity, parameters, queryResult, logger); + return BuildExecuteSuccessResponse(toolName, entity, parameters, queryResult, logger); } catch (OperationCanceledException) { - return McpResponseBuilder.BuildErrorResult("OperationCanceled", "The execute operation was canceled.", logger); + return McpResponseBuilder.BuildErrorResult(toolName, "OperationCanceled", "The execute operation was canceled.", logger); } catch (ArgumentException argEx) { - return McpResponseBuilder.BuildErrorResult("InvalidArguments", argEx.Message, logger); + return McpResponseBuilder.BuildErrorResult(toolName, "InvalidArguments", argEx.Message, logger); } catch (Exception ex) { logger?.LogError(ex, "Unexpected error in ExecuteEntityTool."); return McpResponseBuilder.BuildErrorResult( + toolName, "UnexpectedError", "An unexpected error occurred during the execute operation.", logger); @@ -386,6 +392,7 @@ private static bool TryParseExecuteArguments( /// Builds a successful response for the execute operation. ///
private static CallToolResult BuildExecuteSuccessResponse( + string toolName, string entityName, Dictionary? parameters, IActionResult? queryResult, @@ -426,6 +433,7 @@ private static CallToolResult BuildExecuteSuccessResponse( else if (queryResult is BadRequestObjectResult badRequest) { return McpResponseBuilder.BuildErrorResult( + toolName, "BadRequest", badRequest.Value?.ToString() ?? "Bad request", logger); @@ -433,6 +441,7 @@ private static CallToolResult BuildExecuteSuccessResponse( else if (queryResult is UnauthorizedObjectResult) { return McpResponseBuilder.BuildErrorResult( + toolName, "PermissionDenied", "You do not have permission to execute this entity", logger); diff --git a/src/Azure.DataApiBuilder.Mcp/BuiltInTools/ReadRecordsTool.cs b/src/Azure.DataApiBuilder.Mcp/BuiltInTools/ReadRecordsTool.cs index 42b1f41ea0..7561e1ae54 100644 --- a/src/Azure.DataApiBuilder.Mcp/BuiltInTools/ReadRecordsTool.cs +++ b/src/Azure.DataApiBuilder.Mcp/BuiltInTools/ReadRecordsTool.cs @@ -15,6 +15,7 @@ using Azure.DataApiBuilder.Core.Services; using Azure.DataApiBuilder.Core.Services.MetadataProviders; using Azure.DataApiBuilder.Mcp.Model; +using Azure.DataApiBuilder.Mcp.Utils; using Azure.DataApiBuilder.Service.Exceptions; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; @@ -78,6 +79,7 @@ public async Task ExecuteAsync( CancellationToken cancellationToken = default) { ILogger? logger = serviceProvider.GetService>(); + string toolName = GetToolMetadata().Name; // Get runtime config RuntimeConfigProvider runtimeConfigProvider = serviceProvider.GetRequiredService(); @@ -85,7 +87,8 @@ public async Task ExecuteAsync( if (runtimeConfig.McpDmlTools?.ReadRecords is not true) { - return BuildErrorResult( + return McpResponseBuilder.BuildErrorResult( + toolName, "ToolDisabled", "The read_records tool is disabled in the configuration.", logger); @@ -105,14 +108,14 @@ public async Task ExecuteAsync( // Extract arguments if (arguments == null) { - return BuildErrorResult("InvalidArguments", "No arguments provided.", logger); + return McpResponseBuilder.BuildErrorResult(toolName, "InvalidArguments", "No arguments provided.", logger); } JsonElement root = arguments.RootElement; if (!root.TryGetProperty("entity", out JsonElement entityElement) || string.IsNullOrWhiteSpace(entityElement.GetString())) { - return BuildErrorResult("InvalidArguments", "Missing required argument 'entity'.", logger); + return McpResponseBuilder.BuildErrorResult(toolName, "InvalidArguments", "Missing required argument 'entity'.", logger); } entityName = entityElement.GetString()!; @@ -157,12 +160,12 @@ public async Task ExecuteAsync( } catch (Exception) { - return BuildErrorResult("EntityNotFound", $"Entity '{entityName}' is not defined in the configuration.", logger); + return McpResponseBuilder.BuildErrorResult(toolName, "EntityNotFound", $"Entity '{entityName}' is not defined in the configuration.", logger); } if (!sqlMetadataProvider.EntityToDatabaseObject.TryGetValue(entityName, out DatabaseObject? dbObject) || dbObject is null) { - return BuildErrorResult("EntityNotFound", $"Entity '{entityName}' is not defined in the configuration.", logger); + return McpResponseBuilder.BuildErrorResult(toolName, "EntityNotFound", $"Entity '{entityName}' is not defined in the configuration.", logger); } // Authorization check in the existing entity @@ -173,12 +176,12 @@ public async Task ExecuteAsync( if (httpContext is null || !authResolver.IsValidRoleContext(httpContext)) { - return BuildErrorResult("PermissionDenied", $"You do not have permission to read records for entity '{entityName}'.", logger); + return McpResponseBuilder.BuildErrorResult(toolName, "PermissionDenied", $"You do not have permission to read records for entity '{entityName}'.", logger); } if (!TryResolveAuthorizedRole(httpContext, authResolver, entityName, out string? effectiveRole, out string authError)) { - return BuildErrorResult("PermissionDenied", authError, logger); + return McpResponseBuilder.BuildErrorResult(toolName, "PermissionDenied", authError, logger); } // Build and validate Find context @@ -208,7 +211,7 @@ public async Task ExecuteAsync( { if (string.IsNullOrWhiteSpace(param)) { - return BuildErrorResult("InvalidArguments", "Parameters inside 'orderby' argument cannot be empty or null.", logger); + return McpResponseBuilder.BuildErrorResult(toolName, "InvalidArguments", "Parameters inside 'orderby' argument cannot be empty or null.", logger); } sortQueryString += $"{param}, "; @@ -230,7 +233,7 @@ public async Task ExecuteAsync( requirements: new[] { new ColumnsPermissionsRequirement() }); if (!authorizationResult.Succeeded) { - return BuildErrorResult("PermissionDenied", DataApiBuilderException.AUTHORIZATION_FAILURE, logger); + return McpResponseBuilder.BuildErrorResult(toolName, "PermissionDenied", DataApiBuilderException.AUTHORIZATION_FAILURE, logger); } // Execute @@ -240,34 +243,40 @@ public async Task ExecuteAsync( : SqlResponseHelpers.FormatFindResult(queryResult.RootElement.Clone(), context, metadataProviderFactory.GetMetadataProvider(dataSourceName), runtimeConfigProvider.GetConfig(), httpContext, true); // Normalize response - string rawPayloadJson = ExtractResultJson(actionResult); - JsonDocument result = JsonDocument.Parse(rawPayloadJson); + string rawPayloadJson = McpResponseBuilder.ExtractResultJson(actionResult); + using JsonDocument result = JsonDocument.Parse(rawPayloadJson); JsonElement queryRoot = result.RootElement; - return BuildSuccessResult( - entityName, - queryRoot.Clone(), - logger); + return McpResponseBuilder.BuildSuccessResult( + new Dictionary + { + ["entity"] = entityName, + ["result"] = queryRoot.Clone(), + ["message"] = $"Successfully read records for entity '{entityName}'" + }, + logger, + $"ReadRecordsTool success for entity {entityName}."); } catch (OperationCanceledException) { - return BuildErrorResult("OperationCanceled", "The read operation was canceled.", logger); + return McpResponseBuilder.BuildErrorResult(toolName, "OperationCanceled", "The read operation was canceled.", logger); } catch (DbException argEx) { - return BuildErrorResult("DatabaseOperationFailed", argEx.Message, logger); + return McpResponseBuilder.BuildErrorResult(toolName, "DatabaseOperationFailed", argEx.Message, logger); } catch (ArgumentException argEx) { - return BuildErrorResult("InvalidArguments", argEx.Message, logger); + return McpResponseBuilder.BuildErrorResult(toolName, "InvalidArguments", argEx.Message, logger); } catch (DataApiBuilderException argEx) { - return BuildErrorResult(argEx.StatusCode.ToString(), argEx.Message, logger); + return McpResponseBuilder.BuildErrorResult(toolName, argEx.StatusCode.ToString(), argEx.Message, logger); } - catch (Exception) + catch (Exception ex) { - return BuildErrorResult("UnexpectedError", "Unexpected error occurred in ReadRecordsTool.", logger); + logger?.LogError(ex, "Unexpected error in ReadRecordsTool."); + return McpResponseBuilder.BuildErrorResult(toolName, "UnexpectedError", "Unexpected error occurred in ReadRecordsTool.", logger); } } @@ -324,100 +333,5 @@ private static bool TryResolveAuthorizedRole( error = $"You do not have permission to read records for entity '{entityName}'."; return false; } - - /// - /// Returns a result from the query in the case that it was successfully ran. - /// - /// Name of the entity used in the request. - /// Query result from engine. - /// MCP logger that returns all logged events. - private static CallToolResult BuildSuccessResult( - string entityName, - JsonElement engineRootElement, - ILogger? logger) - { - // Build normalized response - Dictionary normalized = new() - { - ["status"] = "success", - ["result"] = engineRootElement // only requested values - }; - - string output = JsonSerializer.Serialize(normalized, new JsonSerializerOptions { WriteIndented = true }); - - logger?.LogInformation("ReadRecordsTool success for entity {Entity}.", entityName); - - return new CallToolResult - { - Content = new List - { - new TextContentBlock { Type = "text", Text = output } - } - }; - } - - /// - /// Returns an error if the query failed to run at any point. - /// - /// Type of error that is encountered. - /// Error message given to the user. - /// MCP logger that returns all logged events. - private static CallToolResult BuildErrorResult( - string errorType, - string message, - ILogger? logger) - { - Dictionary errorObj = new() - { - ["status"] = "error", - ["error"] = new Dictionary - { - ["type"] = errorType, - ["message"] = message - } - }; - - string output = JsonSerializer.Serialize(errorObj); - - logger?.LogError("ReadRecordsTool error {ErrorType}: {Message}", errorType, message); - - return new CallToolResult - { - Content = - [ - new TextContentBlock { Type = "text", Text = output } - ], - IsError = true - }; - } - - /// - /// Extracts a JSON string from a typical IActionResult. - /// Falls back to "{}" for unsupported/empty cases to avoid leaking internals. - /// - private static string ExtractResultJson(IActionResult? result) - { - switch (result) - { - case ObjectResult obj: - if (obj.Value is JsonElement je) - { - return je.GetRawText(); - } - - if (obj.Value is JsonDocument jd) - { - return jd.RootElement.GetRawText(); - } - - return JsonSerializer.Serialize(obj.Value ?? new object()); - - case ContentResult content: - return string.IsNullOrWhiteSpace(content.Content) ? "{}" : content.Content; - - default: - return "{}"; - } - } } } diff --git a/src/Azure.DataApiBuilder.Mcp/BuiltInTools/UpdateRecordTool.cs b/src/Azure.DataApiBuilder.Mcp/BuiltInTools/UpdateRecordTool.cs index 9e7d101fe6..ab8956b618 100644 --- a/src/Azure.DataApiBuilder.Mcp/BuiltInTools/UpdateRecordTool.cs +++ b/src/Azure.DataApiBuilder.Mcp/BuiltInTools/UpdateRecordTool.cs @@ -13,6 +13,7 @@ using Azure.DataApiBuilder.Core.Services; using Azure.DataApiBuilder.Core.Services.MetadataProviders; using Azure.DataApiBuilder.Mcp.Model; +using Azure.DataApiBuilder.Mcp.Utils; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; @@ -83,8 +84,7 @@ public async Task ExecuteAsync( CancellationToken cancellationToken = default) { ILogger? logger = serviceProvider.GetService>(); - - // 1) Resolve required services & configuration + string toolName = GetToolMetadata().Name; RuntimeConfigProvider runtimeConfigProvider = serviceProvider.GetRequiredService(); RuntimeConfig config = runtimeConfigProvider.GetConfig(); @@ -92,7 +92,8 @@ public async Task ExecuteAsync( // 2)Check if the tool is enabled in configuration before proceeding. if (config.McpDmlTools?.UpdateRecord != true) { - return BuildErrorResult( + return McpResponseBuilder.BuildErrorResult( + toolName, "ToolDisabled", "The update_record tool is disabled in the configuration.", logger); @@ -106,12 +107,12 @@ public async Task ExecuteAsync( // 3) Parsing & basic argument validation (entity, keys, fields) if (arguments is null) { - return BuildErrorResult("InvalidArguments", "No arguments provided.", logger); + return McpResponseBuilder.BuildErrorResult(toolName, "InvalidArguments", "No arguments provided.", logger); } if (!TryParseArguments(arguments.RootElement, out string entityName, out Dictionary keys, out Dictionary fields, out string parseError)) { - return BuildErrorResult("InvalidArguments", parseError, logger); + return McpResponseBuilder.BuildErrorResult(toolName, "InvalidArguments", parseError, logger); } IMetadataProviderFactory metadataProviderFactory = serviceProvider.GetRequiredService(); @@ -128,12 +129,12 @@ public async Task ExecuteAsync( } catch (Exception) { - return BuildErrorResult("EntityNotFound", $"Entity '{entityName}' is not defined in the configuration.", logger); + return McpResponseBuilder.BuildErrorResult(toolName, "EntityNotFound", $"Entity '{entityName}' is not defined in the configuration.", logger); } if (!sqlMetadataProvider.EntityToDatabaseObject.TryGetValue(entityName, out DatabaseObject? dbObject) || dbObject is null) { - return BuildErrorResult("EntityNotFound", $"Entity '{entityName}' is not defined in the configuration.", logger); + return McpResponseBuilder.BuildErrorResult(toolName, "EntityNotFound", $"Entity '{entityName}' is not defined in the configuration.", logger); } // 5) Authorization after we have a known entity @@ -143,12 +144,12 @@ public async Task ExecuteAsync( if (httpContext is null || !authResolver.IsValidRoleContext(httpContext)) { - return BuildErrorResult("PermissionDenied", "Permission denied: unable to resolve a valid role context for update operation.", logger); + return McpResponseBuilder.BuildErrorResult(toolName, "PermissionDenied", "Permission denied: unable to resolve a valid role context for update operation.", logger); } if (!TryResolveAuthorizedRoleHasPermission(httpContext, authResolver, entityName, out string? effectiveRole, out string authError)) { - return BuildErrorResult("PermissionDenied", $"Permission denied: {authError}", logger); + return McpResponseBuilder.BuildErrorResult(toolName, "PermissionDenied", $"Permission denied: {authError}", logger); } // 6) Build and validate Upsert (UpdateIncremental) context @@ -165,7 +166,7 @@ public async Task ExecuteAsync( { if (kvp.Value is null) { - return BuildErrorResult("InvalidArguments", $"Primary key value for '{kvp.Key}' cannot be null.", logger); + return McpResponseBuilder.BuildErrorResult(toolName, "InvalidArguments", $"Primary key value for '{kvp.Key}' cannot be null.", logger); } context.PrimaryKeyValuePairs[kvp.Key] = kvp.Value; @@ -193,7 +194,8 @@ public async Task ExecuteAsync( if (errorMsg.Contains("No Update could be performed, record not found", StringComparison.OrdinalIgnoreCase)) { - return BuildErrorResult( + return McpResponseBuilder.BuildErrorResult( + toolName, "InvalidArguments", "No record found with the given key.", logger); @@ -208,29 +210,47 @@ public async Task ExecuteAsync( cancellationToken.ThrowIfCancellationRequested(); // 8) Normalize response (success or engine error payload) - string rawPayloadJson = ExtractResultJson(mutationResult); + string rawPayloadJson = McpResponseBuilder.ExtractResultJson(mutationResult); using JsonDocument resultDoc = JsonDocument.Parse(rawPayloadJson); JsonElement root = resultDoc.RootElement; - return BuildSuccessResult( - entityName: entityName, - engineRootElement: root.Clone(), - logger: logger); + // Extract first item of value[] array (updated record) + Dictionary filteredResult = new(); + if (root.TryGetProperty("value", out JsonElement valueArray) && + valueArray.ValueKind == JsonValueKind.Array && + valueArray.GetArrayLength() > 0) + { + JsonElement firstItem = valueArray[0]; + foreach (JsonProperty prop in firstItem.EnumerateObject()) + { + filteredResult[prop.Name] = McpResponseBuilder.GetJsonValue(prop.Value); + } + } + + return McpResponseBuilder.BuildSuccessResult( + new Dictionary + { + ["entity"] = entityName, + ["result"] = filteredResult, + ["message"] = $"Successfully updated record in entity '{entityName}'" + }, + logger, + $"UpdateRecordTool success for entity {entityName}."); } catch (OperationCanceledException) { - return BuildErrorResult("OperationCanceled", "The update operation was canceled.", logger); + return McpResponseBuilder.BuildErrorResult(toolName, "OperationCanceled", "The update operation was canceled.", logger); } catch (ArgumentException argEx) { - return BuildErrorResult("InvalidArguments", argEx.Message, logger); + return McpResponseBuilder.BuildErrorResult(toolName, "InvalidArguments", argEx.Message, logger); } catch (Exception ex) { ILogger? innerLogger = serviceProvider.GetService>(); innerLogger?.LogError(ex, "Unexpected error in UpdateRecordTool."); - - return BuildErrorResult( + return McpResponseBuilder.BuildErrorResult( + toolName, "UnexpectedError", ex.Message ?? "An unexpected error occurred during the update operation.", logger); @@ -348,125 +368,5 @@ private static bool TryResolveAuthorizedRoleHasPermission( } #endregion - - #region Response Builders & Utilities - - private static CallToolResult BuildSuccessResult( - string entityName, - JsonElement engineRootElement, - ILogger? logger) - { - // Extract only requested keys and updated fields from engineRootElement - Dictionary filteredResult = new(); - - // Navigate to "value" array in the engine result - if (engineRootElement.TryGetProperty("value", out JsonElement valueArray) && - valueArray.ValueKind == JsonValueKind.Array && - valueArray.GetArrayLength() > 0) - { - JsonElement firstItem = valueArray[0]; - - // Include all properties from the result - foreach (JsonProperty prop in firstItem.EnumerateObject()) - { - filteredResult[prop.Name] = GetJsonValue(prop.Value); - } - } - - // Build normalized response - Dictionary normalized = new() - { - ["status"] = "success", - ["result"] = filteredResult - }; - - string output = JsonSerializer.Serialize(normalized, new JsonSerializerOptions { WriteIndented = true }); - - logger?.LogInformation("UpdateRecordTool success for entity {Entity}.", entityName); - - return new CallToolResult - { - Content = new List - { - new TextContentBlock { Type = "text", Text = output } - } - }; - } - - /// - /// Converts JsonElement to .NET object dynamically. - /// - private static object? GetJsonValue(JsonElement element) - { - return element.ValueKind switch - { - JsonValueKind.String => element.GetString(), - JsonValueKind.Number => element.TryGetInt64(out long l) ? l : element.GetDouble(), - JsonValueKind.True => true, - JsonValueKind.False => false, - JsonValueKind.Null => null, - _ => element.GetRawText() // fallback for arrays/objects - }; - } - - private static CallToolResult BuildErrorResult( - string errorType, - string message, - ILogger? logger) - { - Dictionary errorObj = new() - { - ["status"] = "error", - ["error"] = new Dictionary - { - ["type"] = errorType, - ["message"] = message - } - }; - - string output = JsonSerializer.Serialize(errorObj); - - logger?.LogWarning("UpdateRecordTool error {ErrorType}: {Message}", errorType, message); - - return new CallToolResult - { - Content = - [ - new TextContentBlock { Type = "text", Text = output } - ], - IsError = true - }; - } - - /// - /// Extracts a JSON string from a typical IActionResult. - /// Falls back to "{}" for unsupported/empty cases to avoid leaking internals. - /// - private static string ExtractResultJson(IActionResult? result) - { - switch (result) - { - case ObjectResult obj: - if (obj.Value is JsonElement je) - { - return je.GetRawText(); - } - - if (obj.Value is JsonDocument jd) - { - return jd.RootElement.GetRawText(); - } - - return JsonSerializer.Serialize(obj.Value ?? new object()); - - case ContentResult content: - return string.IsNullOrWhiteSpace(content.Content) ? "{}" : content.Content; - - default: - return "{}"; - } - } - - #endregion } } diff --git a/src/Azure.DataApiBuilder.Mcp/Utils/McpResponseBuilder.cs b/src/Azure.DataApiBuilder.Mcp/Utils/McpResponseBuilder.cs index afbccbda38..49cacef2c3 100644 --- a/src/Azure.DataApiBuilder.Mcp/Utils/McpResponseBuilder.cs +++ b/src/Azure.DataApiBuilder.Mcp/Utils/McpResponseBuilder.cs @@ -43,12 +43,14 @@ public static CallToolResult BuildSuccessResult( /// Builds an error response for MCP tools. ///
public static CallToolResult BuildErrorResult( + string toolName, string errorType, string message, ILogger? logger = null) { Dictionary errorObj = new() { + ["toolName"] = toolName, ["status"] = "error", ["error"] = new Dictionary { @@ -99,5 +101,21 @@ public static string ExtractResultJson(IActionResult? result) return "{}"; } } + + /// + /// Extracts value from a JsonElement. + /// + public static object? GetJsonValue(JsonElement element) + { + return element.ValueKind switch + { + JsonValueKind.String => element.GetString(), + JsonValueKind.Number => element.TryGetInt64(out long l) ? l : element.GetDouble(), + JsonValueKind.True => true, + JsonValueKind.False => false, + JsonValueKind.Null => null, + _ => element.GetRawText() + }; + } } } From 499cd24020855f992f2d73b1c113547221c4deb0 Mon Sep 17 00:00:00 2001 From: aaronburtle <93220300+aaronburtle@users.noreply.github.com> Date: Wed, 26 Nov 2025 14:08:05 -0800 Subject: [PATCH 14/36] Support for .akv files to AKV variable replacement. (#2957) ## Why make this change? Closes https://github.com/Azure/data-api-builder/issues/2748 ## What is this change? Adds the option to use a local .akv file instead of Azure Key Vault for @AKV('') replacement in the config file during deserialization. Similar to how we handle .env files. ## How was this tested? A new test was added that verifies we are able to do the replacement and get the correct resultant configuration. --------- Co-authored-by: Aniruddh Munde --- ...erializationVariableReplacementSettings.cs | 79 +++++++- ...untimeConfigLoaderJsonDeserializerTests.cs | 178 +++++++++++++++++- 2 files changed, 254 insertions(+), 3 deletions(-) diff --git a/src/Config/DeserializationVariableReplacementSettings.cs b/src/Config/DeserializationVariableReplacementSettings.cs index d4c02b5252..5c70f4082b 100644 --- a/src/Config/DeserializationVariableReplacementSettings.cs +++ b/src/Config/DeserializationVariableReplacementSettings.cs @@ -8,6 +8,7 @@ using Azure.DataApiBuilder.Service.Exceptions; using Azure.Identity; using Azure.Security.KeyVault.Secrets; +using Microsoft.Extensions.Logging; namespace Azure.DataApiBuilder.Config { @@ -43,6 +44,8 @@ public class DeserializationVariableReplacementSettings private readonly AzureKeyVaultOptions? _azureKeyVaultOptions; private readonly SecretClient? _akvClient; + private readonly Dictionary? _akvFileSecrets; + private readonly ILogger? _logger; public Dictionary> ReplacementStrategies { get; private set; } = new(); @@ -50,12 +53,14 @@ public DeserializationVariableReplacementSettings( AzureKeyVaultOptions? azureKeyVaultOptions = null, bool doReplaceEnvVar = false, bool doReplaceAkvVar = false, - EnvironmentVariableReplacementFailureMode envFailureMode = EnvironmentVariableReplacementFailureMode.Throw) + EnvironmentVariableReplacementFailureMode envFailureMode = EnvironmentVariableReplacementFailureMode.Throw, + ILogger? logger = null) { _azureKeyVaultOptions = azureKeyVaultOptions; DoReplaceEnvVar = doReplaceEnvVar; DoReplaceAkvVar = doReplaceAkvVar; EnvFailureMode = envFailureMode; + _logger = logger; if (DoReplaceEnvVar) { @@ -66,13 +71,68 @@ public DeserializationVariableReplacementSettings( if (DoReplaceAkvVar && _azureKeyVaultOptions is not null) { - _akvClient = CreateSecretClient(_azureKeyVaultOptions); + // Determine if endpoint points to a local .akv file. If so, load secrets from file; otherwise, use remote AKV. + if (IsLocalAkvFileEndpoint(_azureKeyVaultOptions.Endpoint)) + { + _akvFileSecrets = LoadAkvFileSecrets(_azureKeyVaultOptions.Endpoint!, _logger); + } + else + { + _akvClient = CreateSecretClient(_azureKeyVaultOptions); + } + ReplacementStrategies.Add( new Regex(OUTER_AKV_PATTERN, RegexOptions.Compiled), ReplaceAkvVariable); } } + // Checks if the endpoint is a path to a local .akv file. + private static bool IsLocalAkvFileEndpoint(string? endpoint) + => !string.IsNullOrWhiteSpace(endpoint) + && endpoint.EndsWith(".akv", StringComparison.OrdinalIgnoreCase) + && File.Exists(endpoint); + + // Loads key=value pairs from a .akv file, similar to .env style. Lines starting with '#' are comments. + private static Dictionary LoadAkvFileSecrets(string filePath, ILogger? logger = null) + { + Dictionary secrets = new(StringComparer.OrdinalIgnoreCase); + foreach (string rawLine in File.ReadAllLines(filePath)) + { + string line = rawLine.Trim(); + if (string.IsNullOrEmpty(line) || line.StartsWith('#')) + { + continue; + } + + int eqIndex = line.IndexOf('='); + if (eqIndex <= 0) + { + logger?.LogDebug("Ignoring malformed line in AKV secrets file {FilePath}: {Line}", filePath, rawLine); + continue; + } + + string key = line.Substring(0, eqIndex).Trim(); + string value = line[(eqIndex + 1)..].Trim(); + + // Remove optional surrounding quotes + if (value.Length >= 2 && ((value.StartsWith('"') && value.EndsWith('"')) || (value.StartsWith('\'') && value.EndsWith('\'')))) + { + value = value[1..^1]; + } + + if (!string.IsNullOrEmpty(key)) + { + if (!secrets.TryAdd(key, value)) + { + logger?.LogDebug("Duplicate key '{Key}' encountered in AKV secrets file {FilePath}. Skipping later value.", key, filePath); + } + } + } + + return secrets; + } + private string ReplaceEnvVariable(Match match) { // strips first and last characters, ie: '''hello'' --> ''hello' @@ -170,6 +230,15 @@ private static SecretClient CreateSecretClient(AzureKeyVaultOptions options) DataApiBuilderException.SubStatusCodes.ErrorInInitialization); } + // If endpoint is a local .akv file, we should not create a SecretClient. + if (IsLocalAkvFileEndpoint(options.Endpoint)) + { + throw new DataApiBuilderException( + "Attempted to create Azure Key Vault client for local .akv file endpoint.", + System.Net.HttpStatusCode.InternalServerError, + DataApiBuilderException.SubStatusCodes.ErrorInInitialization); + } + SecretClientOptions clientOptions = new(); if (options.RetryPolicy is not null) @@ -195,6 +264,12 @@ private static SecretClient CreateSecretClient(AzureKeyVaultOptions options) private string? GetAkvVariable(string name) { + // If using local .akv file secrets, return from dictionary. + if (_akvFileSecrets is not null) + { + return _akvFileSecrets.TryGetValue(name, out string? value) ? value : null; + } + if (_akvClient is null) { throw new InvalidOperationException("Azure Key Vault client is not initialized."); diff --git a/src/Service.Tests/UnitTests/RuntimeConfigLoaderJsonDeserializerTests.cs b/src/Service.Tests/UnitTests/RuntimeConfigLoaderJsonDeserializerTests.cs index 47629ca4c8..b990b96368 100644 --- a/src/Service.Tests/UnitTests/RuntimeConfigLoaderJsonDeserializerTests.cs +++ b/src/Service.Tests/UnitTests/RuntimeConfigLoaderJsonDeserializerTests.cs @@ -13,6 +13,7 @@ using Azure.DataApiBuilder.Config.Converters; using Azure.DataApiBuilder.Config.ObjectModel; using Azure.DataApiBuilder.Service.Exceptions; +using Microsoft.Data.SqlClient; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Azure.DataApiBuilder.Service.Tests.UnitTests @@ -240,6 +241,7 @@ public void CheckCommentParsingInConfigFile() /// but have the effect of default values when deserialized. /// It starts with a minimal config and incrementally /// adds the optional subproperties. At each step, tests for valid deserialization. + ///
[TestMethod] public void TestNullableOptionalProps() { @@ -431,7 +433,7 @@ public static string GetModifiedJsonString(string[] reps, string enumString) ""host"": { ""mode"": ""development"", ""cors"": { - ""origins"": [ """ + reps[++index % reps.Length] + @""", """ + reps[++index % reps.Length] + @""" ], + ""origins"": [ """ + reps[++index % reps.Length] + @""", """ + reps[++index % reps.Length] + @"""], ""allow-credentials"": true }, ""authentication"": { @@ -671,5 +673,179 @@ private static bool TryParseAndAssertOnDefaults(string json, out RuntimeConfig p #endregion Helper Functions record StubJsonType(string Foo); + + /// + /// Test to verify Azure Key Vault variable replacement from local .akv file. + /// + [TestMethod] + public void TestAkvVariableReplacementFromLocalFile() + { + // Arrange: create a temporary .akv secrets file + string akvFilePath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString() + ".akv"); + string secretConnectionString = "Server=tcp:127.0.0.1,1433;Persist Security Info=False;Trusted_Connection=True;TrustServerCertificate=True;MultipleActiveResultSets=False;Connection Timeout=5;"; + File.WriteAllText(akvFilePath, $"DBCONN={secretConnectionString}\nAPI_KEY=abcd\n# Comment line should be ignored\n MALFORMEDLINE \n"); + + // Escape backslashes for JSON + string escapedPath = akvFilePath.Replace("\\", "\\\\"); + + string jsonConfig = $$""" + { + "$schema": "https://github.com/Azure/data-api-builder/releases/download/vmajor.minor.patch-alpha/dab.draft.schema.json", + "data-source": { + "database-type": "mssql", + "connection-string": "@akv('DBCONN')" + }, + "azure-key-vault": { + "endpoint": "{{escapedPath}}" + }, + "entities": { } + } + """; + + try + { + // Act + DeserializationVariableReplacementSettings replacementSettings = new( + azureKeyVaultOptions: null, + doReplaceEnvVar: false, + doReplaceAkvVar: true); + bool parsed = RuntimeConfigLoader.TryParseConfig(jsonConfig, out RuntimeConfig config, replacementSettings: replacementSettings); + + // Assert + Assert.IsTrue(parsed, "Config should parse successfully with local AKV file replacement."); + Assert.IsNotNull(config, "Config should not be null."); + Assert.AreEqual(secretConnectionString, config.DataSource.ConnectionString, "Connection string should be replaced from AKV local file secret."); + } + finally + { + // Cleanup + if (File.Exists(akvFilePath)) + { + File.Delete(akvFilePath); + } + } + } + + /// + /// Validates that when an AKV secret's value itself contains an @env('...') pattern, it is NOT further resolved + /// because replacement only runs once per original JSON token. Demonstrates that nested env patterns inside + /// AKV secret values are left intact. + /// + [TestMethod] + public void TestAkvSecretValueContainingEnvPatternIsNotEnvExpanded() + { + string akvFilePath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString() + ".akv"); + // Valid MSSQL connection string which embeds an @env('env') pattern in the Database value. + // This pattern should NOT be expanded because replacement only runs once on the original JSON token (@akv('DBCONN')). + string secretValueWithEnvPattern = "Server=localhost;Database=@env('env');User Id=sa;Password=XXXX;"; + File.WriteAllText(akvFilePath, $"DBCONN={secretValueWithEnvPattern}\n"); + string escapedPath = akvFilePath.Replace("\\", "\\\\"); + + // Set env variable to prove it would be different if expansion occurred. + Environment.SetEnvironmentVariable("env", "SHOULD_NOT_APPEAR"); + + string jsonConfig = $$""" + { + "$schema": "https://github.com/Azure/data-api-builder/releases/download/vmajor.minor.patch-alpha/dab.draft.schema.json", + "data-source": { + "database-type": "mssql", + "connection-string": "@akv('DBCONN')" + }, + "azure-key-vault": { + "endpoint": "{{escapedPath}}" + }, + "entities": { } + } + """; + + try + { + DeserializationVariableReplacementSettings replacementSettings = new( + azureKeyVaultOptions: null, + doReplaceEnvVar: true, + doReplaceAkvVar: true); + bool parsed = RuntimeConfigLoader.TryParseConfig(jsonConfig, out RuntimeConfig config, replacementSettings: replacementSettings); + Assert.IsTrue(parsed, "Config should parse successfully."); + Assert.IsNotNull(config); + + string actual = config.DataSource.ConnectionString; + Assert.IsTrue(actual.Contains("@env('env')"), "Nested @env pattern inside AKV secret should remain unexpanded."); + Assert.IsFalse(actual.Contains("SHOULD_NOT_APPEAR"), "Env var value should not be expanded inside AKV secret."); + Assert.IsTrue(actual.Contains("Application Name="), "Application Name should be appended for MSSQL when env replacement is enabled."); + + var builderOriginal = new SqlConnectionStringBuilder(secretValueWithEnvPattern.Replace("Server=", "Data Source=").Replace("Database=", "Initial Catalog=")); + var builderActual = new SqlConnectionStringBuilder(actual); + Assert.AreEqual(builderOriginal["Data Source"], builderActual["Data Source"], "Server/Data Source should match."); + Assert.AreEqual(builderOriginal["Initial Catalog"], builderActual["Initial Catalog"], "Database/Initial Catalog should match (with env pattern retained)."); + Assert.AreEqual(builderOriginal["User ID"], builderActual["User ID"], "User Id should match."); + Assert.AreEqual(builderOriginal["Password"], builderActual["Password"], "Password should match."); + } + finally + { + if (File.Exists(akvFilePath)) + { + File.Delete(akvFilePath); + } + + Environment.SetEnvironmentVariable("env", null); + } + } + + /// + /// Validates two-pass replacement where an env var resolves to an AKV pattern which then resolves to the secret value. + /// connection-string = @env('env_variable'), env_variable value = @akv('DBCONN'), AKV secret DBCONN holds the final connection string. + /// + [TestMethod] + public void TestEnvVariableResolvingToAkvPatternIsExpandedInSecondPass() + { + string akvFilePath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString() + ".akv"); + string finalSecretValue = "Server=localhost;Database=Test;User Id=sa;Password=XXXX;"; + File.WriteAllText(akvFilePath, $"DBCONN={finalSecretValue}\n"); + string escapedPath = akvFilePath.Replace("\\", "\\\\"); + Environment.SetEnvironmentVariable("env_variable", "@akv('DBCONN')"); + + string jsonConfig = $$""" + { + "$schema": "https://github.com/Azure/data-api-builder/releases/download/vmajor.minor.patch-alpha/dab.draft.schema.json", + "data-source": { + "database-type": "mssql", + "connection-string": "@env('env_variable')" + }, + "azure-key-vault": { + "endpoint": "{{escapedPath}}" + }, + "entities": { } + } + """; + + try + { + DeserializationVariableReplacementSettings replacementSettings = new( + azureKeyVaultOptions: null, + doReplaceEnvVar: true, + doReplaceAkvVar: true); + bool parsed = RuntimeConfigLoader.TryParseConfig(jsonConfig, out RuntimeConfig config, replacementSettings: replacementSettings); + Assert.IsTrue(parsed, "Config should parse successfully."); + Assert.IsNotNull(config); + + string expected = RuntimeConfigLoader.GetConnectionStringWithApplicationName(finalSecretValue); + var builderExpected = new SqlConnectionStringBuilder(expected); + var builderActual = new SqlConnectionStringBuilder(config.DataSource.ConnectionString); + Assert.AreEqual(builderExpected["Data Source"], builderActual["Data Source"], "Data Source should match."); + Assert.AreEqual(builderExpected["Initial Catalog"], builderActual["Initial Catalog"], "Initial Catalog should match."); + Assert.AreEqual(builderExpected["User ID"], builderActual["User ID"], "User ID should match."); + Assert.AreEqual(builderExpected["Password"], builderActual["Password"], "Password should match."); + Assert.IsTrue(builderActual.ApplicationName?.Contains("dab_"), "Application Name should be appended including product identifier."); + } + finally + { + if (File.Exists(akvFilePath)) + { + File.Delete(akvFilePath); + } + + Environment.SetEnvironmentVariable("env_variable", null); + } + } } } From 555faaa5c45d3874b790909963f6e9a9fe8d7610 Mon Sep 17 00:00:00 2001 From: aaronburtle <93220300+aaronburtle@users.noreply.github.com> Date: Wed, 26 Nov 2025 15:39:44 -0800 Subject: [PATCH 15/36] Refactor common code shared amongst built in MCP tools (#2986) ## Why make this change? Closes https://github.com/Azure/data-api-builder/issues/2932 ## What is this change? Add helper class `McpMetadataHelper`, extend `McpArgumentParser`, and utilize `McpAuthorizationHelper` to factor out common code. We now do the initialization of the metadata, the parsing of arguments, and the authorization checks in these shared helper classes. ## How was this tested? With MCP Inspector and against the normal test suite. * DESCRIBE_ENTITIES image * CREATE image * READ image * UPDATE image * DELETE image ## Sample Request(s) N/A --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Souvik Ghosh --- .../BuiltInTools/CreateRecordTool.cs | 126 ++++--------- .../BuiltInTools/DeleteRecordTool.cs | 47 ++--- .../BuiltInTools/DescribeEntitiesTool.cs | 6 +- .../BuiltInTools/ExecuteEntityTool.cs | 86 ++------- .../BuiltInTools/ReadRecordsTool.cs | 125 ++++--------- .../BuiltInTools/UpdateRecordTool.cs | 173 +++--------------- .../Model/McpErrorCode.cs | 14 ++ .../Utils/McpArgumentParser.cs | 140 ++++++++++++-- .../Utils/McpErrorHelpers.cs | 28 +++ .../Utils/McpMetadataHelper.cs | 89 +++++++++ 10 files changed, 386 insertions(+), 448 deletions(-) create mode 100644 src/Azure.DataApiBuilder.Mcp/Model/McpErrorCode.cs create mode 100644 src/Azure.DataApiBuilder.Mcp/Utils/McpErrorHelpers.cs create mode 100644 src/Azure.DataApiBuilder.Mcp/Utils/McpMetadataHelper.cs diff --git a/src/Azure.DataApiBuilder.Mcp/BuiltInTools/CreateRecordTool.cs b/src/Azure.DataApiBuilder.Mcp/BuiltInTools/CreateRecordTool.cs index 9d64fd4cd7..1a944d115b 100644 --- a/src/Azure.DataApiBuilder.Mcp/BuiltInTools/CreateRecordTool.cs +++ b/src/Azure.DataApiBuilder.Mcp/BuiltInTools/CreateRecordTool.cs @@ -5,14 +5,13 @@ using Azure.DataApiBuilder.Auth; using Azure.DataApiBuilder.Config.DatabasePrimitives; using Azure.DataApiBuilder.Config.ObjectModel; -using Azure.DataApiBuilder.Core.Authorization; using Azure.DataApiBuilder.Core.Configurations; using Azure.DataApiBuilder.Core.Models; using Azure.DataApiBuilder.Core.Resolvers; using Azure.DataApiBuilder.Core.Resolvers.Factories; using Azure.DataApiBuilder.Core.Services; -using Azure.DataApiBuilder.Core.Services.MetadataProviders; using Azure.DataApiBuilder.Mcp.Model; +using Azure.DataApiBuilder.Mcp.Utils; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.DependencyInjection; @@ -60,22 +59,18 @@ public async Task ExecuteAsync( string toolName = GetToolMetadata().Name; if (arguments == null) { - return Utils.McpResponseBuilder.BuildErrorResult(toolName, "Invalid Arguments", "No arguments provided", logger); + return McpResponseBuilder.BuildErrorResult(toolName, "InvalidArguments", "No arguments provided.", logger); } RuntimeConfigProvider runtimeConfigProvider = serviceProvider.GetRequiredService(); if (!runtimeConfigProvider.TryGetConfig(out RuntimeConfig? runtimeConfig)) { - return Utils.McpResponseBuilder.BuildErrorResult(toolName, "Invalid Configuration", "Runtime configuration not available", logger); + return McpResponseBuilder.BuildErrorResult(toolName, "InvalidConfiguration", "Runtime configuration not available.", logger); } if (runtimeConfig.McpDmlTools?.CreateRecord != true) { - return Utils.McpResponseBuilder.BuildErrorResult( - toolName, - "ToolDisabled", - "The create_record tool is disabled in the configuration.", - logger); + return McpErrorHelpers.ToolDisabled(toolName, logger); } try @@ -83,39 +78,21 @@ public async Task ExecuteAsync( cancellationToken.ThrowIfCancellationRequested(); JsonElement root = arguments.RootElement; - if (!root.TryGetProperty("entity", out JsonElement entityElement) || - !root.TryGetProperty("data", out JsonElement dataElement)) + if (!McpArgumentParser.TryParseEntityAndData(root, out string entityName, out JsonElement dataElement, out string parseError)) { - return Utils.McpResponseBuilder.BuildErrorResult(toolName, "InvalidArguments", "Missing required arguments 'entity' or 'data'", logger); + return McpResponseBuilder.BuildErrorResult(toolName, "InvalidArguments", parseError, logger); } - string entityName = entityElement.GetString() ?? string.Empty; - if (string.IsNullOrWhiteSpace(entityName)) + if (!McpMetadataHelper.TryResolveMetadata( + entityName, + runtimeConfig, + serviceProvider, + out ISqlMetadataProvider sqlMetadataProvider, + out DatabaseObject dbObject, + out string dataSourceName, + out string metadataError)) { - return Utils.McpResponseBuilder.BuildErrorResult(toolName, "InvalidArguments", "Entity name cannot be empty", logger); - } - - string dataSourceName; - try - { - dataSourceName = runtimeConfig.GetDataSourceNameFromEntityName(entityName); - } - catch (Exception) - { - return Utils.McpResponseBuilder.BuildErrorResult(toolName, "InvalidConfiguration", $"Entity '{entityName}' not found in configuration", logger); - } - - IMetadataProviderFactory metadataProviderFactory = serviceProvider.GetRequiredService(); - ISqlMetadataProvider sqlMetadataProvider = metadataProviderFactory.GetMetadataProvider(dataSourceName); - - DatabaseObject dbObject; - try - { - dbObject = sqlMetadataProvider.GetDatabaseObjectByKey(entityName); - } - catch (Exception) - { - return Utils.McpResponseBuilder.BuildErrorResult(toolName, "InvalidConfiguration", $"Database object for entity '{entityName}' not found", logger); + return McpResponseBuilder.BuildErrorResult(toolName, "EntityNotFound", metadataError, logger); } // Create an HTTP context for authorization @@ -123,15 +100,20 @@ public async Task ExecuteAsync( HttpContext httpContext = httpContextAccessor.HttpContext ?? new DefaultHttpContext(); IAuthorizationResolver authorizationResolver = serviceProvider.GetRequiredService(); - if (httpContext is null || !authorizationResolver.IsValidRoleContext(httpContext)) + if (!McpAuthorizationHelper.ValidateRoleContext(httpContext, authorizationResolver, out string roleCtxError)) { - return Utils.McpResponseBuilder.BuildErrorResult(toolName, "PermissionDenied", "Permission denied: Unable to resolve a valid role context for update operation.", logger); + return McpErrorHelpers.PermissionDenied(toolName, entityName, "create", roleCtxError, logger); } - // Validate that we have at least one role authorized for create - if (!TryResolveAuthorizedRole(httpContext, authorizationResolver, entityName, out string authError)) + if (!McpAuthorizationHelper.TryResolveAuthorizedRole( + httpContext, + authorizationResolver, + entityName, + EntityActionOperation.Create, + out string? effectiveRole, + out string authError)) { - return Utils.McpResponseBuilder.BuildErrorResult(toolName, "PermissionDenied", authError, logger); + return McpErrorHelpers.PermissionDenied(toolName, entityName, "create", authError, logger); } JsonElement insertPayloadRoot = dataElement.Clone(); @@ -152,12 +134,12 @@ public async Task ExecuteAsync( } catch (Exception ex) { - return Utils.McpResponseBuilder.BuildErrorResult(toolName, "ValidationFailed", $"Request validation failed: {ex.Message}", logger); + return McpResponseBuilder.BuildErrorResult(toolName, "ValidationFailed", $"Request validation failed: {ex.Message}", logger); } } else { - return Utils.McpResponseBuilder.BuildErrorResult( + return McpResponseBuilder.BuildErrorResult( toolName, "InvalidCreateTarget", "The create_record tool is only available for tables.", @@ -172,7 +154,7 @@ public async Task ExecuteAsync( if (result is CreatedResult createdResult) { - return Utils.McpResponseBuilder.BuildSuccessResult( + return McpResponseBuilder.BuildSuccessResult( new Dictionary { ["entity"] = entityName, @@ -187,7 +169,7 @@ public async Task ExecuteAsync( bool isError = objectResult.StatusCode.HasValue && objectResult.StatusCode.Value >= 400 && objectResult.StatusCode.Value != 403; if (isError) { - return Utils.McpResponseBuilder.BuildErrorResult( + return McpResponseBuilder.BuildErrorResult( toolName, "CreateFailed", $"Failed to create record in entity '{entityName}'. Error: {JsonSerializer.Serialize(objectResult.Value)}", @@ -195,7 +177,7 @@ public async Task ExecuteAsync( } else { - return Utils.McpResponseBuilder.BuildSuccessResult( + return McpResponseBuilder.BuildSuccessResult( new Dictionary { ["entity"] = entityName, @@ -210,7 +192,7 @@ public async Task ExecuteAsync( { if (result is null) { - return Utils.McpResponseBuilder.BuildErrorResult( + return McpResponseBuilder.BuildErrorResult( toolName, "UnexpectedError", $"Mutation engine returned null result for entity '{entityName}'", @@ -218,7 +200,7 @@ public async Task ExecuteAsync( } else { - return Utils.McpResponseBuilder.BuildSuccessResult( + return McpResponseBuilder.BuildSuccessResult( new Dictionary { ["entity"] = entityName, @@ -231,50 +213,8 @@ public async Task ExecuteAsync( } catch (Exception ex) { - return Utils.McpResponseBuilder.BuildErrorResult(toolName, "Error", $"Error: {ex.Message}", logger); + return McpResponseBuilder.BuildErrorResult(toolName, "Error", $"Error: {ex.Message}", logger); } } - - private static bool TryResolveAuthorizedRole( - HttpContext httpContext, - IAuthorizationResolver authorizationResolver, - string entityName, - out string error) - { - error = string.Empty; - - string roleHeader = httpContext.Request.Headers[AuthorizationResolver.CLIENT_ROLE_HEADER].ToString(); - - if (string.IsNullOrWhiteSpace(roleHeader)) - { - error = "Client role header is missing or empty."; - return false; - } - - string[] roles = roleHeader - .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) - .Distinct(StringComparer.OrdinalIgnoreCase) - .ToArray(); - - if (roles.Length == 0) - { - error = "Client role header is missing or empty."; - return false; - } - - foreach (string role in roles) - { - bool allowed = authorizationResolver.AreRoleAndOperationDefinedForEntity( - entityName, role, EntityActionOperation.Create); - - if (allowed) - { - return true; - } - } - - error = "You do not have permission to create records for this entity."; - return false; - } } } diff --git a/src/Azure.DataApiBuilder.Mcp/BuiltInTools/DeleteRecordTool.cs b/src/Azure.DataApiBuilder.Mcp/BuiltInTools/DeleteRecordTool.cs index eb310ae364..d7837c0103 100644 --- a/src/Azure.DataApiBuilder.Mcp/BuiltInTools/DeleteRecordTool.cs +++ b/src/Azure.DataApiBuilder.Mcp/BuiltInTools/DeleteRecordTool.cs @@ -87,11 +87,7 @@ public async Task ExecuteAsync( // 2) Check if the tool is enabled in configuration before proceeding if (config.McpDmlTools?.DeleteRecord != true) { - return McpResponseBuilder.BuildErrorResult( - toolName, - "ToolDisabled", - $"The {this.GetToolMetadata().Name} tool is disabled in the configuration.", - logger); + return McpErrorHelpers.ToolDisabled(GetToolMetadata().Name, logger); } // 3) Parsing & basic argument validation @@ -105,26 +101,17 @@ public async Task ExecuteAsync( return McpResponseBuilder.BuildErrorResult(toolName, "InvalidArguments", parseError, logger); } - IMetadataProviderFactory metadataProviderFactory = serviceProvider.GetRequiredService(); - IMutationEngineFactory mutationEngineFactory = serviceProvider.GetRequiredService(); - - // 4) Resolve metadata for entity existence check - string dataSourceName; - ISqlMetadataProvider sqlMetadataProvider; - - try + // 4) Resolve metadata for entity existence + if (!McpMetadataHelper.TryResolveMetadata( + entityName, + config, + serviceProvider, + out ISqlMetadataProvider sqlMetadataProvider, + out DatabaseObject dbObject, + out string dataSourceName, + out string metadataError)) { - dataSourceName = config.GetDataSourceNameFromEntityName(entityName); - sqlMetadataProvider = metadataProviderFactory.GetMetadataProvider(dataSourceName); - } - catch (Exception) - { - return McpResponseBuilder.BuildErrorResult(toolName, "EntityNotFound", $"Entity '{entityName}' is not defined in the configuration.", logger); - } - - if (!sqlMetadataProvider.EntityToDatabaseObject.TryGetValue(entityName, out DatabaseObject? dbObject) || dbObject is null) - { - return McpResponseBuilder.BuildErrorResult(toolName, "EntityNotFound", $"Entity '{entityName}' is not defined in the configuration.", logger); + return McpResponseBuilder.BuildErrorResult(toolName, "EntityNotFound", metadataError, logger); } // Validate it's a table or view @@ -140,7 +127,7 @@ public async Task ExecuteAsync( if (!McpAuthorizationHelper.ValidateRoleContext(httpContext, authResolver, out string roleError)) { - return McpResponseBuilder.BuildErrorResult(toolName, "PermissionDenied", $"Permission denied: {roleError}", logger); + return McpErrorHelpers.PermissionDenied(toolName, entityName, "delete", roleError, logger); } if (!McpAuthorizationHelper.TryResolveAuthorizedRole( @@ -151,10 +138,11 @@ public async Task ExecuteAsync( out string? effectiveRole, out string authError)) { - return McpResponseBuilder.BuildErrorResult(toolName, "PermissionDenied", $"Permission denied: {authError}", logger); + return McpErrorHelpers.PermissionDenied(toolName, entityName, "delete", authError, logger); } - // 6) Build and validate Delete context + // Need MetadataProviderFactory for RequestValidator; resolve here. + IMetadataProviderFactory metadataProviderFactory = serviceProvider.GetRequiredService(); RequestValidator requestValidator = new(metadataProviderFactory, runtimeConfigProvider); DeleteRequestContext context = new( @@ -174,7 +162,7 @@ public async Task ExecuteAsync( requestValidator.ValidatePrimaryKey(context); - // 7) Execute + IMutationEngineFactory mutationEngineFactory = serviceProvider.GetRequiredService(); DatabaseType dbType = config.GetDataSourceFromDataSourceName(dataSourceName).DatabaseType; IMutationEngine mutationEngine = mutationEngineFactory.GetMutationEngine(dbType); @@ -343,8 +331,7 @@ public async Task ExecuteAsync( } catch (Exception ex) { - ILogger? innerLogger = serviceProvider.GetService>(); - innerLogger?.LogError(ex, "Unexpected error in DeleteRecordTool."); + logger?.LogError(ex, "Unexpected error in DeleteRecordTool."); return McpResponseBuilder.BuildErrorResult( toolName, diff --git a/src/Azure.DataApiBuilder.Mcp/BuiltInTools/DescribeEntitiesTool.cs b/src/Azure.DataApiBuilder.Mcp/BuiltInTools/DescribeEntitiesTool.cs index b8c7d975a2..cd2a7cc28b 100644 --- a/src/Azure.DataApiBuilder.Mcp/BuiltInTools/DescribeEntitiesTool.cs +++ b/src/Azure.DataApiBuilder.Mcp/BuiltInTools/DescribeEntitiesTool.cs @@ -78,11 +78,7 @@ public Task ExecuteAsync( if (!IsToolEnabled(runtimeConfig)) { - return Task.FromResult(McpResponseBuilder.BuildErrorResult( - toolName, - "ToolDisabled", - $"The {GetToolMetadata().Name} tool is disabled in the configuration.", - logger)); + return Task.FromResult(McpErrorHelpers.ToolDisabled(GetToolMetadata().Name, logger)); } // Get authorization services to determine current user's role diff --git a/src/Azure.DataApiBuilder.Mcp/BuiltInTools/ExecuteEntityTool.cs b/src/Azure.DataApiBuilder.Mcp/BuiltInTools/ExecuteEntityTool.cs index 6b0bc28383..e780c8ddeb 100644 --- a/src/Azure.DataApiBuilder.Mcp/BuiltInTools/ExecuteEntityTool.cs +++ b/src/Azure.DataApiBuilder.Mcp/BuiltInTools/ExecuteEntityTool.cs @@ -87,11 +87,7 @@ public async Task ExecuteAsync( // 2) Check if the tool is enabled in configuration before proceeding if (config.McpDmlTools?.ExecuteEntity != true) { - return McpResponseBuilder.BuildErrorResult( - toolName, - "ToolDisabled", - $"The {toolName} tool is disabled in the configuration.", - logger); + return McpErrorHelpers.ToolDisabled(this.GetToolMetadata().Name, logger); } // 3) Parsing & basic argument validation @@ -100,7 +96,7 @@ public async Task ExecuteAsync( return McpResponseBuilder.BuildErrorResult(toolName, "InvalidArguments", "No arguments provided.", logger); } - if (!TryParseExecuteArguments(arguments.RootElement, out string entity, out Dictionary parameters, out string parseError)) + if (!McpArgumentParser.TryParseExecuteArguments(arguments.RootElement, out string entity, out Dictionary parameters, out string parseError)) { return McpResponseBuilder.BuildErrorResult(toolName, "InvalidArguments", parseError, logger); } @@ -125,23 +121,17 @@ public async Task ExecuteAsync( return McpResponseBuilder.BuildErrorResult(toolName, "InvalidEntity", $"Entity {entity} cannot be executed.", logger); } - // 5) Resolve metadata - string dataSourceName; - ISqlMetadataProvider sqlMetadataProvider; - - try - { - dataSourceName = config.GetDataSourceNameFromEntityName(entity); - sqlMetadataProvider = metadataProviderFactory.GetMetadataProvider(dataSourceName); - } - catch (Exception) - { - return McpResponseBuilder.BuildErrorResult(toolName, "EntityNotFound", $"Failed to resolve entity metadata for '{entity}'.", logger); - } - - if (!sqlMetadataProvider.EntityToDatabaseObject.TryGetValue(entity, out DatabaseObject? dbObject) || dbObject is null) + // Use shared metadata helper. + if (!McpMetadataHelper.TryResolveMetadata( + entity, + config, + serviceProvider, + out ISqlMetadataProvider sqlMetadataProvider, + out DatabaseObject dbObject, + out string dataSourceName, + out string metadataError)) { - return McpResponseBuilder.BuildErrorResult(toolName, "EntityNotFound", $"Failed to resolve database object for entity '{entity}'.", logger); + return McpResponseBuilder.BuildErrorResult(toolName, "EntityNotFound", metadataError, logger); } // 6) Authorization - Never bypass permissions @@ -151,7 +141,7 @@ public async Task ExecuteAsync( if (!McpAuthorizationHelper.ValidateRoleContext(httpContext, authResolver, out string roleError)) { - return McpResponseBuilder.BuildErrorResult(toolName, "PermissionDenied", roleError, logger); + return McpErrorHelpers.PermissionDenied(toolName, entity, "execute", roleError, logger); } if (!McpAuthorizationHelper.TryResolveAuthorizedRole( @@ -162,7 +152,7 @@ public async Task ExecuteAsync( out string? effectiveRole, out string authError)) { - return McpResponseBuilder.BuildErrorResult(toolName, "PermissionDenied", authError, logger); + return McpErrorHelpers.PermissionDenied(toolName, entity, "execute", authError, logger); } // 7) Validate parameters against metadata @@ -327,48 +317,6 @@ public async Task ExecuteAsync( } } - /// - /// Parses the execute arguments from the JSON input. - /// - private static bool TryParseExecuteArguments( - JsonElement rootElement, - out string entity, - out Dictionary parameters, - out string parseError) - { - entity = string.Empty; - parameters = new Dictionary(); - parseError = string.Empty; - - if (rootElement.ValueKind != JsonValueKind.Object) - { - parseError = "Arguments must be an object"; - return false; - } - - // Extract entity name (required) - if (!rootElement.TryGetProperty("entity", out JsonElement entityElement) || - entityElement.ValueKind != JsonValueKind.String) - { - parseError = "Missing or invalid 'entity' parameter"; - return false; - } - - entity = entityElement.GetString() ?? string.Empty; - - // Extract parameters if provided (optional) - if (rootElement.TryGetProperty("parameters", out JsonElement parametersElement) && - parametersElement.ValueKind == JsonValueKind.Object) - { - foreach (JsonProperty property in parametersElement.EnumerateObject()) - { - parameters[property.Name] = GetParameterValue(property.Value); - } - } - - return true; - } - /// /// Converts a JSON element to its appropriate CLR type matching GraphQL data types. /// @@ -440,11 +388,7 @@ private static CallToolResult BuildExecuteSuccessResponse( } else if (queryResult is UnauthorizedObjectResult) { - return McpResponseBuilder.BuildErrorResult( - toolName, - "PermissionDenied", - "You do not have permission to execute this entity", - logger); + return McpErrorHelpers.PermissionDenied(toolName, entityName, "execute", "You do not have permission to execute this entity", logger); } else { diff --git a/src/Azure.DataApiBuilder.Mcp/BuiltInTools/ReadRecordsTool.cs b/src/Azure.DataApiBuilder.Mcp/BuiltInTools/ReadRecordsTool.cs index 7561e1ae54..1ed91c30a8 100644 --- a/src/Azure.DataApiBuilder.Mcp/BuiltInTools/ReadRecordsTool.cs +++ b/src/Azure.DataApiBuilder.Mcp/BuiltInTools/ReadRecordsTool.cs @@ -87,11 +87,7 @@ public async Task ExecuteAsync( if (runtimeConfig.McpDmlTools?.ReadRecords is not true) { - return McpResponseBuilder.BuildErrorResult( - toolName, - "ToolDisabled", - "The read_records tool is disabled in the configuration.", - logger); + return McpErrorHelpers.ToolDisabled(toolName, logger); } try @@ -113,13 +109,11 @@ public async Task ExecuteAsync( JsonElement root = arguments.RootElement; - if (!root.TryGetProperty("entity", out JsonElement entityElement) || string.IsNullOrWhiteSpace(entityElement.GetString())) + if (!McpArgumentParser.TryParseEntity(root, out entityName, out string parseError)) { - return McpResponseBuilder.BuildErrorResult(toolName, "InvalidArguments", "Missing required argument 'entity'.", logger); + return McpResponseBuilder.BuildErrorResult(toolName, "InvalidArguments", parseError, logger); } - entityName = entityElement.GetString()!; - if (root.TryGetProperty("select", out JsonElement selectElement)) { select = selectElement.GetString(); @@ -145,27 +139,16 @@ public async Task ExecuteAsync( after = afterElement.GetString(); } - // Get required services & configuration - IQueryEngineFactory queryEngineFactory = serviceProvider.GetRequiredService(); - IMetadataProviderFactory metadataProviderFactory = serviceProvider.GetRequiredService(); - - // Check metadata for entity exists - string dataSourceName; - ISqlMetadataProvider sqlMetadataProvider; - - try - { - dataSourceName = runtimeConfig.GetDataSourceNameFromEntityName(entityName); - sqlMetadataProvider = metadataProviderFactory.GetMetadataProvider(dataSourceName); - } - catch (Exception) + if (!McpMetadataHelper.TryResolveMetadata( + entityName, + runtimeConfig, + serviceProvider, + out ISqlMetadataProvider sqlMetadataProvider, + out DatabaseObject dbObject, + out string dataSourceName, + out string metadataError)) { - return McpResponseBuilder.BuildErrorResult(toolName, "EntityNotFound", $"Entity '{entityName}' is not defined in the configuration.", logger); - } - - if (!sqlMetadataProvider.EntityToDatabaseObject.TryGetValue(entityName, out DatabaseObject? dbObject) || dbObject is null) - { - return McpResponseBuilder.BuildErrorResult(toolName, "EntityNotFound", $"Entity '{entityName}' is not defined in the configuration.", logger); + return McpResponseBuilder.BuildErrorResult(toolName, "EntityNotFound", metadataError, logger); } // Authorization check in the existing entity @@ -174,20 +157,29 @@ public async Task ExecuteAsync( IHttpContextAccessor httpContextAccessor = serviceProvider.GetRequiredService(); HttpContext? httpContext = httpContextAccessor.HttpContext; - if (httpContext is null || !authResolver.IsValidRoleContext(httpContext)) + if (!McpAuthorizationHelper.ValidateRoleContext(httpContext, authResolver, out string roleCtxError)) { - return McpResponseBuilder.BuildErrorResult(toolName, "PermissionDenied", $"You do not have permission to read records for entity '{entityName}'.", logger); + return McpErrorHelpers.PermissionDenied(toolName, entityName, "read", roleCtxError, logger); } - if (!TryResolveAuthorizedRole(httpContext, authResolver, entityName, out string? effectiveRole, out string authError)) + if (!McpAuthorizationHelper.TryResolveAuthorizedRole( + httpContext!, + authResolver, + entityName, + EntityActionOperation.Read, + out string? effectiveRole, + out string readAuthError)) { - return McpResponseBuilder.BuildErrorResult(toolName, "PermissionDenied", authError, logger); + string finalError = readAuthError.StartsWith("You do not have permission", StringComparison.OrdinalIgnoreCase) + ? $"You do not have permission to read records for entity '{entityName}'." + : readAuthError; + return McpErrorHelpers.PermissionDenied(toolName, entityName, "read", finalError, logger); } // Build and validate Find context - RequestValidator requestValidator = new(metadataProviderFactory, runtimeConfigProvider); + RequestValidator requestValidator = new(serviceProvider.GetRequiredService(), runtimeConfigProvider); FindRequestContext context = new(entityName, dbObject, true); - httpContext.Request.Method = "GET"; + httpContext!.Request.Method = "GET"; requestValidator.ValidateEntity(entityName); @@ -233,14 +225,17 @@ public async Task ExecuteAsync( requirements: new[] { new ColumnsPermissionsRequirement() }); if (!authorizationResult.Succeeded) { - return McpResponseBuilder.BuildErrorResult(toolName, "PermissionDenied", DataApiBuilderException.AUTHORIZATION_FAILURE, logger); + return McpErrorHelpers.PermissionDenied(toolName, entityName, "read", DataApiBuilderException.AUTHORIZATION_FAILURE, logger); } // Execute + IQueryEngineFactory queryEngineFactory = serviceProvider.GetRequiredService(); IQueryEngine queryEngine = queryEngineFactory.GetQueryEngine(sqlMetadataProvider.GetDatabaseType()); JsonDocument? queryResult = await queryEngine.ExecuteAsync(context); - IActionResult actionResult = queryResult is null ? SqlResponseHelpers.FormatFindResult(JsonDocument.Parse("[]").RootElement.Clone(), context, metadataProviderFactory.GetMetadataProvider(dataSourceName), runtimeConfigProvider.GetConfig(), httpContext, true) - : SqlResponseHelpers.FormatFindResult(queryResult.RootElement.Clone(), context, metadataProviderFactory.GetMetadataProvider(dataSourceName), runtimeConfigProvider.GetConfig(), httpContext, true); + IMetadataProviderFactory metadataProviderFactory = serviceProvider.GetRequiredService(); + IActionResult actionResult = queryResult is null + ? SqlResponseHelpers.FormatFindResult(JsonDocument.Parse("[]").RootElement.Clone(), context, sqlMetadataProvider, runtimeConfig, httpContext, true) + : SqlResponseHelpers.FormatFindResult(queryResult.RootElement.Clone(), context, sqlMetadataProvider, runtimeConfig, httpContext, true); // Normalize response string rawPayloadJson = McpResponseBuilder.ExtractResultJson(actionResult); @@ -279,59 +274,5 @@ public async Task ExecuteAsync( return McpResponseBuilder.BuildErrorResult(toolName, "UnexpectedError", "Unexpected error occurred in ReadRecordsTool.", logger); } } - - /// - /// Ensures that the role used on the request has the necessary authorizations. - /// - /// Contains request headers and metadata of the user. - /// Resolver used to check if role has necessary authorizations. - /// Name of the entity used in the request. - /// Role defined in client role header. - /// Error message given to the user. - /// True if the user role is authorized, along with the role. - private static bool TryResolveAuthorizedRole( - HttpContext httpContext, - IAuthorizationResolver authorizationResolver, - string entityName, - out string? effectiveRole, - out string error) - { - effectiveRole = null; - error = string.Empty; - - string roleHeader = httpContext.Request.Headers[AuthorizationResolver.CLIENT_ROLE_HEADER].ToString(); - - if (string.IsNullOrWhiteSpace(roleHeader)) - { - error = $"Client role header '{AuthorizationResolver.CLIENT_ROLE_HEADER}' is missing or empty."; - return false; - } - - string[] roles = roleHeader - .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) - .Distinct(StringComparer.OrdinalIgnoreCase) - .ToArray(); - - if (roles.Length == 0) - { - error = $"Client role header '{AuthorizationResolver.CLIENT_ROLE_HEADER}' is missing or empty."; - return false; - } - - foreach (string role in roles) - { - bool allowed = authorizationResolver.AreRoleAndOperationDefinedForEntity( - entityName, role, EntityActionOperation.Read); - - if (allowed) - { - effectiveRole = role; - return true; - } - } - - error = $"You do not have permission to read records for entity '{entityName}'."; - return false; - } } } diff --git a/src/Azure.DataApiBuilder.Mcp/BuiltInTools/UpdateRecordTool.cs b/src/Azure.DataApiBuilder.Mcp/BuiltInTools/UpdateRecordTool.cs index ab8956b618..195e27a0cd 100644 --- a/src/Azure.DataApiBuilder.Mcp/BuiltInTools/UpdateRecordTool.cs +++ b/src/Azure.DataApiBuilder.Mcp/BuiltInTools/UpdateRecordTool.cs @@ -5,7 +5,6 @@ using Azure.DataApiBuilder.Auth; using Azure.DataApiBuilder.Config.DatabasePrimitives; using Azure.DataApiBuilder.Config.ObjectModel; -using Azure.DataApiBuilder.Core.Authorization; using Azure.DataApiBuilder.Core.Configurations; using Azure.DataApiBuilder.Core.Models; using Azure.DataApiBuilder.Core.Resolvers; @@ -92,11 +91,7 @@ public async Task ExecuteAsync( // 2)Check if the tool is enabled in configuration before proceeding. if (config.McpDmlTools?.UpdateRecord != true) { - return McpResponseBuilder.BuildErrorResult( - toolName, - "ToolDisabled", - "The update_record tool is disabled in the configuration.", - logger); + return McpErrorHelpers.ToolDisabled(GetToolMetadata().Name, logger); } try @@ -110,7 +105,12 @@ public async Task ExecuteAsync( return McpResponseBuilder.BuildErrorResult(toolName, "InvalidArguments", "No arguments provided.", logger); } - if (!TryParseArguments(arguments.RootElement, out string entityName, out Dictionary keys, out Dictionary fields, out string parseError)) + if (!McpArgumentParser.TryParseEntityKeysAndFields( + arguments.RootElement, + out string entityName, + out Dictionary keys, + out Dictionary fields, + out string parseError)) { return McpResponseBuilder.BuildErrorResult(toolName, "InvalidArguments", parseError, logger); } @@ -118,23 +118,16 @@ public async Task ExecuteAsync( IMetadataProviderFactory metadataProviderFactory = serviceProvider.GetRequiredService(); IMutationEngineFactory mutationEngineFactory = serviceProvider.GetRequiredService(); - // 4) Resolve metadata for entity existence check - string dataSourceName; - ISqlMetadataProvider sqlMetadataProvider; - - try - { - dataSourceName = config.GetDataSourceNameFromEntityName(entityName); - sqlMetadataProvider = metadataProviderFactory.GetMetadataProvider(dataSourceName); - } - catch (Exception) - { - return McpResponseBuilder.BuildErrorResult(toolName, "EntityNotFound", $"Entity '{entityName}' is not defined in the configuration.", logger); - } - - if (!sqlMetadataProvider.EntityToDatabaseObject.TryGetValue(entityName, out DatabaseObject? dbObject) || dbObject is null) + if (!McpMetadataHelper.TryResolveMetadata( + entityName, + config, + serviceProvider, + out ISqlMetadataProvider sqlMetadataProvider, + out DatabaseObject dbObject, + out string dataSourceName, + out string metadataError)) { - return McpResponseBuilder.BuildErrorResult(toolName, "EntityNotFound", $"Entity '{entityName}' is not defined in the configuration.", logger); + return McpResponseBuilder.BuildErrorResult(toolName, "EntityNotFound", metadataError, logger); } // 5) Authorization after we have a known entity @@ -144,12 +137,18 @@ public async Task ExecuteAsync( if (httpContext is null || !authResolver.IsValidRoleContext(httpContext)) { - return McpResponseBuilder.BuildErrorResult(toolName, "PermissionDenied", "Permission denied: unable to resolve a valid role context for update operation.", logger); + return McpErrorHelpers.PermissionDenied(toolName, entityName, "update", "unable to resolve a valid role context for update operation.", logger); } - if (!TryResolveAuthorizedRoleHasPermission(httpContext, authResolver, entityName, out string? effectiveRole, out string authError)) + if (!McpAuthorizationHelper.TryResolveAuthorizedRole( + httpContext!, + authResolver, + entityName, + EntityActionOperation.Update, + out string? effectiveRole, + out string authError)) { - return McpResponseBuilder.BuildErrorResult(toolName, "PermissionDenied", $"Permission denied: {authError}", logger); + return McpErrorHelpers.PermissionDenied(toolName, entityName, "update", authError, logger); } // 6) Build and validate Upsert (UpdateIncremental) context @@ -194,11 +193,7 @@ public async Task ExecuteAsync( if (errorMsg.Contains("No Update could be performed, record not found", StringComparison.OrdinalIgnoreCase)) { - return McpResponseBuilder.BuildErrorResult( - toolName, - "InvalidArguments", - "No record found with the given key.", - logger); + return McpResponseBuilder.BuildErrorResult(toolName, "InvalidArguments", "No record found with the given key.", logger); } else { @@ -247,8 +242,8 @@ public async Task ExecuteAsync( } catch (Exception ex) { - ILogger? innerLogger = serviceProvider.GetService>(); - innerLogger?.LogError(ex, "Unexpected error in UpdateRecordTool."); + logger?.LogError(ex, "Unexpected error in UpdateRecordTool."); + return McpResponseBuilder.BuildErrorResult( toolName, "UnexpectedError", @@ -256,117 +251,5 @@ public async Task ExecuteAsync( logger); } } - - #region Parsing & Authorization - - private static bool TryParseArguments( - JsonElement root, - out string entityName, - out Dictionary keys, - out Dictionary fields, - out string error) - { - entityName = string.Empty; - keys = new Dictionary(); - fields = new Dictionary(); - error = string.Empty; - - if (!root.TryGetProperty("entity", out JsonElement entityEl) || - !root.TryGetProperty("keys", out JsonElement keysEl) || - !root.TryGetProperty("fields", out JsonElement fieldsEl)) - { - error = "Missing required arguments 'entity', 'keys', or 'fields'."; - return false; - } - - // Parse and validate required arguments: entity, keys, fields - entityName = entityEl.GetString() ?? string.Empty; - if (string.IsNullOrWhiteSpace(entityName)) - { - throw new ArgumentException("Entity is required", nameof(entityName)); - } - - if (keysEl.ValueKind != JsonValueKind.Object || fieldsEl.ValueKind != JsonValueKind.Object) - { - throw new ArgumentException("'keys' and 'fields' must be JSON objects."); - } - - try - { - keys = JsonSerializer.Deserialize>(keysEl.GetRawText()) ?? new Dictionary(); - fields = JsonSerializer.Deserialize>(fieldsEl.GetRawText()) ?? new Dictionary(); - } - catch (Exception ex) - { - throw new ArgumentException("Failed to parse 'keys' or 'fields'", ex); - } - - if (keys.Count == 0) - { - throw new ArgumentException("Keys are required to update an entity"); - } - - if (fields.Count == 0) - { - throw new ArgumentException("At least one field must be provided to update an entity", nameof(fields)); - } - - foreach (KeyValuePair kv in keys) - { - if (kv.Value is null || (kv.Value is string str && string.IsNullOrWhiteSpace(str))) - { - throw new ArgumentException($"Key value for '{kv.Key}' cannot be null or empty."); - } - } - - return true; - } - - private static bool TryResolveAuthorizedRoleHasPermission( - HttpContext httpContext, - IAuthorizationResolver authorizationResolver, - string entityName, - out string? effectiveRole, - out string error) - { - effectiveRole = null; - error = string.Empty; - - string roleHeader = httpContext.Request.Headers[AuthorizationResolver.CLIENT_ROLE_HEADER].ToString(); - - if (string.IsNullOrWhiteSpace(roleHeader)) - { - error = "Client role header is missing or empty."; - return false; - } - - string[] roles = roleHeader - .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) - .Distinct(StringComparer.OrdinalIgnoreCase) - .ToArray(); - - if (roles.Length == 0) - { - error = "Client role header is missing or empty."; - return false; - } - - foreach (string role in roles) - { - bool allowed = authorizationResolver.AreRoleAndOperationDefinedForEntity( - entityName, role, EntityActionOperation.Update); - - if (allowed) - { - effectiveRole = role; - return true; - } - } - - error = "You do not have permission to update records for this entity."; - return false; - } - - #endregion } } diff --git a/src/Azure.DataApiBuilder.Mcp/Model/McpErrorCode.cs b/src/Azure.DataApiBuilder.Mcp/Model/McpErrorCode.cs new file mode 100644 index 0000000000..ed13f62783 --- /dev/null +++ b/src/Azure.DataApiBuilder.Mcp/Model/McpErrorCode.cs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace Azure.DataApiBuilder.Mcp.Model +{ + /// + /// MCP error codes standardized for built-in tools. + /// + public enum McpErrorCode + { + ToolDisabled, + PermissionDenied + } +} diff --git a/src/Azure.DataApiBuilder.Mcp/Utils/McpArgumentParser.cs b/src/Azure.DataApiBuilder.Mcp/Utils/McpArgumentParser.cs index 04d14eb5d6..02344c2956 100644 --- a/src/Azure.DataApiBuilder.Mcp/Utils/McpArgumentParser.cs +++ b/src/Azure.DataApiBuilder.Mcp/Utils/McpArgumentParser.cs @@ -11,26 +11,24 @@ namespace Azure.DataApiBuilder.Mcp.Utils public static class McpArgumentParser { /// - /// Parses entity and keys arguments for delete/update operations. + /// Parses only the entity name from arguments. /// - public static bool TryParseEntityAndKeys( + public static bool TryParseEntity( JsonElement root, out string entityName, - out Dictionary keys, - out string error) + out string error, + CancellationToken cancellationToken = default) { + cancellationToken.ThrowIfCancellationRequested(); entityName = string.Empty; - keys = new Dictionary(); error = string.Empty; - if (!root.TryGetProperty("entity", out JsonElement entityEl) || - !root.TryGetProperty("keys", out JsonElement keysEl)) + if (!root.TryGetProperty("entity", out JsonElement entityEl)) { - error = "Missing required arguments 'entity' or 'keys'."; + error = "Missing required argument 'entity'."; return false; } - // Parse and validate entity name entityName = entityEl.GetString() ?? string.Empty; if (string.IsNullOrWhiteSpace(entityName)) { @@ -38,7 +36,65 @@ public static bool TryParseEntityAndKeys( return false; } - // Parse and validate keys + return true; + } + + /// + /// Parses entity and data arguments for create operations. + /// + public static bool TryParseEntityAndData( + JsonElement root, + out string entityName, + out JsonElement dataElement, + out string error, + CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + dataElement = default; + + if (!TryParseEntity(root, out entityName, out error, cancellationToken)) + { + return false; + } + + if (!root.TryGetProperty("data", out dataElement)) + { + error = "Missing required argument 'data'."; + return false; + } + + if (dataElement.ValueKind != JsonValueKind.Object) + { + error = "'data' must be a JSON object."; + return false; + } + + return true; + } + + /// + /// Parses entity and keys arguments for delete/update operations. + /// + public static bool TryParseEntityAndKeys( + JsonElement root, + out string entityName, + out Dictionary keys, + out string error, + CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + keys = new Dictionary(); + if (!TryParseEntity(root, out entityName, out error, cancellationToken)) + { + return false; + } + + if (!root.TryGetProperty("keys", out JsonElement keysEl)) + { + error = "Missing required argument 'keys'."; + return false; + } + if (keysEl.ValueKind != JsonValueKind.Object) { error = "'keys' must be a JSON object."; @@ -64,6 +120,8 @@ public static bool TryParseEntityAndKeys( // Validate key values foreach (KeyValuePair kv in keys) { + cancellationToken.ThrowIfCancellationRequested(); + if (kv.Value is null || (kv.Value is string str && string.IsNullOrWhiteSpace(str))) { error = $"Primary key value for '{kv.Key}' cannot be null or empty"; @@ -82,12 +140,14 @@ public static bool TryParseEntityKeysAndFields( out string entityName, out Dictionary keys, out Dictionary fields, - out string error) + out string error, + CancellationToken cancellationToken = default) { + cancellationToken.ThrowIfCancellationRequested(); fields = new Dictionary(); // First parse entity and keys - if (!TryParseEntityAndKeys(root, out entityName, out keys, out error)) + if (!TryParseEntityAndKeys(root, out entityName, out keys, out error, cancellationToken)) { return false; } @@ -123,5 +183,61 @@ public static bool TryParseEntityKeysAndFields( return true; } + + /// + /// Parses the execute arguments from the JSON input. + /// + public static bool TryParseExecuteArguments( + JsonElement rootElement, + out string entity, + out Dictionary parameters, + out string parseError, + CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + entity = string.Empty; + parameters = new Dictionary(); + + if (rootElement.ValueKind != JsonValueKind.Object) + { + parseError = "Arguments must be an object"; + return false; + } + + if (!TryParseEntity(rootElement, out entity, out parseError, cancellationToken)) + { + return false; + } + + // Extract parameters if provided (optional) + if (rootElement.TryGetProperty("parameters", out JsonElement parametersElement) && + parametersElement.ValueKind == JsonValueKind.Object) + { + foreach (JsonProperty property in parametersElement.EnumerateObject()) + { + cancellationToken.ThrowIfCancellationRequested(); + parameters[property.Name] = GetExecuteParameterValue(property.Value); + } + } + + return true; + } + + // Local helper replicating ExecuteEntityTool.GetParameterValue without refactoring other tools. + private static object? GetExecuteParameterValue(JsonElement element) + { + return element.ValueKind switch + { + JsonValueKind.String => element.GetString(), + JsonValueKind.Number => + element.TryGetInt64(out long longValue) ? longValue : + element.TryGetDecimal(out decimal decimalValue) ? decimalValue : + element.GetDouble(), + JsonValueKind.True => true, + JsonValueKind.False => false, + JsonValueKind.Null => null, + _ => element.ToString() + }; + } } } diff --git a/src/Azure.DataApiBuilder.Mcp/Utils/McpErrorHelpers.cs b/src/Azure.DataApiBuilder.Mcp/Utils/McpErrorHelpers.cs new file mode 100644 index 0000000000..75335b2db1 --- /dev/null +++ b/src/Azure.DataApiBuilder.Mcp/Utils/McpErrorHelpers.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.Extensions.Logging; +using ModelContextProtocol.Protocol; + +namespace Azure.DataApiBuilder.Mcp.Utils +{ + /// + /// Helper utilities for creating standardized MCP error responses. + /// Only includes helpers currently being centralized. + /// + public static class McpErrorHelpers + { + public static CallToolResult PermissionDenied(string toolName, string entityName, string operation, string detail, ILogger? logger) + { + string message = $"Permission denied for {operation} on entity '{entityName}'. {detail}"; + return McpResponseBuilder.BuildErrorResult(toolName, Model.McpErrorCode.PermissionDenied.ToString(), message, logger); + } + + // Centralized language for 'tool disabled' errors. Pass the tool name, e.g. "read_records". + public static CallToolResult ToolDisabled(string toolName, ILogger? logger) + { + string message = $"The {toolName} tool is disabled in the configuration."; + return McpResponseBuilder.BuildErrorResult(toolName, Model.McpErrorCode.ToolDisabled.ToString(), message, logger); + } + } +} diff --git a/src/Azure.DataApiBuilder.Mcp/Utils/McpMetadataHelper.cs b/src/Azure.DataApiBuilder.Mcp/Utils/McpMetadataHelper.cs new file mode 100644 index 0000000000..1e79e86b15 --- /dev/null +++ b/src/Azure.DataApiBuilder.Mcp/Utils/McpMetadataHelper.cs @@ -0,0 +1,89 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Azure.DataApiBuilder.Config.DatabasePrimitives; +using Azure.DataApiBuilder.Config.ObjectModel; +using Azure.DataApiBuilder.Service.Exceptions; // Added for DataApiBuilderException +using Microsoft.Extensions.DependencyInjection; + +namespace Azure.DataApiBuilder.Mcp.Utils +{ + /// + /// Utility class for resolving metadata and datasource information for MCP tools. + /// + public static class McpMetadataHelper + { + public static bool TryResolveMetadata( + string entityName, + RuntimeConfig config, + IServiceProvider serviceProvider, + out Azure.DataApiBuilder.Core.Services.ISqlMetadataProvider sqlMetadataProvider, + out DatabaseObject dbObject, + out string dataSourceName, + out string error, + CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + sqlMetadataProvider = default!; + dbObject = default!; + dataSourceName = string.Empty; + error = string.Empty; + + if (string.IsNullOrWhiteSpace(entityName)) + { + error = "Entity name cannot be null or empty."; + return false; + } + + var metadataProviderFactory = serviceProvider.GetRequiredService(); + + // Resolve datasource name for the entity. + try + { + cancellationToken.ThrowIfCancellationRequested(); + dataSourceName = config.GetDataSourceNameFromEntityName(entityName); + } + catch (DataApiBuilderException dabEx) when (dabEx.SubStatusCode == DataApiBuilderException.SubStatusCodes.EntityNotFound) + { + error = $"Entity '{entityName}' is not defined in the configuration."; + return false; + } + catch (DataApiBuilderException dabEx) + { + // Other DAB exceptions during entity->datasource resolution. + error = dabEx.Message; + return false; + } + + // Resolve metadata provider for the datasource. + try + { + cancellationToken.ThrowIfCancellationRequested(); + sqlMetadataProvider = metadataProviderFactory.GetMetadataProvider(dataSourceName); + } + catch (DataApiBuilderException dabEx) when (dabEx.SubStatusCode == DataApiBuilderException.SubStatusCodes.DataSourceNotFound) + { + error = $"Data source '{dataSourceName}' for entity '{entityName}' is not defined in the configuration."; + return false; + } + catch (DataApiBuilderException dabEx) + { + // Other DAB exceptions during metadata provider resolution. + error = dabEx.Message; + return false; + } + + cancellationToken.ThrowIfCancellationRequested(); + + // Validate entity exists in metadata mapping. + if (!sqlMetadataProvider.EntityToDatabaseObject.TryGetValue(entityName, out DatabaseObject? temp) || temp is null) + { + error = $"Entity '{entityName}' is not defined in the configuration."; + return false; + } + + dbObject = temp; + return true; + } + } +} From 7d762869a8431fa9ad32b4e7062f68aa8677e099 Mon Sep 17 00:00:00 2001 From: Alekhya-Polavarapu Date: Thu, 4 Dec 2025 10:56:29 -0800 Subject: [PATCH 16/36] Fix the Serialization/Deserialization issue with $ prefix columns (#2944) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### Why make this change? Serialization and deserialization of metadata currently fail when column names are prefixed with the $ symbol. ### Root cause This issue occurs because we’ve enabled the ReferenceHandler flag in our System.Text.Json serialization settings. When this flag is active, the serializer treats $ as a reserved character used for special metadata (e.g., $id, $ref). As a result, any property name starting with $ is interpreted as metadata and cannot be deserialized properly. ### What is this change? This update introduces custom logic in the converter’s Write and Read methods to handle $-prefixed column names safely. - During serialization, columns beginning with $ are escaped as "_$". - During deserialization, this transformation is reversed to restore the original property names. ### How was this tested - [x] Unit tests --------- Co-authored-by: Aniruddh Munde --- .../Converters/DatabaseObjectConverter.cs | 78 +++++++++++++- .../SerializationDeserializationTests.cs | 101 ++++++++++++++++-- 2 files changed, 172 insertions(+), 7 deletions(-) diff --git a/src/Core/Services/MetadataProviders/Converters/DatabaseObjectConverter.cs b/src/Core/Services/MetadataProviders/Converters/DatabaseObjectConverter.cs index c6e694a394..d1894c7c45 100644 --- a/src/Core/Services/MetadataProviders/Converters/DatabaseObjectConverter.cs +++ b/src/Core/Services/MetadataProviders/Converters/DatabaseObjectConverter.cs @@ -17,6 +17,11 @@ namespace Azure.DataApiBuilder.Core.Services.MetadataProviders.Converters public class DatabaseObjectConverter : JsonConverter { private const string TYPE_NAME = "TypeName"; + private const string DOLLAR_CHAR = "$"; + + // ``DAB_ESCAPE$`` is used to escape column names that start with `$` during serialization. + // It is chosen to be unique enough to avoid collisions with actual column names. + private const string ESCAPED_DOLLARCHAR = "DAB_ESCAPE$"; public override DatabaseObject Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { @@ -29,6 +34,15 @@ public override DatabaseObject Read(ref Utf8JsonReader reader, Type typeToConver DatabaseObject objA = (DatabaseObject)JsonSerializer.Deserialize(document, concreteType, options)!; + foreach (PropertyInfo prop in objA.GetType().GetProperties().Where(IsSourceDefinitionProperty)) + { + SourceDefinition? sourceDef = (SourceDefinition?)prop.GetValue(objA); + if (sourceDef is not null) + { + UnescapeDollaredColumns(sourceDef); + } + } + return objA; } } @@ -58,12 +72,74 @@ public override void Write(Utf8JsonWriter writer, DatabaseObject value, JsonSeri } writer.WritePropertyName(prop.Name); - JsonSerializer.Serialize(writer, prop.GetValue(value), options); + object? propVal = prop.GetValue(value); + Type propType = prop.PropertyType; + + // Only escape columns for properties whose type is exactly SourceDefinition (not subclasses). + // This is because, we do not want unnecessary mutation of subclasses of SourceDefinition unless needed. + if (IsSourceDefinitionProperty(prop) && propVal is SourceDefinition sourceDef && propVal.GetType() == typeof(SourceDefinition)) + { + EscapeDollaredColumns(sourceDef); + } + + JsonSerializer.Serialize(writer, propVal, propType, options); } writer.WriteEndObject(); } + private static bool IsSourceDefinitionProperty(PropertyInfo prop) + { + // Only return true for properties whose type is exactly SourceDefinition (not subclasses) + return prop.PropertyType == typeof(SourceDefinition); + } + + /// + /// Escapes column keys that start with '$' to '_$' for serialization. + /// + private static void EscapeDollaredColumns(SourceDefinition sourceDef) + { + if (sourceDef.Columns is null || sourceDef.Columns.Count == 0) + { + return; + } + + List keysToEscape = sourceDef.Columns.Keys + .Where(k => k.StartsWith(DOLLAR_CHAR, StringComparison.Ordinal)) + .ToList(); + + foreach (string key in keysToEscape) + { + ColumnDefinition col = sourceDef.Columns[key]; + sourceDef.Columns.Remove(key); + string newKey = ESCAPED_DOLLARCHAR + key[1..]; + sourceDef.Columns[newKey] = col; + } + } + + /// + /// Unescapes column keys that start with '_$' to '$' for deserialization. + /// + private static void UnescapeDollaredColumns(SourceDefinition sourceDef) + { + if (sourceDef.Columns is null || sourceDef.Columns.Count == 0) + { + return; + } + + List keysToUnescape = sourceDef.Columns.Keys + .Where(k => k.StartsWith(ESCAPED_DOLLARCHAR, StringComparison.Ordinal)) + .ToList(); + + foreach (string key in keysToUnescape) + { + ColumnDefinition col = sourceDef.Columns[key]; + sourceDef.Columns.Remove(key); + string newKey = DOLLAR_CHAR + key[11..]; + sourceDef.Columns[newKey] = col; + } + } + private static Type GetTypeFromName(string typeName) { Type? type = Type.GetType(typeName); diff --git a/src/Service.Tests/UnitTests/SerializationDeserializationTests.cs b/src/Service.Tests/UnitTests/SerializationDeserializationTests.cs index 74d548fef4..a4cb2848ad 100644 --- a/src/Service.Tests/UnitTests/SerializationDeserializationTests.cs +++ b/src/Service.Tests/UnitTests/SerializationDeserializationTests.cs @@ -277,8 +277,96 @@ public void TestDictionaryDatabaseObjectSerializationDeserialization() VerifySourceDefinitionSerializationDeserialization(deserializedDatabaseTable.TableDefinition, _databaseTable.TableDefinition, "FirstName"); } - private void InitializeObjects() + /// + /// Validates serialization and deserilization of Dictionary containing DatabaseTable + /// The table will have dollar sign prefix ($) in the column name + /// this is how we serialize and deserialize metadataprovider.EntityToDatabaseObject dict. + /// + [TestMethod] + public void TestDictionaryDatabaseObjectSerializationDeserialization_WithDollarColumn() + { + InitializeObjects(generateDollaredColumn: true); + + _options = new() + { + Converters = { + new DatabaseObjectConverter(), + new TypeConverter() + }, + ReferenceHandler = ReferenceHandler.Preserve + }; + + Dictionary dict = new() { { "person", _databaseTable } }; + + string serializedDict = JsonSerializer.Serialize(dict, _options); + Dictionary deserializedDict = JsonSerializer.Deserialize>(serializedDict, _options)!; + + DatabaseTable deserializedDatabaseTable = (DatabaseTable)deserializedDict["person"]; + + Assert.AreEqual(deserializedDatabaseTable.SourceType, _databaseTable.SourceType); + Assert.AreEqual(deserializedDatabaseTable.FullName, _databaseTable.FullName); + deserializedDatabaseTable.Equals(_databaseTable); + VerifySourceDefinitionSerializationDeserialization(deserializedDatabaseTable.SourceDefinition, _databaseTable.SourceDefinition, "$FirstName"); + VerifySourceDefinitionSerializationDeserialization(deserializedDatabaseTable.TableDefinition, _databaseTable.TableDefinition, "$FirstName"); + } + + /// + /// Validates serialization and deserilization of Dictionary containing DatabaseView + /// The table will have dollar sign prefix ($) in the column name + /// this is how we serialize and deserialize metadataprovider.EntityToDatabaseObject dict. + /// + [TestMethod] + public void TestDatabaseViewSerializationDeserialization_WithDollarColumn() + { + InitializeObjects(generateDollaredColumn: true); + + TestTypeNameChanges(_databaseView, "DatabaseView"); + + // Test to catch if there is change in number of properties/fields + // Note: On Addition of property make sure it is added in following object creation _databaseView and include in serialization + // and deserialization test. + int fields = typeof(DatabaseView).GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance).Length; + Assert.AreEqual(fields, 6); + + string serializedDatabaseView = JsonSerializer.Serialize(_databaseView, _options); + DatabaseView deserializedDatabaseView = JsonSerializer.Deserialize(serializedDatabaseView, _options)!; + + Assert.AreEqual(deserializedDatabaseView.SourceType, _databaseView.SourceType); + deserializedDatabaseView.Equals(_databaseView); + VerifySourceDefinitionSerializationDeserialization(deserializedDatabaseView.SourceDefinition, _databaseView.SourceDefinition, "$FirstName"); + VerifySourceDefinitionSerializationDeserialization(deserializedDatabaseView.ViewDefinition, _databaseView.ViewDefinition, "$FirstName"); + } + + /// + /// Validates serialization and deserilization of Dictionary containing DatabaseStoredProcedure + /// The table will have dollar sign prefix ($) in the column name + /// this is how we serialize and deserialize metadataprovider.EntityToDatabaseObject dict. + /// + [TestMethod] + public void TestDatabaseStoredProcedureSerializationDeserialization_WithDollarColumn() { + InitializeObjects(generateDollaredColumn: true); + + TestTypeNameChanges(_databaseStoredProcedure, "DatabaseStoredProcedure"); + + // Test to catch if there is change in number of properties/fields + // Note: On Addition of property make sure it is added in following object creation _databaseStoredProcedure and include in serialization + // and deserialization test. + int fields = typeof(DatabaseStoredProcedure).GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance).Length; + Assert.AreEqual(fields, 6); + + string serializedDatabaseSP = JsonSerializer.Serialize(_databaseStoredProcedure, _options); + DatabaseStoredProcedure deserializedDatabaseSP = JsonSerializer.Deserialize(serializedDatabaseSP, _options)!; + + Assert.AreEqual(deserializedDatabaseSP.SourceType, _databaseStoredProcedure.SourceType); + deserializedDatabaseSP.Equals(_databaseStoredProcedure); + VerifySourceDefinitionSerializationDeserialization(deserializedDatabaseSP.SourceDefinition, _databaseStoredProcedure.SourceDefinition, "$FirstName", true); + VerifySourceDefinitionSerializationDeserialization(deserializedDatabaseSP.StoredProcedureDefinition, _databaseStoredProcedure.StoredProcedureDefinition, "$FirstName", true); + } + + private void InitializeObjects(bool generateDollaredColumn = false) + { + string columnName = generateDollaredColumn ? "$FirstName" : "FirstName"; _options = new() { // ObjectConverter behavior different in .NET8 most likely due to @@ -290,10 +378,11 @@ private void InitializeObjects() new DatabaseObjectConverter(), new TypeConverter() } + }; _columnDefinition = GetColumnDefinition(typeof(string), DbType.String, true, false, false, new string("John"), false); - _sourceDefinition = GetSourceDefinition(false, false, new List() { "FirstName" }, _columnDefinition); + _sourceDefinition = GetSourceDefinition(false, false, new List() { columnName }, _columnDefinition); _databaseTable = new DatabaseTable() { @@ -312,10 +401,10 @@ private void InitializeObjects() { IsInsertDMLTriggerEnabled = false, IsUpdateDMLTriggerEnabled = false, - PrimaryKey = new List() { "FirstName" }, + PrimaryKey = new List() { columnName }, }, }; - _databaseView.ViewDefinition.Columns.Add("FirstName", _columnDefinition); + _databaseView.ViewDefinition.Columns.Add(columnName, _columnDefinition); _parameterDefinition = new() { @@ -332,10 +421,10 @@ private void InitializeObjects() SourceType = EntitySourceType.StoredProcedure, StoredProcedureDefinition = new() { - PrimaryKey = new List() { "FirstName" }, + PrimaryKey = new List() { columnName }, } }; - _databaseStoredProcedure.StoredProcedureDefinition.Columns.Add("FirstName", _columnDefinition); + _databaseStoredProcedure.StoredProcedureDefinition.Columns.Add(columnName, _columnDefinition); _databaseStoredProcedure.StoredProcedureDefinition.Parameters.Add("Id", _parameterDefinition); } From e83ccd8cae9e1a99e0df351810ff3d4ccf23102d Mon Sep 17 00:00:00 2001 From: Anusha Kolan Date: Fri, 5 Dec 2025 14:11:40 -0800 Subject: [PATCH 17/36] [MCP] Added support for `--mcp-stdio` flag to `dab start` (#2983) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Why make this change? - Add MCP stdio support to Data API Builder and wire it through both the engine and CLI so DAB can be used as a Model Context Protocol (MCP) server. - Ensures MCP sessions can run under a specific DAB authorization role, making it possible to test and use MCP tools with permissions from `dab-config.json`. ## What is this change? Service entrypoint - Detects `--mcp-stdio` early, configures stdin/stdout encodings, and redirects all non‑MCP output to STDERR to keep STDOUT clean for MCP JSON. - Parses an optional `role:` argument (e.g. role:anonymous, role:authenticated) and injects it into configuration as `MCP:Role`, defaulting to `anonymous` when omitted. - In MCP stdio mode, forces `Runtime:Host:Authentication:Provider = "Simulator"` via in‑memory configuration so the requested role is always available during MCP sessions. - Starts the full ASP.NET Core host, registers all MCP tools from DI, and runs the MCP stdio loop instead of the normal HTTP `host.Run(`). CLI Integration - Adds `--mcp-stdio` to `dab start` to launch the engine in MCP stdio mode. - Adds an optional positional `role` argument (e.g. `role:anonymous`) captured as `StartOptions.McpRole`. - Keeps existing behavior for non‑MCP `dab start` unchanged. Note - `ExecuteEntityTool` now looks for MCP tool inputs under arguments (the standard MCP field) and falls back to the legacy parameters property only if arguments is missing. This aligns our server with how current MCP clients (like VS Code) actually send tool arguments, and preserves backward compatibility for any older clients that still use parameters. ## How was this tested? Integration-like manual testing via MCP clients against: - Engine-based MCP server: `dotnet Azure.DataApiBuilder.Service.dll --mcp-stdio role:authenticated`. - CLI-based MCP server: `dab start --mcp-stdio role:authenticated`. Manual verification of all MCP tools: - `describe_entities` shows correct entities and effective permissions for the active role. - `read_records`, `create_record`, `update_record`, `delete_record`, `execute_entity` succeed when the role has the appropriate permissions. ## Sample Request(s) 1. MCP server via CLI (dab) ` { "mcpServers": { "dab-with-exe": { "command": "C:\\DAB\\data-api-builder\\out\\publish\\Debug\\net8.0\\win-x64\\dab\\Microsoft.DataApiBuilder.exe", "args": ["start", "--mcp-stdio", "role:authenticated", "--config", "C:\\DAB\\data-api-builder\\dab-config.json"], "env": { "DAB_ENVIRONMENT": "Development" } } } ` 2. MCP server via engine DLL ` { "mcpServers": { "dab": { "command": "dotnet", "args": [ "C:\\DAB\\data-api-builder\\out\\publish\\Debug\\net8.0\\win-x64\\dab\\Azure.DataApiBuilder.Service.dll", "--mcp-stdio", "role:authenticated", "--config", "C:\\DAB\\data-api-builder\\dab-config.json" ], "type": "stdio" } } } ` --- .../Core/McpProtocolDefaults.cs | 30 ++ .../Core/McpStdioServer.cs | 486 ++++++++++++++++++ .../IMcpStdioServer.cs | 7 + src/Cli/Commands/StartOptions.cs | 13 +- src/Cli/ConfigGenerator.cs | 11 + src/Cli/Exporter.cs | 8 +- src/Service/Program.cs | 68 ++- src/Service/Startup.cs | 13 +- src/Service/Utilities/McpStdioHelper.cs | 97 ++++ 9 files changed, 717 insertions(+), 16 deletions(-) create mode 100644 src/Azure.DataApiBuilder.Mcp/Core/McpProtocolDefaults.cs create mode 100644 src/Azure.DataApiBuilder.Mcp/Core/McpStdioServer.cs create mode 100644 src/Azure.DataApiBuilder.Mcp/IMcpStdioServer.cs create mode 100644 src/Service/Utilities/McpStdioHelper.cs diff --git a/src/Azure.DataApiBuilder.Mcp/Core/McpProtocolDefaults.cs b/src/Azure.DataApiBuilder.Mcp/Core/McpProtocolDefaults.cs new file mode 100644 index 0000000000..8e307a7c0e --- /dev/null +++ b/src/Azure.DataApiBuilder.Mcp/Core/McpProtocolDefaults.cs @@ -0,0 +1,30 @@ +using Microsoft.Extensions.Configuration; + +namespace Azure.DataApiBuilder.Mcp.Core +{ + /// + /// Centralized defaults and configuration keys for MCP protocol settings. + /// + public static class McpProtocolDefaults + { + /// + /// 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/McpStdioServer.cs b/src/Azure.DataApiBuilder.Mcp/Core/McpStdioServer.cs new file mode 100644 index 0000000000..79ccf39356 --- /dev/null +++ b/src/Azure.DataApiBuilder.Mcp/Core/McpStdioServer.cs @@ -0,0 +1,486 @@ +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: -32600, 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: -32700, message: "Parse error"); + continue; + } + catch (Exception ex) + { + Console.Error.WriteLine($"[MCP DEBUG] Unexpected error parsing request: {ex.Message}"); + WriteError(id: null, code: -32603, 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, -32600, "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, -32601, $"Method not found: {method}"); + break; + } + } + catch (Exception) + { + WriteError(id, -32603, "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) + { + // Extract the actual id value from the request + object? requestId = id.HasValue ? GetIdValue(id.Value) : null; + + // Create the initialize response + var response = new + { + jsonrpc = "2.0", + id = requestId, + result = new + { + protocolVersion = _protocolVersion, + capabilities = new + { + tools = new { listChanged = true }, + logging = new { } + }, + serverInfo = new + { + name = "Data API Builder", + version = "1.0.0" + } + } + }; + + string json = JsonSerializer.Serialize(response); + Console.Out.WriteLine(json); + } + + /// + /// 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, -32602, "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, -32602, "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, -32602, $"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/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..1d673c11e3 100644 --- a/src/Cli/ConfigGenerator.cs +++ b/src/Cli/ConfigGenerator.cs @@ -2359,6 +2359,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/Service/Program.cs b/src/Service/Program.cs index 1059fd52ff..e0a74bd9d1 100644 --- a/src/Service/Program.cs +++ b/src/Service/Program.cs @@ -1,20 +1,20 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - using System; 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 +33,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 +48,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 +86,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 +165,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 +261,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..bb164d18e7 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(); } diff --git a/src/Service/Utilities/McpStdioHelper.cs b/src/Service/Utilities/McpStdioHelper.cs new file mode 100644 index 0000000000..9e337d0809 --- /dev/null +++ b/src/Service/Utilities/McpStdioHelper.cs @@ -0,0 +1,97 @@ +using System; +using System.Collections.Generic; +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, if specified. + /// + public static bool ShouldRunMcpStdio(string[] args, 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; + } + } + + 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) + { + _ = tool.GetToolMetadata(); + registry.RegisterTool(tool); + } + + IServiceScopeFactory scopeFactory = + host.Services.GetRequiredService(); + using IServiceScope scope = scopeFactory.CreateScope(); + IHostApplicationLifetime lifetime = + scope.ServiceProvider.GetRequiredService(); + Mcp.Core.IMcpStdioServer stdio = + scope.ServiceProvider.GetRequiredService(); + + stdio.RunAsync(lifetime.ApplicationStopping).GetAwaiter().GetResult(); + host.StopAsync().GetAwaiter().GetResult(); + + return true; + } + } +} From d98a4ea6197d00516779b56839cf10c5f6b71a0b Mon Sep 17 00:00:00 2001 From: aaronburtle <93220300+aaronburtle@users.noreply.github.com> Date: Tue, 9 Dec 2025 20:12:36 -0800 Subject: [PATCH 18/36] Suppress SM05137 in deserialization variable replacement settings (#3004) ## Why make this change? Silences CodeQL flag. ## What is this change? Adds the suppression language to the usage of `DefaultAzureCredential()` ## How was this tested? Against usual test suite, no real code change, just a comment. ## Sample Request(s) N/A --- src/Config/DeserializationVariableReplacementSettings.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Config/DeserializationVariableReplacementSettings.cs b/src/Config/DeserializationVariableReplacementSettings.cs index 5c70f4082b..350824409b 100644 --- a/src/Config/DeserializationVariableReplacementSettings.cs +++ b/src/Config/DeserializationVariableReplacementSettings.cs @@ -259,7 +259,7 @@ private static SecretClient CreateSecretClient(AzureKeyVaultOptions options) clientOptions.Retry.NetworkTimeout = TimeSpan.FromSeconds(options.RetryPolicy.NetworkTimeoutSeconds ?? AKVRetryPolicyOptions.DEFAULT_NETWORK_TIMEOUT_SECONDS); } - return new SecretClient(new Uri(options.Endpoint), new DefaultAzureCredential(), clientOptions); + return new SecretClient(new Uri(options.Endpoint), new DefaultAzureCredential(), clientOptions); // CodeQL [SM05137] DefaultAzureCredential will use Managed Identity if available or fallback to default. } private string? GetAkvVariable(string name) From 9dad3d510810281cecbec8d6f20df492c4abb6d7 Mon Sep 17 00:00:00 2001 From: Souvik Ghosh Date: Wed, 17 Dec 2025 09:37:19 +0530 Subject: [PATCH 19/36] Add entity-level MCP configuration support (#2989) ## Why make this change? This change allows entity-level MCP configuration to control which entities participate in MCP runtime tools, providing granular control over DML operations and custom tool exposure. - Closes on #2948 ## What is this change? This change introduces an optional mcp property at the entity level that controls participation in MCP's runtime tools. This is a prerequisite for custom tools support. The MCP property supports two formats: - **Boolean shorthand**: `"mcp": true` or `"mcp": false` - **Object format**: `{"dml-tools": boolean, "custom-tool": boolean}` Property Behavior: 1. Boolean Shorthand (`"mcp": true/false`) - `"mcp": true`: Enables DML tools only; custom tools remain disabled. - `"mcp": false`: Disables all MCP functionality for the entity. 2. Object Format `("mcp": { ... })` - `{ "dml-tools": true, "custom-tool": true }`: Enables both (valid only for stored procedures). - `{ "dml-tools": true, "custom-tool": false }`: DML only. - `{ "dml-tools": false, "custom-tool": true }`: Custom tool only (stored procedures). - `{ "dml-tools": false, "custom-tool": false }`: Fully disabled. Single-property cases: - `{"dml-tools": true}`: Enables DML only; auto-serializes to `"mcp": true`. - `{"custom-tool": true}`: Enables custom tool only; serializes as given. 3. No MCP Configuration in Entity (default) - `dml-tools` will still be enabled by default and no other change is behavior ## How was this tested? - [x] Unit Tests - [x] Integrations Tests - [x] CLI Command Testing Sample CLI commands: Add table with DML tools enabled `dab add Book --source books --permissions "anonymous:*" --mcp.dml-tools true` Add stored procedure with custom tool enabled `dab add GetBookById --source dbo.get_book_by_id --source.type stored-procedure --permissions "anonymous:execute" --mcp.custom-tool true` Add stored procedure with both properties `dab add UpdateBook --source dbo.update_book --source.type stored-procedure --permissions "anonymous:execute" --mcp.custom-tool true --mcp.dml-tools false` --- schemas/dab.draft.schema.json | 54 ++- src/Cli.Tests/AddEntityTests.cs | 254 +++++++++++ src/Cli.Tests/ModuleInitializer.cs | 4 + ...rocedureWithBothMcpProperties.verified.txt | 61 +++ ...eWithBothMcpPropertiesEnabled.verified.txt | 61 +++ ...edureWithMcpCustomToolEnabled.verified.txt | 61 +++ ...DmlTools=false_source=authors.verified.txt | 57 +++ ...mcpDmlTools=true_source=books.verified.txt | 57 +++ ...rocedureWithBothMcpProperties.verified.txt | 64 +++ ...eWithBothMcpPropertiesEnabled.verified.txt | 64 +++ ...edureWithMcpCustomToolEnabled.verified.txt | 64 +++ ...DmlTools_newMcpDmlTools=false.verified.txt | 61 +++ ...pDmlTools_newMcpDmlTools=true.verified.txt | 61 +++ src/Cli.Tests/UpdateEntityTests.cs | 422 +++++++++++++++++- src/Cli/Commands/AddOptions.cs | 6 +- src/Cli/Commands/EntityOptions.cs | 12 +- src/Cli/Commands/UpdateOptions.cs | 6 +- src/Cli/ConfigGenerator.cs | 38 +- src/Cli/Utils.cs | 51 +++ .../EntityMcpOptionsConverterFactory.cs | 123 +++++ src/Config/ObjectModel/Entity.cs | 8 +- src/Config/ObjectModel/EntityMcpOptions.cs | 59 +++ src/Config/RuntimeConfigLoader.cs | 1 + .../EntityMcpConfigurationTests.cs | 389 ++++++++++++++++ src/Service.Tests/ModuleInitializer.cs | 4 + 25 files changed, 2033 insertions(+), 9 deletions(-) create mode 100644 src/Cli.Tests/Snapshots/AddEntityTests.AddStoredProcedureWithBothMcpProperties.verified.txt create mode 100644 src/Cli.Tests/Snapshots/AddEntityTests.AddStoredProcedureWithBothMcpPropertiesEnabled.verified.txt create mode 100644 src/Cli.Tests/Snapshots/AddEntityTests.AddStoredProcedureWithMcpCustomToolEnabled.verified.txt create mode 100644 src/Cli.Tests/Snapshots/AddEntityTests.AddTableEntityWithMcpDmlTools_mcpDmlTools=false_source=authors.verified.txt create mode 100644 src/Cli.Tests/Snapshots/AddEntityTests.AddTableEntityWithMcpDmlTools_mcpDmlTools=true_source=books.verified.txt create mode 100644 src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateStoredProcedureWithBothMcpProperties.verified.txt create mode 100644 src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateStoredProcedureWithBothMcpPropertiesEnabled.verified.txt create mode 100644 src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateStoredProcedureWithMcpCustomToolEnabled.verified.txt create mode 100644 src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateTableEntityWithMcpDmlTools_newMcpDmlTools=false.verified.txt create mode 100644 src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateTableEntityWithMcpDmlTools_newMcpDmlTools=true.verified.txt create mode 100644 src/Config/Converters/EntityMcpOptionsConverterFactory.cs create mode 100644 src/Config/ObjectModel/EntityMcpOptions.cs create mode 100644 src/Service.Tests/Configuration/EntityMcpConfigurationTests.cs diff --git a/schemas/dab.draft.schema.json b/schemas/dab.draft.schema.json index 80cfd953ad..61ab7474b7 100644 --- a/schemas/dab.draft.schema.json +++ b/schemas/dab.draft.schema.json @@ -695,7 +695,7 @@ }, "entities": { "type": "object", - "description": "Entities that will be exposed via REST and/or GraphQL", + "description": "Entities that will be exposed via REST, GraphQL and/or MCP", "patternProperties": { "^.*$": { "type": "object", @@ -961,6 +961,31 @@ "default": 5 } } + }, + "mcp": { + "oneOf": [ + { + "type": "boolean", + "description": "Boolean shorthand: true enables dml-tools only (custom-tool remains false), false disables all MCP functionality." + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "dml-tools": { + "type": "boolean", + "description": "Enable MCP DML (Data Manipulation Language) tools for this entity. Allows CRUD operations via MCP.", + "default": true + }, + "custom-tool": { + "type": "boolean", + "description": "Enable MCP custom tool for this entity. Only valid for stored procedures.", + "default": false + } + } + } + ], + "description": "Model Context Protocol (MCP) configuration for this entity. Controls whether the entity is exposed via MCP tools." } }, "if": { @@ -1145,6 +1170,33 @@ ] } } + }, + { + "if": { + "properties": { + "mcp": { + "properties": { + "custom-tool": { + "const": true + } + } + } + }, + "required": ["mcp"] + }, + "then": { + "properties": { + "source": { + "properties": { + "type": { + "const": "stored-procedure" + } + }, + "required": ["type"] + } + }, + "errorMessage": "custom-tool can only be enabled for entities with source type 'stored-procedure'." + } } ] } diff --git a/src/Cli.Tests/AddEntityTests.cs b/src/Cli.Tests/AddEntityTests.cs index 9386916f7f..e96d131880 100644 --- a/src/Cli.Tests/AddEntityTests.cs +++ b/src/Cli.Tests/AddEntityTests.cs @@ -633,5 +633,259 @@ private Task ExecuteVerifyTest(AddOptions options, string config = INITIAL_CONFI return Verify(updatedRuntimeConfig, settings); } + + #region MCP Entity Configuration Tests + + /// + /// Test adding table entity with MCP dml-tools enabled or disabled + /// + [DataTestMethod] + [DataRow("true", "books", "Book", DisplayName = "AddTableEntityWithMcpDmlToolsEnabled")] + [DataRow("false", "authors", "Author", DisplayName = "AddTableEntityWithMcpDmlToolsDisabled")] + public Task AddTableEntityWithMcpDmlTools(string mcpDmlTools, string source, string entity) + { + AddOptions options = new( + source: source, + permissions: new string[] { "anonymous", "*" }, + entity: entity, + description: null, + sourceType: "table", + sourceParameters: null, + sourceKeyFields: null, + restRoute: null, + graphQLType: null, + fieldsToInclude: Array.Empty(), + fieldsToExclude: Array.Empty(), + policyRequest: null, + policyDatabase: null, + cacheEnabled: null, + cacheTtl: null, + config: TEST_RUNTIME_CONFIG_FILE, + restMethodsForStoredProcedure: null, + graphQLOperationForStoredProcedure: null, + parametersNameCollection: null, + parametersDescriptionCollection: null, + parametersRequiredCollection: null, + parametersDefaultCollection: null, + fieldsNameCollection: [], + fieldsAliasCollection: [], + fieldsDescriptionCollection: [], + fieldsPrimaryKeyCollection: [], + mcpDmlTools: mcpDmlTools, + mcpCustomTool: null + ); + + VerifySettings settings = new(); + settings.UseParameters(mcpDmlTools, source); + return ExecuteVerifyTest(options, settings: settings); + } + + /// + /// Test adding stored procedure with MCP custom-tool enabled (should serialize as object) + /// + [TestMethod] + public Task AddStoredProcedureWithMcpCustomToolEnabled() + { + AddOptions options = new( + source: "dbo.GetBookById", + permissions: new string[] { "anonymous", "execute" }, + entity: "GetBookById", + description: null, + sourceType: "stored-procedure", + sourceParameters: null, + sourceKeyFields: null, + restRoute: null, + graphQLType: null, + fieldsToInclude: Array.Empty(), + fieldsToExclude: Array.Empty(), + policyRequest: null, + policyDatabase: null, + cacheEnabled: null, + cacheTtl: null, + config: TEST_RUNTIME_CONFIG_FILE, + restMethodsForStoredProcedure: null, + graphQLOperationForStoredProcedure: null, + parametersNameCollection: null, + parametersDescriptionCollection: null, + parametersRequiredCollection: null, + parametersDefaultCollection: null, + fieldsNameCollection: [], + fieldsAliasCollection: [], + fieldsDescriptionCollection: [], + fieldsPrimaryKeyCollection: [], + mcpDmlTools: null, + mcpCustomTool: "true" + ); + return ExecuteVerifyTest(options); + } + + /// + /// Test adding stored procedure with both MCP properties set to different values (should serialize as object with both) + /// + [TestMethod] + public Task AddStoredProcedureWithBothMcpProperties() + { + AddOptions options = new( + source: "dbo.UpdateBook", + permissions: new string[] { "anonymous", "execute" }, + entity: "UpdateBook", + description: null, + sourceType: "stored-procedure", + sourceParameters: null, + sourceKeyFields: null, + restRoute: null, + graphQLType: null, + fieldsToInclude: Array.Empty(), + fieldsToExclude: Array.Empty(), + policyRequest: null, + policyDatabase: null, + cacheEnabled: null, + cacheTtl: null, + config: TEST_RUNTIME_CONFIG_FILE, + restMethodsForStoredProcedure: null, + graphQLOperationForStoredProcedure: null, + parametersNameCollection: null, + parametersDescriptionCollection: null, + parametersRequiredCollection: null, + parametersDefaultCollection: null, + fieldsNameCollection: [], + fieldsAliasCollection: [], + fieldsDescriptionCollection: [], + fieldsPrimaryKeyCollection: [], + mcpDmlTools: "false", + mcpCustomTool: "true" + ); + return ExecuteVerifyTest(options); + } + + /// + /// Test adding stored procedure with both MCP properties enabled (common use case) + /// + [TestMethod] + public Task AddStoredProcedureWithBothMcpPropertiesEnabled() + { + AddOptions options = new( + source: "dbo.GetAllBooks", + permissions: new string[] { "anonymous", "execute" }, + entity: "GetAllBooks", + description: null, + sourceType: "stored-procedure", + sourceParameters: null, + sourceKeyFields: null, + restRoute: null, + graphQLType: null, + fieldsToInclude: Array.Empty(), + fieldsToExclude: Array.Empty(), + policyRequest: null, + policyDatabase: null, + cacheEnabled: null, + cacheTtl: null, + config: TEST_RUNTIME_CONFIG_FILE, + restMethodsForStoredProcedure: null, + graphQLOperationForStoredProcedure: null, + parametersNameCollection: null, + parametersDescriptionCollection: null, + parametersRequiredCollection: null, + parametersDefaultCollection: null, + fieldsNameCollection: [], + fieldsAliasCollection: [], + fieldsDescriptionCollection: [], + fieldsPrimaryKeyCollection: [], + mcpDmlTools: "true", + mcpCustomTool: "true" + ); + return ExecuteVerifyTest(options); + } + + /// + /// Test that adding table entity with custom-tool fails validation + /// + [TestMethod] + public void AddTableEntityWithInvalidMcpCustomTool() + { + AddOptions options = new( + source: "reviews", + permissions: new string[] { "anonymous", "*" }, + entity: "Review", + description: null, + sourceType: "table", + sourceParameters: null, + sourceKeyFields: null, + restRoute: null, + graphQLType: null, + fieldsToInclude: Array.Empty(), + fieldsToExclude: Array.Empty(), + policyRequest: null, + policyDatabase: null, + cacheEnabled: null, + cacheTtl: null, + config: TEST_RUNTIME_CONFIG_FILE, + restMethodsForStoredProcedure: null, + graphQLOperationForStoredProcedure: null, + parametersNameCollection: null, + parametersDescriptionCollection: null, + parametersRequiredCollection: null, + parametersDefaultCollection: null, + fieldsNameCollection: [], + fieldsAliasCollection: [], + fieldsDescriptionCollection: [], + fieldsPrimaryKeyCollection: [], + mcpDmlTools: null, + mcpCustomTool: "true" + ); + + RuntimeConfigLoader.TryParseConfig(INITIAL_CONFIG, out RuntimeConfig? runtimeConfig); + + Assert.IsFalse(TryAddNewEntity(options, runtimeConfig!, out RuntimeConfig _), + "Should fail to add table entity with custom-tool enabled"); + } + + /// + /// Test that invalid MCP option value fails + /// + [DataTestMethod] + [DataRow("invalid", null, DisplayName = "Invalid dml-tools value")] + [DataRow(null, "invalid", DisplayName = "Invalid custom-tool value")] + [DataRow("yes", "no", DisplayName = "Invalid boolean-like values")] + public void AddEntityWithInvalidMcpOptions(string? mcpDmlTools, string? mcpCustomTool) + { + AddOptions options = new( + source: "MyTable", + permissions: new string[] { "anonymous", "*" }, + entity: "MyEntity", + description: null, + sourceType: "table", + sourceParameters: null, + sourceKeyFields: null, + restRoute: null, + graphQLType: null, + fieldsToInclude: Array.Empty(), + fieldsToExclude: Array.Empty(), + policyRequest: null, + policyDatabase: null, + cacheEnabled: null, + cacheTtl: null, + config: TEST_RUNTIME_CONFIG_FILE, + restMethodsForStoredProcedure: null, + graphQLOperationForStoredProcedure: null, + parametersNameCollection: null, + parametersDescriptionCollection: null, + parametersRequiredCollection: null, + parametersDefaultCollection: null, + fieldsNameCollection: [], + fieldsAliasCollection: [], + fieldsDescriptionCollection: [], + fieldsPrimaryKeyCollection: [], + mcpDmlTools: mcpDmlTools, + mcpCustomTool: mcpCustomTool + ); + + RuntimeConfigLoader.TryParseConfig(INITIAL_CONFIG, out RuntimeConfig? runtimeConfig); + + Assert.IsFalse(TryAddNewEntity(options, runtimeConfig!, out RuntimeConfig _), + "Should fail with invalid MCP option values"); + } + + #endregion MCP Entity Configuration Tests } } diff --git a/src/Cli.Tests/ModuleInitializer.cs b/src/Cli.Tests/ModuleInitializer.cs index e00dc00a89..a0c882ae74 100644 --- a/src/Cli.Tests/ModuleInitializer.cs +++ b/src/Cli.Tests/ModuleInitializer.cs @@ -65,6 +65,10 @@ public static void Init() VerifierSettings.IgnoreMember(entity => entity.IsLinkingEntity); // Ignore the UserProvidedTtlOptions. They aren't serialized to our config file, enforced by EntityCacheOptionsConverter. VerifierSettings.IgnoreMember(cacheOptions => cacheOptions.UserProvidedTtlOptions); + // Ignore the UserProvidedCustomToolEnabled. They aren't serialized to our config file, enforced by EntityMcpOptionsConverterFactory. + VerifierSettings.IgnoreMember(mcpOptions => mcpOptions.UserProvidedCustomToolEnabled); + // Ignore the UserProvidedDmlToolsEnabled. They aren't serialized to our config file, enforced by EntityMcpOptionsConverterFactory. + VerifierSettings.IgnoreMember(mcpOptions => mcpOptions.UserProvidedDmlToolsEnabled); // Ignore the IsRequestBodyStrict as that's unimportant from a test standpoint. VerifierSettings.IgnoreMember(config => config.IsRequestBodyStrict); // Ignore the IsGraphQLEnabled as that's unimportant from a test standpoint. 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..d7d3ed0056 --- /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: StaticWebApps + } + } + }, + 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..aa30025561 --- /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: StaticWebApps + } + } + }, + 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..576e84f6d8 --- /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: StaticWebApps + } + } + }, + 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..51a278d2a3 --- /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: StaticWebApps + } + } + }, + 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..4dc41a4d45 --- /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: StaticWebApps + } + } + }, + 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/UpdateEntityTests.TestUpdateStoredProcedureWithBothMcpProperties.verified.txt b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateStoredProcedureWithBothMcpProperties.verified.txt new file mode 100644 index 0000000000..627e8e9e01 --- /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: StaticWebApps, + 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..47d181d59d --- /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: StaticWebApps, + 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..4cb7fb45ef --- /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: StaticWebApps, + 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..42cb419190 --- /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: StaticWebApps, + 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..4084b29397 --- /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: StaticWebApps, + 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/UpdateEntityTests.cs b/src/Cli.Tests/UpdateEntityTests.cs index 3a106c0adc..2cc03dd8f8 100644 --- a/src/Cli.Tests/UpdateEntityTests.cs +++ b/src/Cli.Tests/UpdateEntityTests.cs @@ -1160,7 +1160,9 @@ private static UpdateOptions GenerateBaseUpdateOptions( string? graphQLOperationForStoredProcedure = null, string? cacheEnabled = null, string? cacheTtl = null, - string? description = null + string? description = null, + string? mcpDmlTools = null, + string? mcpCustomTool = null ) { return new( @@ -1197,7 +1199,9 @@ private static UpdateOptions GenerateBaseUpdateOptions( fieldsNameCollection: null, fieldsAliasCollection: null, fieldsDescriptionCollection: null, - fieldsPrimaryKeyCollection: null + fieldsPrimaryKeyCollection: null, + mcpDmlTools: mcpDmlTools, + mcpCustomTool: mcpCustomTool ); } @@ -1211,5 +1215,419 @@ private Task ExecuteVerifyTest(string initialConfig, UpdateOptions options, Veri return Verify(updatedRuntimeConfig, settings); } + + #region MCP Entity Configuration Tests + + /// + /// Test updating table entity with MCP dml-tools from false to true, or true to false + /// Tests actual update scenario where existing MCP config is modified + /// + [DataTestMethod] + [DataRow("true", "false", DisplayName = "TestUpdateTableEntityWithMcpDmlToolsEnabled")] + [DataRow("false", "true", DisplayName = "TestUpdateTableEntityWithMcpDmlToolsDisabled")] + public Task TestUpdateTableEntityWithMcpDmlTools(string newMcpDmlTools, string initialMcpDmlTools) + { + UpdateOptions options = new( + source: null, + permissions: null, + entity: "MyEntity", + sourceType: null, + sourceParameters: null, + sourceKeyFields: null, + restRoute: null, + graphQLType: null, + fieldsToInclude: null, + fieldsToExclude: null, + policyRequest: null, + policyDatabase: null, + relationship: null, + cardinality: null, + targetEntity: null, + linkingObject: null, + linkingSourceFields: null, + linkingTargetFields: null, + relationshipFields: null, + map: null, + cacheEnabled: null, + cacheTtl: null, + config: TEST_RUNTIME_CONFIG_FILE, + restMethodsForStoredProcedure: null, + graphQLOperationForStoredProcedure: null, + description: null, + parametersNameCollection: null, + parametersDescriptionCollection: null, + parametersRequiredCollection: null, + parametersDefaultCollection: null, + fieldsNameCollection: null, + fieldsAliasCollection: null, + fieldsDescriptionCollection: null, + fieldsPrimaryKeyCollection: null, + mcpDmlTools: newMcpDmlTools, + mcpCustomTool: null + ); + + string initialConfig = GetInitialConfigString() + "," + @" + ""entities"": { + ""MyEntity"": { + ""source"": ""MyTable"", + ""permissions"": [ + { + ""role"": ""anonymous"", + ""actions"": [""*""] + } + ], + ""mcp"": " + initialMcpDmlTools + @" + } + } + }"; + + VerifySettings settings = new(); + settings.UseParameters(newMcpDmlTools); + return ExecuteVerifyTest(initialConfig, options, settings: settings); + } + + /// + /// Test updating stored procedure with MCP custom-tool from false to true + /// Tests actual update scenario where existing MCP config is modified + /// + [TestMethod] + public Task TestUpdateStoredProcedureWithMcpCustomToolEnabled() + { + UpdateOptions options = new( + source: null, + permissions: null, + entity: "GetBookById", + sourceType: null, + sourceParameters: null, + sourceKeyFields: null, + restRoute: null, + graphQLType: null, + fieldsToInclude: null, + fieldsToExclude: null, + policyRequest: null, + policyDatabase: null, + relationship: null, + cardinality: null, + targetEntity: null, + linkingObject: null, + linkingSourceFields: null, + linkingTargetFields: null, + relationshipFields: null, + map: null, + cacheEnabled: null, + cacheTtl: null, + config: TEST_RUNTIME_CONFIG_FILE, + restMethodsForStoredProcedure: null, + graphQLOperationForStoredProcedure: null, + description: null, + parametersNameCollection: null, + parametersDescriptionCollection: null, + parametersRequiredCollection: null, + parametersDefaultCollection: null, + fieldsNameCollection: null, + fieldsAliasCollection: null, + fieldsDescriptionCollection: null, + fieldsPrimaryKeyCollection: null, + mcpDmlTools: null, + mcpCustomTool: "true" + ); + + string initialConfig = GetInitialConfigString() + "," + @" + ""entities"": { + ""GetBookById"": { + ""source"": ""dbo.GetBookById"", + ""source"": { + ""type"": ""stored-procedure"" + }, + ""permissions"": [ + { + ""role"": ""anonymous"", + ""actions"": [""execute""] + } + ], + ""mcp"": { + ""custom-tool"": false + } + } + } + }"; + + return ExecuteVerifyTest(initialConfig, options); + } + + /// + /// Test updating stored procedure with both MCP properties + /// Updates from both true to custom-tool=true, dml-tools=false + /// + [TestMethod] + public Task TestUpdateStoredProcedureWithBothMcpProperties() + { + UpdateOptions options = new( + source: null, + permissions: null, + entity: "UpdateBook", + sourceType: null, + sourceParameters: null, + sourceKeyFields: null, + restRoute: null, + graphQLType: null, + fieldsToInclude: null, + fieldsToExclude: null, + policyRequest: null, + policyDatabase: null, + relationship: null, + cardinality: null, + targetEntity: null, + linkingObject: null, + linkingSourceFields: null, + linkingTargetFields: null, + relationshipFields: null, + map: null, + cacheEnabled: null, + cacheTtl: null, + config: TEST_RUNTIME_CONFIG_FILE, + restMethodsForStoredProcedure: null, + graphQLOperationForStoredProcedure: null, + description: null, + parametersNameCollection: null, + parametersDescriptionCollection: null, + parametersRequiredCollection: null, + parametersDefaultCollection: null, + fieldsNameCollection: null, + fieldsAliasCollection: null, + fieldsDescriptionCollection: null, + fieldsPrimaryKeyCollection: null, + mcpDmlTools: "false", + mcpCustomTool: "true" + ); + + string initialConfig = GetInitialConfigString() + "," + @" + ""entities"": { + ""UpdateBook"": { + ""source"": ""dbo.UpdateBook"", + ""source"": { + ""type"": ""stored-procedure"" + }, + ""permissions"": [ + { + ""role"": ""anonymous"", + ""actions"": [""execute""] + } + ], + ""mcp"": { + ""custom-tool"": false, + ""dml-tools"": true + } + } + } + }"; + + return ExecuteVerifyTest(initialConfig, options); + } + + /// + /// Test updating stored procedure with both MCP properties enabled + /// Updates from both false to both true + /// + [TestMethod] + public Task TestUpdateStoredProcedureWithBothMcpPropertiesEnabled() + { + UpdateOptions options = new( + source: null, + permissions: null, + entity: "GetAllBooks", + sourceType: null, + sourceParameters: null, + sourceKeyFields: null, + restRoute: null, + graphQLType: null, + fieldsToInclude: null, + fieldsToExclude: null, + policyRequest: null, + policyDatabase: null, + relationship: null, + cardinality: null, + targetEntity: null, + linkingObject: null, + linkingSourceFields: null, + linkingTargetFields: null, + relationshipFields: null, + map: null, + cacheEnabled: null, + cacheTtl: null, + config: TEST_RUNTIME_CONFIG_FILE, + restMethodsForStoredProcedure: null, + graphQLOperationForStoredProcedure: null, + description: null, + parametersNameCollection: null, + parametersDescriptionCollection: null, + parametersRequiredCollection: null, + parametersDefaultCollection: null, + fieldsNameCollection: null, + fieldsAliasCollection: null, + fieldsDescriptionCollection: null, + fieldsPrimaryKeyCollection: null, + mcpDmlTools: "true", + mcpCustomTool: "true" + ); + + string initialConfig = GetInitialConfigString() + "," + @" + ""entities"": { + ""GetAllBooks"": { + ""source"": ""dbo.GetAllBooks"", + ""source"": { + ""type"": ""stored-procedure"" + }, + ""permissions"": [ + { + ""role"": ""anonymous"", + ""actions"": [""execute""] + } + ], + ""mcp"": { + ""custom-tool"": false, + ""dml-tools"": false + } + } + } + }"; + + return ExecuteVerifyTest(initialConfig, options); + } + + /// + /// Test that updating table entity with custom-tool fails validation + /// + [TestMethod] + public void TestUpdateTableEntityWithInvalidMcpCustomTool() + { + UpdateOptions options = new( + source: null, + permissions: null, + entity: "MyEntity", + sourceType: null, + sourceParameters: null, + sourceKeyFields: null, + restRoute: null, + graphQLType: null, + fieldsToInclude: null, + fieldsToExclude: null, + policyRequest: null, + policyDatabase: null, + relationship: null, + cardinality: null, + targetEntity: null, + linkingObject: null, + linkingSourceFields: null, + linkingTargetFields: null, + relationshipFields: null, + map: null, + cacheEnabled: null, + cacheTtl: null, + config: TEST_RUNTIME_CONFIG_FILE, + restMethodsForStoredProcedure: null, + graphQLOperationForStoredProcedure: null, + description: null, + parametersNameCollection: null, + parametersDescriptionCollection: null, + parametersRequiredCollection: null, + parametersDefaultCollection: null, + fieldsNameCollection: null, + fieldsAliasCollection: null, + fieldsDescriptionCollection: null, + fieldsPrimaryKeyCollection: null, + mcpDmlTools: null, + mcpCustomTool: "true" + ); + + string initialConfig = GetInitialConfigString() + "," + @" + ""entities"": { + ""MyEntity"": { + ""source"": ""MyTable"", + ""permissions"": [ + { + ""role"": ""anonymous"", + ""actions"": [""*""] + } + ] + } + } + }"; + + Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(initialConfig, out RuntimeConfig? runtimeConfig)); + + Assert.IsFalse(TryUpdateExistingEntity(options, runtimeConfig!, out RuntimeConfig _), + "Should fail to update table entity with custom-tool enabled"); + } + + /// + /// Test that invalid MCP option value fails + /// + [DataTestMethod] + [DataRow("invalid", null, DisplayName = "Invalid dml-tools value")] + [DataRow(null, "invalid", DisplayName = "Invalid custom-tool value")] + [DataRow("yes", "no", DisplayName = "Invalid boolean-like values")] + public void TestUpdateEntityWithInvalidMcpOptions(string? mcpDmlTools, string? mcpCustomTool) + { + UpdateOptions options = new( + source: null, + permissions: null, + entity: "MyEntity", + sourceType: null, + sourceParameters: null, + sourceKeyFields: null, + restRoute: null, + graphQLType: null, + fieldsToInclude: null, + fieldsToExclude: null, + policyRequest: null, + policyDatabase: null, + relationship: null, + cardinality: null, + targetEntity: null, + linkingObject: null, + linkingSourceFields: null, + linkingTargetFields: null, + relationshipFields: null, + map: null, + cacheEnabled: null, + cacheTtl: null, + config: TEST_RUNTIME_CONFIG_FILE, + restMethodsForStoredProcedure: null, + graphQLOperationForStoredProcedure: null, + description: null, + parametersNameCollection: null, + parametersDescriptionCollection: null, + parametersRequiredCollection: null, + parametersDefaultCollection: null, + fieldsNameCollection: null, + fieldsAliasCollection: null, + fieldsDescriptionCollection: null, + fieldsPrimaryKeyCollection: null, + mcpDmlTools: mcpDmlTools, + mcpCustomTool: mcpCustomTool + ); + + string initialConfig = GetInitialConfigString() + "," + @" + ""entities"": { + ""MyEntity"": { + ""source"": ""MyTable"", + ""permissions"": [ + { + ""role"": ""anonymous"", + ""actions"": [""*""] + } + ] + } + } + }"; + + Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(initialConfig, out RuntimeConfig? runtimeConfig)); + + Assert.IsFalse(TryUpdateExistingEntity(options, runtimeConfig!, out RuntimeConfig _), + $"Should fail to update entity with invalid MCP options: dml-tools={mcpDmlTools}, custom-tool={mcpCustomTool}"); + } + + #endregion MCP Entity Configuration Tests } } diff --git a/src/Cli/Commands/AddOptions.cs b/src/Cli/Commands/AddOptions.cs index b7d9fbeb08..e7e378d94b 100644 --- a/src/Cli/Commands/AddOptions.cs +++ b/src/Cli/Commands/AddOptions.cs @@ -43,7 +43,9 @@ public AddOptions( IEnumerable? fieldsAliasCollection, IEnumerable? fieldsDescriptionCollection, IEnumerable? fieldsPrimaryKeyCollection, - string? config + string? mcpDmlTools = null, + string? mcpCustomTool = null, + string? config = null ) : base( entity, @@ -69,6 +71,8 @@ public AddOptions( fieldsAliasCollection, fieldsDescriptionCollection, fieldsPrimaryKeyCollection, + mcpDmlTools, + mcpCustomTool, config ) { diff --git a/src/Cli/Commands/EntityOptions.cs b/src/Cli/Commands/EntityOptions.cs index 7f26816800..3b2b77d9b2 100644 --- a/src/Cli/Commands/EntityOptions.cs +++ b/src/Cli/Commands/EntityOptions.cs @@ -34,7 +34,9 @@ public EntityOptions( IEnumerable? fieldsAliasCollection, IEnumerable? fieldsDescriptionCollection, IEnumerable? fieldsPrimaryKeyCollection, - string? config + string? mcpDmlTools = null, + string? mcpCustomTool = null, + string? config = null ) : base(config) { @@ -61,6 +63,8 @@ public EntityOptions( FieldsAliasCollection = fieldsAliasCollection; FieldsDescriptionCollection = fieldsDescriptionCollection; FieldsPrimaryKeyCollection = fieldsPrimaryKeyCollection; + McpDmlTools = mcpDmlTools; + McpCustomTool = mcpCustomTool; } // Entity is required but we have made required as false to have custom error message (more user friendly), if not provided. @@ -132,5 +136,11 @@ public EntityOptions( [Option("fields.primary-key", Required = false, Separator = ',', HelpText = "Set this field as a primary key.")] public IEnumerable? FieldsPrimaryKeyCollection { get; } + + [Option("mcp.dml-tools", Required = false, HelpText = "Enable MCP DML (Data Manipulation Language) tools for this entity. Allows CRUD operations via MCP. Default value is true.")] + public string? McpDmlTools { get; } + + [Option("mcp.custom-tool", Required = false, HelpText = "Enable MCP custom tool for this entity. Only valid for stored procedures. Default value is false.")] + public string? McpCustomTool { get; } } } diff --git a/src/Cli/Commands/UpdateOptions.cs b/src/Cli/Commands/UpdateOptions.cs index fe1664c5bb..050afa2ddb 100644 --- a/src/Cli/Commands/UpdateOptions.cs +++ b/src/Cli/Commands/UpdateOptions.cs @@ -51,7 +51,9 @@ public UpdateOptions( IEnumerable? fieldsAliasCollection, IEnumerable? fieldsDescriptionCollection, IEnumerable? fieldsPrimaryKeyCollection, - string? config) + string? mcpDmlTools = null, + string? mcpCustomTool = null, + string? config = null) : base(entity, sourceType, sourceParameters, @@ -75,6 +77,8 @@ public UpdateOptions( fieldsAliasCollection, fieldsDescriptionCollection, fieldsPrimaryKeyCollection, + mcpDmlTools, + mcpCustomTool, config) { Source = source; diff --git a/src/Cli/ConfigGenerator.cs b/src/Cli/ConfigGenerator.cs index 1d673c11e3..8fcc7cae31 100644 --- a/src/Cli/ConfigGenerator.cs +++ b/src/Cli/ConfigGenerator.cs @@ -449,6 +449,18 @@ public static bool TryAddNewEntity(AddOptions options, RuntimeConfig initialRunt EntityRestOptions restOptions = ConstructRestOptions(options.RestRoute, SupportedRestMethods, initialRuntimeConfig.DataSource.DatabaseType == DatabaseType.CosmosDB_NoSQL); EntityGraphQLOptions graphqlOptions = ConstructGraphQLTypeDetails(options.GraphQLType, graphQLOperationsForStoredProcedures); EntityCacheOptions? cacheOptions = ConstructCacheOptions(options.CacheEnabled, options.CacheTtl); + EntityMcpOptions? mcpOptions = null; + + if (options.McpDmlTools is not null || options.McpCustomTool is not null) + { + mcpOptions = ConstructMcpOptions(options.McpDmlTools, options.McpCustomTool, isStoredProcedure); + + if (mcpOptions is null) + { + _logger.LogError("Failed to construct MCP options."); + return false; + } + } // Create new entity. Entity entity = new( @@ -460,7 +472,8 @@ public static bool TryAddNewEntity(AddOptions options, RuntimeConfig initialRunt Relationships: null, Mappings: null, Cache: cacheOptions, - Description: string.IsNullOrWhiteSpace(options.Description) ? null : options.Description); + Description: string.IsNullOrWhiteSpace(options.Description) ? null : options.Description, + Mcp: mcpOptions); // Add entity to existing runtime config. IDictionary entities = new Dictionary(initialRuntimeConfig.Entities.Entities) @@ -1621,6 +1634,26 @@ public static bool TryUpdateExistingEntity(UpdateOptions options, RuntimeConfig EntityActionFields? updatedFields = GetFieldsForOperation(options.FieldsToInclude, options.FieldsToExclude); EntityCacheOptions? updatedCacheOptions = ConstructCacheOptions(options.CacheEnabled, options.CacheTtl); + // Determine if the entity is or will be a stored procedure + bool isStoredProcedureAfterUpdate = doOptionsRepresentStoredProcedure || (isCurrentEntityStoredProcedure && options.SourceType is null); + + // Construct and validate MCP options if provided + EntityMcpOptions? updatedMcpOptions = null; + if (options.McpDmlTools is not null || options.McpCustomTool is not null) + { + updatedMcpOptions = ConstructMcpOptions(options.McpDmlTools, options.McpCustomTool, isStoredProcedureAfterUpdate); + if (updatedMcpOptions is null) + { + _logger.LogError("Failed to construct MCP options."); + return false; + } + } + else + { + // Keep existing MCP options if no updates provided + updatedMcpOptions = entity.Mcp; + } + if (!updatedGraphQLDetails.Enabled) { _logger.LogWarning("Disabling GraphQL for this entity will restrict its usage in relationships"); @@ -1857,7 +1890,8 @@ public static bool TryUpdateExistingEntity(UpdateOptions options, RuntimeConfig Relationships: updatedRelationships, Mappings: updatedMappings, Cache: updatedCacheOptions, - Description: string.IsNullOrWhiteSpace(options.Description) ? entity.Description : options.Description + Description: string.IsNullOrWhiteSpace(options.Description) ? entity.Description : options.Description, + Mcp: updatedMcpOptions ); IDictionary entities = new Dictionary(initialConfig.Entities.Entities) { diff --git a/src/Cli/Utils.cs b/src/Cli/Utils.cs index 451c330503..48edd4411c 100644 --- a/src/Cli/Utils.cs +++ b/src/Cli/Utils.cs @@ -892,6 +892,57 @@ public static EntityGraphQLOptions ConstructGraphQLTypeDetails(string? graphQL, return cacheOptions with { Enabled = isEnabled, TtlSeconds = ttl, UserProvidedTtlOptions = isCacheTtlUserProvided }; } + /// + /// Constructs the EntityMcpOptions for Add/Update. + /// + /// String value that defines if DML tools are enabled for MCP. + /// String value that defines if custom tool is enabled for MCP. + /// Whether the entity is a stored procedure. + /// EntityMcpOptions if values are provided, null otherwise. + public static EntityMcpOptions? ConstructMcpOptions(string? mcpDmlTools, string? mcpCustomTool, bool isStoredProcedure) + { + if (mcpDmlTools is null && mcpCustomTool is null) + { + return null; + } + + bool? dmlToolsEnabled = null; + bool? customToolEnabled = null; + + // Parse dml-tools option + if (mcpDmlTools is not null) + { + if (!bool.TryParse(mcpDmlTools, out bool dmlValue)) + { + _logger.LogError("Invalid format for --mcp.dml-tools. Accepted values are true/false."); + return null; + } + + dmlToolsEnabled = dmlValue; + } + + // Parse custom-tool option + if (mcpCustomTool is not null) + { + if (!bool.TryParse(mcpCustomTool, out bool customValue)) + { + _logger.LogError("Invalid format for --mcp.custom-tool. Accepted values are true/false."); + return null; + } + + // Validate that custom-tool can only be used with stored procedures + if (customValue && !isStoredProcedure) + { + _logger.LogError("--mcp.custom-tool can only be enabled for stored procedures."); + return null; + } + + customToolEnabled = customValue; + } + + return new EntityMcpOptions(customToolEnabled, dmlToolsEnabled); + } + /// /// Check if add/update command has Entity provided. Return false otherwise. /// diff --git a/src/Config/Converters/EntityMcpOptionsConverterFactory.cs b/src/Config/Converters/EntityMcpOptionsConverterFactory.cs new file mode 100644 index 0000000000..b4ad0e9170 --- /dev/null +++ b/src/Config/Converters/EntityMcpOptionsConverterFactory.cs @@ -0,0 +1,123 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json; +using System.Text.Json.Serialization; +using Azure.DataApiBuilder.Config.ObjectModel; + +namespace Azure.DataApiBuilder.Config.Converters; + +/// +/// Factory for creating EntityMcpOptions converters. +/// +internal class EntityMcpOptionsConverterFactory : JsonConverterFactory +{ + public override bool CanConvert(Type typeToConvert) + { + return typeToConvert == typeof(EntityMcpOptions); + } + + public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) + { + return new EntityMcpOptionsConverter(); + } + + /// + /// Converter for EntityMcpOptions that handles both boolean and object representations. + /// When boolean: true enables dml-tools and custom-tool remains false (default), false disables dml-tools and custom-tool remains false. + /// When object: can specify individual properties (custom-tool and dml-tools). + /// + private class EntityMcpOptionsConverter : JsonConverter + { + public override EntityMcpOptions? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.Null) + { + return null; + } + + // Handle boolean shorthand: true/false + if (reader.TokenType == JsonTokenType.True || reader.TokenType == JsonTokenType.False) + { + bool value = reader.GetBoolean(); + // Boolean true means: dml-tools=true, custom-tool=false (default) + // Boolean false means: dml-tools=false, custom-tool=false + // Pass null for customToolEnabled to keep it as default (not user-provided) + return new EntityMcpOptions( + customToolEnabled: null, + dmlToolsEnabled: value + ); + } + + // Handle object representation + if (reader.TokenType == JsonTokenType.StartObject) + { + bool? customToolEnabled = null; + bool? dmlToolsEnabled = null; + + while (reader.Read()) + { + if (reader.TokenType == JsonTokenType.EndObject) + { + return new EntityMcpOptions(customToolEnabled, dmlToolsEnabled); + } + + if (reader.TokenType == JsonTokenType.PropertyName) + { + string? propertyName = reader.GetString(); + reader.Read(); // Move to the value + + switch (propertyName) + { + case "custom-tool": + customToolEnabled = reader.GetBoolean(); + break; + case "dml-tools": + dmlToolsEnabled = reader.GetBoolean(); + break; + default: + throw new JsonException($"Unknown property '{propertyName}' in EntityMcpOptions"); + } + } + } + } + + throw new JsonException($"Unexpected token type {reader.TokenType} for EntityMcpOptions"); + } + + public override void Write(Utf8JsonWriter writer, EntityMcpOptions value, JsonSerializerOptions options) + { + if (value == null) + { + return; + } + + // Check if we should write as boolean shorthand + // Write as boolean if: only dml-tools is set (or custom-tool is default false) + bool writeAsBoolean = !value.UserProvidedCustomToolEnabled && value.UserProvidedDmlToolsEnabled; + + if (writeAsBoolean) + { + // Write as boolean shorthand + writer.WriteBooleanValue(value.DmlToolEnabled); + } + else if (value.UserProvidedCustomToolEnabled || value.UserProvidedDmlToolsEnabled) + { + // Write as object + writer.WriteStartObject(); + + if (value.UserProvidedCustomToolEnabled) + { + writer.WriteBoolean("custom-tool", value.CustomToolEnabled); + } + + if (value.UserProvidedDmlToolsEnabled) + { + writer.WriteBoolean("dml-tools", value.DmlToolEnabled); + } + + writer.WriteEndObject(); + } + } + } +} diff --git a/src/Config/ObjectModel/Entity.cs b/src/Config/ObjectModel/Entity.cs index c9f247e0f6..1e8c5a6dba 100644 --- a/src/Config/ObjectModel/Entity.cs +++ b/src/Config/ObjectModel/Entity.cs @@ -3,6 +3,7 @@ using System.Diagnostics.CodeAnalysis; using System.Text.Json.Serialization; +using Azure.DataApiBuilder.Config.Converters; using Azure.DataApiBuilder.Config.HealthCheck; namespace Azure.DataApiBuilder.Config.ObjectModel; @@ -39,6 +40,9 @@ public record Entity public EntityCacheOptions? Cache { get; init; } public EntityHealthCheckConfig? Health { get; init; } + [JsonConverter(typeof(EntityMcpOptionsConverterFactory))] + public EntityMcpOptions? Mcp { get; init; } + [JsonIgnore] public bool IsLinkingEntity { get; init; } @@ -54,7 +58,8 @@ public Entity( EntityCacheOptions? Cache = null, bool IsLinkingEntity = false, EntityHealthCheckConfig? Health = null, - string? Description = null) + string? Description = null, + EntityMcpOptions? Mcp = null) { this.Health = Health; this.Source = Source; @@ -67,6 +72,7 @@ public Entity( this.Cache = Cache; this.IsLinkingEntity = IsLinkingEntity; this.Description = Description; + this.Mcp = Mcp; } /// diff --git a/src/Config/ObjectModel/EntityMcpOptions.cs b/src/Config/ObjectModel/EntityMcpOptions.cs new file mode 100644 index 0000000000..ad928a21ab --- /dev/null +++ b/src/Config/ObjectModel/EntityMcpOptions.cs @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json.Serialization; + +namespace Azure.DataApiBuilder.Config.ObjectModel +{ + /// + /// Options for Model Context Protocol (MCP) tools at the entity level. + /// + public record EntityMcpOptions + { + /// + /// Indicates whether custom tools are enabled for this entity. + /// Only applicable for stored procedures. + /// + [JsonPropertyName("custom-tool")] + public bool CustomToolEnabled { get; init; } = false; + + /// + /// Indicates whether DML tools are enabled for this entity. + /// Defaults to true when not explicitly provided. + /// + [JsonPropertyName("dml-tools")] + public bool DmlToolEnabled { get; init; } = true; + + /// + /// Flag which informs CLI and JSON serializer whether to write the CustomToolEnabled + /// + [JsonIgnore(Condition = JsonIgnoreCondition.Always)] + public bool UserProvidedCustomToolEnabled { get; init; } = false; + + /// + /// Flag which informs CLI and JSON serializer whether to write the DmlToolEnabled + /// + [JsonIgnore(Condition = JsonIgnoreCondition.Always)] + public bool UserProvidedDmlToolsEnabled { get; init; } = false; + + /// + /// Constructor for EntityMcpOptions + /// + /// The custom tool enabled flag. + /// The DML tools enabled flag. + public EntityMcpOptions(bool? customToolEnabled, bool? dmlToolsEnabled) + { + if (customToolEnabled.HasValue) + { + this.CustomToolEnabled = customToolEnabled.Value; + this.UserProvidedCustomToolEnabled = true; + } + + if (dmlToolsEnabled.HasValue) + { + this.DmlToolEnabled = dmlToolsEnabled.Value; + this.UserProvidedDmlToolsEnabled = true; + } + } + } +} diff --git a/src/Config/RuntimeConfigLoader.cs b/src/Config/RuntimeConfigLoader.cs index bad5aa8680..6d6cf6d51b 100644 --- a/src/Config/RuntimeConfigLoader.cs +++ b/src/Config/RuntimeConfigLoader.cs @@ -314,6 +314,7 @@ public static JsonSerializerOptions GetSerializationOptions( options.Converters.Add(new EntityActionConverterFactory()); options.Converters.Add(new DataSourceFilesConverter()); options.Converters.Add(new EntityCacheOptionsConverterFactory(replacementSettings)); + options.Converters.Add(new EntityMcpOptionsConverterFactory()); options.Converters.Add(new RuntimeCacheOptionsConverterFactory()); options.Converters.Add(new RuntimeCacheLevel2OptionsConverterFactory()); options.Converters.Add(new MultipleCreateOptionsConverter()); diff --git a/src/Service.Tests/Configuration/EntityMcpConfigurationTests.cs b/src/Service.Tests/Configuration/EntityMcpConfigurationTests.cs new file mode 100644 index 0000000000..5ce34c9355 --- /dev/null +++ b/src/Service.Tests/Configuration/EntityMcpConfigurationTests.cs @@ -0,0 +1,389 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Azure.DataApiBuilder.Config; +using Azure.DataApiBuilder.Config.ObjectModel; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Azure.DataApiBuilder.Service.Tests.Configuration +{ + /// + /// Tests for entity-level MCP configuration deserialization and validation. + /// Validates that EntityMcpOptions are correctly deserialized from runtime config JSON. + /// + [TestClass] + public class EntityMcpConfigurationTests + { + private const string BASE_CONFIG_TEMPLATE = @"{{ + ""$schema"": ""test-schema"", + ""data-source"": {{ + ""database-type"": ""mssql"", + ""connection-string"": ""test"" + }}, + ""runtime"": {{ + ""rest"": {{ ""enabled"": true, ""path"": ""/api"" }}, + ""graphql"": {{ ""enabled"": true, ""path"": ""/graphql"" }}, + ""host"": {{ ""mode"": ""development"" }} + }}, + ""entities"": {{ + {0} + }} + }}"; + + /// + /// Helper method to create a config with specified entities JSON + /// + private static string CreateConfig(string entitiesJson) + { + return string.Format(BASE_CONFIG_TEMPLATE, entitiesJson); + } + + /// + /// Helper method to assert entity MCP configuration + /// + private static void AssertEntityMcp(Entity entity, bool? expectedDmlTools, bool? expectedCustomTool, string message = null) + { + if (expectedDmlTools == null && expectedCustomTool == null) + { + Assert.IsNull(entity.Mcp, "MCP options should be null when not specified"); + return; + } + + Assert.IsNotNull(entity.Mcp, message ?? "MCP options should be present"); + + bool actualDmlTools = entity.Mcp?.DmlToolEnabled ?? true; // Default is true + bool actualCustomTool = entity.Mcp?.CustomToolEnabled ?? false; // Default is false + + Assert.AreEqual(expectedDmlTools ?? true, actualDmlTools, + $"DmlToolEnabled should be {expectedDmlTools ?? true}"); + Assert.AreEqual(expectedCustomTool ?? false, actualCustomTool, + $"CustomToolEnabled should be {expectedCustomTool ?? false}"); + } + /// + /// Test that deserializing boolean 'true' shorthand correctly sets dml-tools enabled. + /// + [TestMethod] + public void DeserializeConfig_McpBooleanTrue_EnablesDmlToolsOnly() + { + // Arrange + string config = CreateConfig(@" + ""Book"": { + ""source"": ""books"", + ""permissions"": [{ ""role"": ""anonymous"", ""actions"": [""*""] }], + ""mcp"": true + } + "); + + // Act + bool success = RuntimeConfigLoader.TryParseConfig(config, out RuntimeConfig runtimeConfig); + + // Assert + Assert.IsTrue(success, "Config should parse successfully"); + Assert.IsNotNull(runtimeConfig); + Assert.IsTrue(runtimeConfig.Entities.ContainsKey("Book")); + AssertEntityMcp(runtimeConfig.Entities["Book"], expectedDmlTools: true, expectedCustomTool: false); + } + + /// + /// Test that deserializing boolean 'false' shorthand correctly sets dml-tools disabled. + /// + [TestMethod] + public void DeserializeConfig_McpBooleanFalse_DisablesDmlTools() + { + // Arrange + string config = CreateConfig(@" + ""Book"": { + ""source"": ""books"", + ""permissions"": [{ ""role"": ""anonymous"", ""actions"": [""*""] }], + ""mcp"": false + } + "); + + // Act + bool success = RuntimeConfigLoader.TryParseConfig(config, out RuntimeConfig runtimeConfig); + + // Assert + Assert.IsTrue(success, "Config should parse successfully"); + Assert.IsNotNull(runtimeConfig); + Assert.IsTrue(runtimeConfig.Entities.ContainsKey("Book")); + AssertEntityMcp(runtimeConfig.Entities["Book"], expectedDmlTools: false, expectedCustomTool: false); + } + + /// + /// Test that deserializing object format with both properties works correctly. + /// + [TestMethod] + public void DeserializeConfig_McpObject_SetsBothProperties() + { + // Arrange + string config = CreateConfig(@" + ""GetBook"": { + ""source"": { ""type"": ""stored-procedure"", ""object"": ""dbo.GetBook"" }, + ""permissions"": [{ ""role"": ""anonymous"", ""actions"": [""execute""] }], + ""mcp"": { + ""custom-tool"": true, + ""dml-tools"": false + } + } + "); + + // Act + bool success = RuntimeConfigLoader.TryParseConfig(config, out RuntimeConfig runtimeConfig); + + // Assert + Assert.IsTrue(success, "Config should parse successfully"); + Assert.IsNotNull(runtimeConfig); + Assert.IsTrue(runtimeConfig.Entities.ContainsKey("GetBook")); + AssertEntityMcp(runtimeConfig.Entities["GetBook"], expectedDmlTools: false, expectedCustomTool: true); + } + + /// + /// Test that deserializing object format with only dml-tools works. + /// + [TestMethod] + public void DeserializeConfig_McpObjectWithDmlToolsOnly_WorksCorrectly() + { + // Arrange + string config = CreateConfig(@" + ""Book"": { + ""source"": ""books"", + ""permissions"": [{ ""role"": ""anonymous"", ""actions"": [""*""] }], + ""mcp"": { + ""dml-tools"": true + } + } + "); + + // Act + bool success = RuntimeConfigLoader.TryParseConfig(config, out RuntimeConfig runtimeConfig); + + // Assert + Assert.IsTrue(success, "Config should parse successfully"); + Assert.IsNotNull(runtimeConfig); + Assert.IsTrue(runtimeConfig.Entities.ContainsKey("Book")); + AssertEntityMcp(runtimeConfig.Entities["Book"], expectedDmlTools: true, expectedCustomTool: false); + } + + /// + /// Test that entity without MCP configuration has null Mcp property. + /// + [TestMethod] + public void DeserializeConfig_NoMcp_HasNullMcpOptions() + { + // Arrange + string config = CreateConfig(@" + ""Book"": { + ""source"": ""books"", + ""permissions"": [{ ""role"": ""anonymous"", ""actions"": [""*""] }] + } + "); + + // Act + bool success = RuntimeConfigLoader.TryParseConfig(config, out RuntimeConfig runtimeConfig); + + // Assert + Assert.IsTrue(success, "Config should parse successfully"); + Assert.IsNotNull(runtimeConfig); + Assert.IsTrue(runtimeConfig.Entities.ContainsKey("Book")); + Assert.IsNull(runtimeConfig.Entities["Book"].Mcp, "MCP options should be null when not specified"); + } + + /// + /// Test that deserializing object format with both properties set to true works correctly. + /// + [TestMethod] + public void DeserializeConfig_McpObjectWithBothTrue_SetsCorrectly() + { + // Arrange + string config = CreateConfig(@" + ""GetBook"": { + ""source"": { ""type"": ""stored-procedure"", ""object"": ""dbo.GetBook"" }, + ""permissions"": [{ ""role"": ""anonymous"", ""actions"": [""execute""] }], + ""mcp"": { + ""custom-tool"": true, + ""dml-tools"": true + } + } + "); + + // Act + bool success = RuntimeConfigLoader.TryParseConfig(config, out RuntimeConfig runtimeConfig); + + // Assert + Assert.IsTrue(success, "Config should parse successfully"); + Assert.IsNotNull(runtimeConfig); + Assert.IsTrue(runtimeConfig.Entities.ContainsKey("GetBook")); + AssertEntityMcp(runtimeConfig.Entities["GetBook"], expectedDmlTools: true, expectedCustomTool: true); + } + + /// + /// Test that deserializing object format with both properties set to false works correctly. + /// + [TestMethod] + public void DeserializeConfig_McpObjectWithBothFalse_SetsCorrectly() + { + // Arrange + string config = CreateConfig(@" + ""GetBook"": { + ""source"": { ""type"": ""stored-procedure"", ""object"": ""dbo.GetBook"" }, + ""permissions"": [{ ""role"": ""anonymous"", ""actions"": [""execute""] }], + ""mcp"": { + ""custom-tool"": false, + ""dml-tools"": false + } + } + "); + + // Act + bool success = RuntimeConfigLoader.TryParseConfig(config, out RuntimeConfig runtimeConfig); + + // Assert + Assert.IsTrue(success, "Config should parse successfully"); + Assert.IsNotNull(runtimeConfig); + Assert.IsTrue(runtimeConfig.Entities.ContainsKey("GetBook")); + AssertEntityMcp(runtimeConfig.Entities["GetBook"], expectedDmlTools: false, expectedCustomTool: false); + } + + /// + /// Test that deserializing object format with only custom-tool works. + /// + [TestMethod] + public void DeserializeConfig_McpObjectWithCustomToolOnly_WorksCorrectly() + { + // Arrange + string config = CreateConfig(@" + ""GetBook"": { + ""source"": { ""type"": ""stored-procedure"", ""object"": ""dbo.GetBook"" }, + ""permissions"": [{ ""role"": ""anonymous"", ""actions"": [""execute""] }], + ""mcp"": { + ""custom-tool"": true + } + } + "); + + // Act + bool success = RuntimeConfigLoader.TryParseConfig(config, out RuntimeConfig runtimeConfig); + + // Assert + Assert.IsTrue(success, "Config should parse successfully"); + Assert.IsNotNull(runtimeConfig); + Assert.IsTrue(runtimeConfig.Entities.ContainsKey("GetBook")); + AssertEntityMcp(runtimeConfig.Entities["GetBook"], expectedDmlTools: true, expectedCustomTool: true); + } + + /// + /// Test that deserializing config with multiple entities having different MCP settings works. + /// + [TestMethod] + public void DeserializeConfig_MultipleEntitiesWithDifferentMcpSettings_WorksCorrectly() + { + // Arrange + string config = @"{ + ""$schema"": ""test-schema"", + ""data-source"": { + ""database-type"": ""mssql"", + ""connection-string"": ""test"" + }, + ""runtime"": { + ""rest"": { ""enabled"": true, ""path"": ""/api"" }, + ""graphql"": { ""enabled"": true, ""path"": ""/graphql"" }, + ""host"": { ""mode"": ""development"" } + }, + ""entities"": { + ""Book"": { + ""source"": ""books"", + ""permissions"": [{ ""role"": ""anonymous"", ""actions"": [""*""] }], + ""mcp"": true + }, + ""Author"": { + ""source"": ""authors"", + ""permissions"": [{ ""role"": ""anonymous"", ""actions"": [""*""] }], + ""mcp"": false + }, + ""Publisher"": { + ""source"": ""publishers"", + ""permissions"": [{ ""role"": ""anonymous"", ""actions"": [""*""] }] + }, + ""GetBook"": { + ""source"": { ""type"": ""stored-procedure"", ""object"": ""dbo.GetBook"" }, + ""permissions"": [{ ""role"": ""anonymous"", ""actions"": [""execute""] }], + ""mcp"": { + ""custom-tool"": true, + ""dml-tools"": false + } + } + } + }"; + + // Act + bool success = RuntimeConfigLoader.TryParseConfig(config, out RuntimeConfig runtimeConfig); + + // Assert + Assert.IsTrue(success, "Config should parse successfully"); + Assert.IsNotNull(runtimeConfig); + + // Book: mcp = true + Assert.IsTrue(runtimeConfig.Entities.ContainsKey("Book")); + AssertEntityMcp(runtimeConfig.Entities["Book"], expectedDmlTools: true, expectedCustomTool: false); + + // Author: mcp = false + Assert.IsTrue(runtimeConfig.Entities.ContainsKey("Author")); + AssertEntityMcp(runtimeConfig.Entities["Author"], expectedDmlTools: false, expectedCustomTool: false); + + // Publisher: no mcp (null) + Assert.IsTrue(runtimeConfig.Entities.ContainsKey("Publisher")); + Assert.IsNull(runtimeConfig.Entities["Publisher"].Mcp, "Mcp should be null when not specified"); + + // GetBook: mcp object + Assert.IsTrue(runtimeConfig.Entities.ContainsKey("GetBook")); + AssertEntityMcp(runtimeConfig.Entities["GetBook"], expectedDmlTools: false, expectedCustomTool: true); + } + + /// + /// Test that deserializing invalid MCP value (non-boolean, non-object) fails gracefully. + /// + [TestMethod] + public void DeserializeConfig_InvalidMcpValue_FailsGracefully() + { + // Arrange + string config = CreateConfig(@" + ""Book"": { + ""source"": ""books"", + ""permissions"": [{ ""role"": ""anonymous"", ""actions"": [""*""] }], + ""mcp"": ""invalid"" + } + "); + + // Act + bool success = RuntimeConfigLoader.TryParseConfig(config, out _); + + // Assert + Assert.IsFalse(success, "Config parsing should fail with invalid MCP value"); + } + + /// + /// Test that deserializing MCP object with unknown property fails gracefully. + /// + [TestMethod] + public void DeserializeConfig_McpObjectWithUnknownProperty_FailsGracefully() + { + // Arrange + string config = CreateConfig(@" + ""Book"": { + ""source"": ""books"", + ""permissions"": [{ ""role"": ""anonymous"", ""actions"": [""*""] }], + ""mcp"": { + ""dml-tools"": true, + ""unknown-property"": true + } + } + "); + + // Act + bool success = RuntimeConfigLoader.TryParseConfig(config, out _); + + // Assert + Assert.IsFalse(success, "Config parsing should fail with unknown MCP property"); + } + } +} diff --git a/src/Service.Tests/ModuleInitializer.cs b/src/Service.Tests/ModuleInitializer.cs index ba0407ecd5..f0c3984a72 100644 --- a/src/Service.Tests/ModuleInitializer.cs +++ b/src/Service.Tests/ModuleInitializer.cs @@ -69,6 +69,10 @@ public static void Init() VerifierSettings.IgnoreMember(entity => entity.IsLinkingEntity); // Ignore the UserProvidedTtlOptions. They aren't serialized to our config file, enforced by EntityCacheOptionsConverter. VerifierSettings.IgnoreMember(cacheOptions => cacheOptions.UserProvidedTtlOptions); + // Ignore the UserProvidedCustomToolEnabled. They aren't serialized to our config file, enforced by EntityMcpOptionsConverterFactory. + VerifierSettings.IgnoreMember(mcpOptions => mcpOptions.UserProvidedCustomToolEnabled); + // Ignore the UserProvidedDmlToolsEnabled. They aren't serialized to our config file, enforced by EntityMcpOptionsConverterFactory. + VerifierSettings.IgnoreMember(mcpOptions => mcpOptions.UserProvidedDmlToolsEnabled); // Ignore the CosmosDataSourceUsed as that's unimportant from a test standpoint. VerifierSettings.IgnoreMember(config => config.CosmosDataSourceUsed); // Ignore the IsRequestBodyStrict as that's unimportant from a test standpoint. From 0b43e3ba4235046684d19d52d103c196f3a3c2a3 Mon Sep 17 00:00:00 2001 From: RubenCerna2079 <32799214+RubenCerna2079@users.noreply.github.com> Date: Thu, 18 Dec 2025 14:06:28 -0800 Subject: [PATCH 20/36] Partial fix for MCP test (#2925) ## Why make this change? - #2874 There is a test that is failing and it needs to be fixed in order to comply with the creation of the new MCP endpoint. The test already existed and covered the scenarios for REST and GraphQL endpoints. It will only be partially fixed, only fixing the issues related to the non-hosted scenario, this is due to the fact that fixing the hosted-scenario will need more changes than expected. ## What is this change? This change partially fixes the test that was failing, the non-hosted scenario was failing because the MCP server was not able to start in time before the test tried to access the MCP endpoint. In order to fix it we added a delay so the server is available before the test tries to access the endpoint. On the other hand, the hosted scenario is failing because of the way that DAB initializes its MCP service, which means that the base needs to be changed. Which is a bigger task than what is expected of this PR. ## How was this tested? - [ ] Integration Tests - [X] Unit Tests --- .../Configuration/ConfigurationTests.cs | 162 +++++++++++++----- 1 file changed, 119 insertions(+), 43 deletions(-) diff --git a/src/Service.Tests/Configuration/ConfigurationTests.cs b/src/Service.Tests/Configuration/ConfigurationTests.cs index 0614e7688f..9728c27ee7 100644 --- a/src/Service.Tests/Configuration/ConfigurationTests.cs +++ b/src/Service.Tests/Configuration/ConfigurationTests.cs @@ -74,7 +74,7 @@ public class ConfigurationTests private const string BROWSER_ACCEPT_HEADER = "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9"; private const int RETRY_COUNT = 5; - private const int RETRY_WAIT_SECONDS = 1; + private const int RETRY_WAIT_SECONDS = 2; /// /// @@ -1128,7 +1128,7 @@ public async Task TestSqlSettingPostStartupConfigurations(string configurationEn await httpClient.GetAsync($"/{OPENAPI_SWAGGER_ENDPOINT}"); Assert.AreEqual(HttpStatusCode.ServiceUnavailable, preConfigOpenApiSwaggerEndpointAvailability.StatusCode); - HttpStatusCode responseCode = await HydratePostStartupConfiguration(httpClient, content, configurationEndpoint); + HttpStatusCode responseCode = await HydratePostStartupConfiguration(httpClient, content, configurationEndpoint, configuration.Runtime.Rest); // When the authorization resolver is properly configured, authorization will have failed // because no auth headers are present. @@ -2538,26 +2538,37 @@ public async Task TestRuntimeBaseRouteInNextLinkForPaginatedRestResponse() /// Expected HTTP status code code for the GraphQL request [DataTestMethod] [TestCategory(TestCategory.MSSQL)] - [DataRow(true, true, HttpStatusCode.OK, HttpStatusCode.OK, CONFIGURATION_ENDPOINT, DisplayName = "V1 - Both Rest and GraphQL endpoints enabled globally")] - [DataRow(true, false, HttpStatusCode.OK, HttpStatusCode.NotFound, CONFIGURATION_ENDPOINT, DisplayName = "V1 - Rest enabled and GraphQL endpoints disabled globally")] - [DataRow(false, true, HttpStatusCode.NotFound, HttpStatusCode.OK, CONFIGURATION_ENDPOINT, DisplayName = "V1 - Rest disabled and GraphQL endpoints enabled globally")] - [DataRow(true, true, HttpStatusCode.OK, HttpStatusCode.OK, CONFIGURATION_ENDPOINT_V2, DisplayName = "V2 - Both Rest and GraphQL endpoints enabled globally")] - [DataRow(true, false, HttpStatusCode.OK, HttpStatusCode.NotFound, CONFIGURATION_ENDPOINT_V2, DisplayName = "V2 - Rest enabled and GraphQL endpoints disabled globally")] - [DataRow(false, true, HttpStatusCode.NotFound, HttpStatusCode.OK, CONFIGURATION_ENDPOINT_V2, DisplayName = "V2 - Rest disabled and GraphQL endpoints enabled globally")] - public async Task TestGlobalFlagToEnableRestAndGraphQLForHostedAndNonHostedEnvironment( + [DataRow(true, true, true, HttpStatusCode.OK, HttpStatusCode.OK, HttpStatusCode.OK, CONFIGURATION_ENDPOINT, DisplayName = "V1 - Rest, GraphQL, and MCP enabled globally")] + [DataRow(true, true, false, HttpStatusCode.OK, HttpStatusCode.OK, HttpStatusCode.NotFound, CONFIGURATION_ENDPOINT, DisplayName = "V1 - Rest and GraphQL enabled, MCP disabled globally")] + [DataRow(true, false, true, HttpStatusCode.OK, HttpStatusCode.NotFound, HttpStatusCode.OK, CONFIGURATION_ENDPOINT, DisplayName = "V1 - Rest enabled, GraphQL disabled, and MCP enabled globally")] + [DataRow(true, false, false, HttpStatusCode.OK, HttpStatusCode.NotFound, HttpStatusCode.NotFound, CONFIGURATION_ENDPOINT, DisplayName = "V1 - Rest enabled, GraphQL and MCP disabled globally")] + [DataRow(false, true, true, HttpStatusCode.NotFound, HttpStatusCode.OK, HttpStatusCode.OK, CONFIGURATION_ENDPOINT, DisplayName = "V1 - Rest disabled, GraphQL and MCP enabled globally")] + [DataRow(false, true, false, HttpStatusCode.NotFound, HttpStatusCode.OK, HttpStatusCode.NotFound, CONFIGURATION_ENDPOINT, DisplayName = "V1 - Rest disabled, GraphQL enabled, and MCP disabled globally")] + [DataRow(false, false, true, HttpStatusCode.NotFound, HttpStatusCode.NotFound, HttpStatusCode.OK, CONFIGURATION_ENDPOINT, DisplayName = "V1 - Rest and GraphQL disabled, MCP enabled globally")] + [DataRow(true, true, true, HttpStatusCode.OK, HttpStatusCode.OK, HttpStatusCode.OK, CONFIGURATION_ENDPOINT_V2, DisplayName = "V2 - Rest, GraphQL, and MCP enabled globally")] + [DataRow(true, true, false, HttpStatusCode.OK, HttpStatusCode.OK, HttpStatusCode.NotFound, CONFIGURATION_ENDPOINT_V2, DisplayName = "V2 - Rest and GraphQL enabled, MCP disabled globally")] + [DataRow(true, false, true, HttpStatusCode.OK, HttpStatusCode.NotFound, HttpStatusCode.OK, CONFIGURATION_ENDPOINT_V2, DisplayName = "V2 - Rest enabled, GraphQL disabled, and MCP enabled globally")] + [DataRow(true, false, false, HttpStatusCode.OK, HttpStatusCode.NotFound, HttpStatusCode.NotFound, CONFIGURATION_ENDPOINT_V2, DisplayName = "V2 - Rest enabled, GraphQL and MCP disabled globally")] + [DataRow(false, true, true, HttpStatusCode.NotFound, HttpStatusCode.OK, HttpStatusCode.OK, CONFIGURATION_ENDPOINT_V2, DisplayName = "V2 - Rest disabled, GraphQL and MCP enabled globally")] + [DataRow(false, true, false, HttpStatusCode.NotFound, HttpStatusCode.OK, HttpStatusCode.NotFound, CONFIGURATION_ENDPOINT_V2, DisplayName = "V2 - Rest disabled, GraphQL enabled, and MCP disabled globally")] + [DataRow(false, false, true, HttpStatusCode.NotFound, HttpStatusCode.NotFound, HttpStatusCode.OK, CONFIGURATION_ENDPOINT_V2, DisplayName = "V2 - Rest and GraphQL disabled, MCP enabled globally")] + public async Task TestGlobalFlagToEnableRestGraphQLAndMcpForHostedAndNonHostedEnvironment( bool isRestEnabled, bool isGraphQLEnabled, + bool isMcpEnabled, HttpStatusCode expectedStatusCodeForREST, HttpStatusCode expectedStatusCodeForGraphQL, + HttpStatusCode expectedStatusCodeForMcp, string configurationEndpoint) { GraphQLRuntimeOptions graphqlOptions = new(Enabled: isGraphQLEnabled); RestRuntimeOptions restRuntimeOptions = new(Enabled: isRestEnabled); + McpRuntimeOptions mcpRuntimeOptions = new(Enabled: isMcpEnabled); DataSource dataSource = new(DatabaseType.MSSQL, GetConnectionStringFromEnvironmentConfig(environment: TestCategory.MSSQL), Options: null); - RuntimeConfig configuration = InitMinimalRuntimeConfig(dataSource, graphqlOptions, restRuntimeOptions, null); + RuntimeConfig configuration = InitMinimalRuntimeConfig(dataSource, graphqlOptions, restRuntimeOptions, mcpRuntimeOptions); const string CUSTOM_CONFIG = "custom-config.json"; File.WriteAllText(CUSTOM_CONFIG, configuration.ToJson()); @@ -2580,17 +2591,23 @@ public async Task TestGlobalFlagToEnableRestAndGraphQLForHostedAndNonHostedEnvir object payload = new { query }; - HttpRequestMessage graphQLRequest = new(HttpMethod.Post, "/graphql") + // GraphQL request + HttpRequestMessage graphQLRequest = new(HttpMethod.Post, configuration.Runtime.GraphQL.Path) { Content = JsonContent.Create(payload) }; HttpResponseMessage graphQLResponse = await client.SendAsync(graphQLRequest); - Assert.AreEqual(expectedStatusCodeForGraphQL, graphQLResponse.StatusCode); + Assert.AreEqual(expectedStatusCodeForGraphQL, graphQLResponse.StatusCode, "The GraphQL response is different from the expected result."); - HttpRequestMessage restRequest = new(HttpMethod.Get, "/api/Book"); + // REST request + HttpRequestMessage restRequest = new(HttpMethod.Get, $"{configuration.Runtime.Rest.Path}/Book"); HttpResponseMessage restResponse = await client.SendAsync(restRequest); - Assert.AreEqual(expectedStatusCodeForREST, restResponse.StatusCode); + Assert.AreEqual(expectedStatusCodeForREST, restResponse.StatusCode, "The REST response is different from the expected result."); + + // MCP request + HttpStatusCode mcpResponseCode = await GetMcpResponse(client, configuration.Runtime.Mcp); + Assert.AreEqual(expectedStatusCodeForMcp, mcpResponseCode, "The MCP response is different from the expected result."); } // Hosted Scenario @@ -2600,18 +2617,19 @@ public async Task TestGlobalFlagToEnableRestAndGraphQLForHostedAndNonHostedEnvir { JsonContent content = GetPostStartupConfigParams(MSSQL_ENVIRONMENT, configuration, configurationEndpoint); - HttpResponseMessage postResult = - await client.PostAsync(configurationEndpoint, content); - Assert.AreEqual(HttpStatusCode.OK, postResult.StatusCode); - - HttpStatusCode restResponseCode = await GetRestResponsePostConfigHydration(client); - - Assert.AreEqual(expected: expectedStatusCodeForREST, actual: restResponseCode); + HttpResponseMessage postResult = await client.PostAsync(configurationEndpoint, content); + Assert.AreEqual(HttpStatusCode.OK, postResult.StatusCode, "The hydration post-response is different from the expected result."); - HttpStatusCode graphqlResponseCode = await GetGraphQLResponsePostConfigHydration(client); + HttpStatusCode restResponseCode = await GetRestResponsePostConfigHydration(client, configuration.Runtime.Rest); + Assert.AreEqual(expected: expectedStatusCodeForREST, actual: restResponseCode, "The REST hydration post-response is different from the expected result."); - Assert.AreEqual(expected: expectedStatusCodeForGraphQL, actual: graphqlResponseCode); + HttpStatusCode graphqlResponseCode = await GetGraphQLResponsePostConfigHydration(client, configuration.Runtime.GraphQL); + Assert.AreEqual(expected: expectedStatusCodeForGraphQL, actual: graphqlResponseCode, "The GraphQL hydration post-response is different from the expected result."); + // TODO: Issue #3012 - Currently DAB is unable to start MCP with the hydration post-response. + // This needs to be fixed before uncommenting the MCP check + // HttpStatusCode mcpResponseCode = await GetMcpResponse(client, configuration.Runtime.Mcp); + // Assert.AreEqual(expected: expectedStatusCodeForMcp, actual: mcpResponseCode, "The MCP hydration post-response is different from the expected result."); } } @@ -3661,7 +3679,7 @@ public async Task TestSchemaIntrospectionQuery(bool enableIntrospection, bool ex using (HttpClient client = server.CreateClient()) { JsonContent content = GetPostStartupConfigParams(MSSQL_ENVIRONMENT, configuration, configurationEndpoint); - HttpStatusCode responseCode = await HydratePostStartupConfiguration(client, content, configurationEndpoint); + HttpStatusCode responseCode = await HydratePostStartupConfiguration(client, content, configurationEndpoint, configuration.Runtime.Rest); Assert.AreEqual(expected: HttpStatusCode.OK, actual: responseCode, message: "Configuration hydration failed."); @@ -5256,41 +5274,48 @@ private static JsonContent GetPostStartupConfigParams(string environment, Runtim /// by executing HTTP requests against the engine until a non-503 error is received. /// /// Client used for request execution. - /// Post-startup configuration + /// New config file content that will be added to DAB. + /// Endpoint through which content will be sent to DAB." + /// Global settings used at runtime for REST APIs. /// ServiceUnavailable if service is not successfully hydrated with config - private static async Task HydratePostStartupConfiguration(HttpClient httpClient, JsonContent content, string configurationEndpoint) + private static async Task HydratePostStartupConfiguration(HttpClient httpClient, JsonContent content, string configurationEndpoint, RestRuntimeOptions rest) { // Hydrate configuration post-startup HttpResponseMessage postResult = await httpClient.PostAsync(configurationEndpoint, content); Assert.AreEqual(HttpStatusCode.OK, postResult.StatusCode); - return await GetRestResponsePostConfigHydration(httpClient); + return await GetRestResponsePostConfigHydration(httpClient, rest); } /// /// Executing REST requests against the engine until a non-503 error is received. /// /// Client used for request execution. + /// Global settings used at runtime for REST APIs. /// ServiceUnavailable if service is not successfully hydrated with config, /// else the response code from the REST request - private static async Task GetRestResponsePostConfigHydration(HttpClient httpClient) + private static async Task GetRestResponsePostConfigHydration(HttpClient httpClient, RestRuntimeOptions rest) { - // Retry request RETRY_COUNT times in 1 second increments to allow required services - // time to instantiate and hydrate permissions. - int retryCount = RETRY_COUNT; + // Retry request RETRY_COUNT times in exponential increments to allow + // required services time to instantiate and hydrate permissions because + // the DAB services may take an unpredictable amount of time to become ready. + // + // The service might still fail due to the service not being available yet, + // but it is highly unlikely to be the case. + int retryCount = 0; HttpStatusCode responseCode = HttpStatusCode.ServiceUnavailable; - while (retryCount > 0) + while (retryCount < RETRY_COUNT) { // Spot test authorization resolver utilization to ensure configuration is used. HttpResponseMessage postConfigHydrationResult = - await httpClient.GetAsync($"api/{POST_STARTUP_CONFIG_ENTITY}"); + await httpClient.GetAsync($"{rest.Path}/{POST_STARTUP_CONFIG_ENTITY}"); responseCode = postConfigHydrationResult.StatusCode; if (postConfigHydrationResult.StatusCode == HttpStatusCode.ServiceUnavailable) { - retryCount--; - Thread.Sleep(TimeSpan.FromSeconds(RETRY_WAIT_SECONDS)); + retryCount++; + await Task.Delay(TimeSpan.FromSeconds(Math.Pow(RETRY_WAIT_SECONDS, retryCount))); continue; } @@ -5306,13 +5331,17 @@ private static async Task GetRestResponsePostConfigHydration(Htt /// Client used for request execution. /// ServiceUnavailable if service is not successfully hydrated with config, /// else the response code from the GRAPHQL request - private static async Task GetGraphQLResponsePostConfigHydration(HttpClient httpClient) + private static async Task GetGraphQLResponsePostConfigHydration(HttpClient httpClient, GraphQLRuntimeOptions graphQL) { - // Retry request RETRY_COUNT times in 1 second increments to allow required services - // time to instantiate and hydrate permissions. - int retryCount = RETRY_COUNT; + // Retry request RETRY_COUNT times in exponential increments to allow + // required services time to instantiate and hydrate permissions because + // the DAB services may take an unpredictable amount of time to become ready. + // + // The service might still fail due to the service not being available yet, + // but it is highly unlikely to be the case. + int retryCount = 0; HttpStatusCode responseCode = HttpStatusCode.ServiceUnavailable; - while (retryCount > 0) + while (retryCount < RETRY_COUNT) { string query = @"{ book_by_pk(id: 1) { @@ -5324,7 +5353,7 @@ private static async Task GetGraphQLResponsePostConfigHydration( object payload = new { query }; - HttpRequestMessage graphQLRequest = new(HttpMethod.Post, "/graphql") + HttpRequestMessage graphQLRequest = new(HttpMethod.Post, graphQL.Path) { Content = JsonContent.Create(payload) }; @@ -5334,8 +5363,55 @@ private static async Task GetGraphQLResponsePostConfigHydration( if (responseCode == HttpStatusCode.ServiceUnavailable) { - retryCount--; - Thread.Sleep(TimeSpan.FromSeconds(RETRY_WAIT_SECONDS)); + retryCount++; + await Task.Delay(TimeSpan.FromSeconds(Math.Pow(RETRY_WAIT_SECONDS, retryCount))); + continue; + } + + break; + } + + return responseCode; + } + + /// + /// Executing MCP POST requests against the engine until a non-503 error is received. + /// + /// Client used for request execution. + /// ServiceUnavailable if service is not successfully hydrated with config, + /// else the response code from the MCP request + public static async Task GetMcpResponse(HttpClient httpClient, McpRuntimeOptions mcp) + { + // Retry request RETRY_COUNT times in exponential increments to allow + // required services time to instantiate and hydrate permissions because + // the DAB services may take an unpredictable amount of time to become ready. + // + // The service might still fail due to the service not being available yet, + // but it is highly unlikely to be the case. + int retryCount = 0; + HttpStatusCode responseCode = HttpStatusCode.ServiceUnavailable; + while (retryCount < RETRY_COUNT) + { + // Minimal MCP request (list tools) – valid JSON-RPC request + object payload = new + { + jsonrpc = "2.0", + id = 1, + method = "tools/list" + }; + HttpRequestMessage mcpRequest = new(HttpMethod.Post, mcp.Path) + { + Content = JsonContent.Create(payload) + }; + mcpRequest.Headers.Add("Accept", "*/*"); + + HttpResponseMessage mcpResponse = await httpClient.SendAsync(mcpRequest); + responseCode = mcpResponse.StatusCode; + + if (responseCode == HttpStatusCode.ServiceUnavailable || responseCode == HttpStatusCode.NotFound) + { + retryCount++; + await Task.Delay(TimeSpan.FromSeconds(Math.Pow(RETRY_WAIT_SECONDS, retryCount))); continue; } From eddd4c645eb382e7b27b59968268b158c6011616 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 19 Dec 2025 19:11:59 +0000 Subject: [PATCH 21/36] Bump dotnet-sdk from 8.0.415 to 8.0.416 (#2970) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [dotnet-sdk](https://github.com/dotnet/sdk) from 8.0.415 to 8.0.416.
Release notes

Sourced from dotnet-sdk's releases.

.NET 8.0.22

Release

What's Changed

Full Changelog: https://github.com/dotnet/sdk/compare/v8.0.415...v8.0.416

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=dotnet-sdk&package-manager=dotnet_sdk&previous-version=8.0.415&new-version=8.0.416)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- global.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/global.json b/global.json index 3d952f846d..285b9d3725 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "8.0.415", + "version": "8.0.416", "rollForward": "latestFeature" } } From 8d62beb7e3bbc6bbfa7db41d2ee7436162d178c8 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Mon, 22 Dec 2025 22:07:57 +0530 Subject: [PATCH 22/36] Fix: Exclude stored procedures from health checks (#2997) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Why make this change? Closes #2977 Health check endpoint was returning results for stored procedures. Stored procedures should be excluded because: 1. They require parameters not configurable via health settings 2. They are not deterministic, making health checks unreliable ## What is this change? Added filter in `HealthCheckHelper.UpdateEntityHealthCheckResultsAsync()` to exclude entities with `EntitySourceType.StoredProcedure`: ```csharp // Before .Where(e => e.Value.IsEntityHealthEnabled) // After .Where(e => e.Value.IsEntityHealthEnabled && e.Value.Source.Type != EntitySourceType.StoredProcedure) ``` Only tables and views are now included in entity health checks. ## How was this tested? - [ ] Integration Tests - [x] Unit Tests Added `HealthChecks_ExcludeStoredProcedures()` unit test that creates a `RuntimeConfig` with both table and stored procedure entities, then applies the same filter used in `HealthCheckHelper.UpdateEntityHealthCheckResultsAsync` to verify stored procedures are excluded while tables are included. ## Sample Request(s) Health check response after fix (stored procedure `GetSeriesActors` no longer appears): ```json { "status": "Healthy", "checks": [ { "name": "MSSQL", "tags": ["data-source"] }, { "name": "Book", "tags": ["rest", "endpoint"] } ] } ```
Original prompt > > ---- > > *This section details on the original issue you should resolve* > > [Bug]: Health erroneously checks Stored Procedures > ## What? > > Health check returns check results for stored procs. It should ONLY include tables and views. > > ## Health output sample > > ```json > { > "status": "Healthy", > "version": "1.7.81", > "app-name": "dab_oss_1.7.81", > "timestamp": "2025-11-17T20:33:42.2752261Z", > "configuration": { > "rest": true, > "graphql": true, > "mcp": true, > "caching": true, > "telemetry": false, > "mode": "Development" > }, > "checks": [ > { > "status": "Healthy", > "name": "MSSQL", > "tags": [ > "data-source" > ], > "data": { > "response-ms": 3, > "threshold-ms": 1000 > } > }, > { > "status": "Healthy", > "name": "GetSeriesActors", // stored procedure > "tags": [ > "graphql", > "endpoint" > ], > "data": { > "response-ms": 1, > "threshold-ms": 1000 > } > }, > { > "status": "Healthy", > "name": "GetSeriesActors", // stored procedure > "tags": [ > "rest", > "endpoint" > ], > "data": { > "response-ms": 5, > "threshold-ms": 1000 > } > } > ] > } > ``` > > ## Comments on the Issue (you are @copilot in this section) > > > @souvikghosh04 > @JerryNixon / @Aniruddh25 should stored procedures and functions be discarded from health checks permanently? > @JerryNixon > The entity checks in the Health endpoint check every table and view type entity with a user-configurable select with a first compared against a user-configurable threshold. We do not check stored procedures, and cannot check stored procedures, as we do not have any mechanism to take parameters as Health configuration values. Also stored procedures are not guaranteed to be deterministic, making checks that would call them potentially be unreliable. So, yes, stored procedures should be ignored. > >
- Fixes Azure/data-api-builder#2982 --- 💬 We'd love your input! Share your thoughts on Copilot coding agent in our [2 minute survey](https://gh.io/copilot-coding-agent-survey). --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Aniruddh25 <3513779+Aniruddh25@users.noreply.github.com> Co-authored-by: JerryNixon <1749983+JerryNixon@users.noreply.github.com> --- .../Configuration/HealthEndpointTests.cs | 88 +++++++++++++++++++ src/Service/HealthCheck/HealthCheckHelper.cs | 3 +- 2 files changed, 90 insertions(+), 1 deletion(-) diff --git a/src/Service.Tests/Configuration/HealthEndpointTests.cs b/src/Service.Tests/Configuration/HealthEndpointTests.cs index 1eac7416e3..84ea40a5a9 100644 --- a/src/Service.Tests/Configuration/HealthEndpointTests.cs +++ b/src/Service.Tests/Configuration/HealthEndpointTests.cs @@ -564,6 +564,94 @@ private static RuntimeConfig CreateRuntimeConfig(Dictionary enti return runtimeConfig; } + /// + /// Verifies that stored procedures are excluded from health check results. + /// Creates a config with both a table entity and a stored procedure entity, + /// then validates that only the table entity appears in the health endpoint response. + /// + [TestMethod] + [TestCategory(TestCategory.MSSQL)] + public async Task HealthEndpoint_ExcludesStoredProcedures() + { + // Create a table entity + Entity tableEntity = new( + Health: new(enabled: true), + Source: new("books", EntitySourceType.Table, null, null), + Fields: null, + Rest: new(Enabled: true), + GraphQL: new("book", "bookLists", true), + Permissions: new[] { ConfigurationTests.GetMinimalPermissionConfig(AuthorizationResolver.ROLE_ANONYMOUS) }, + Relationships: null, + Mappings: null); + + // Create a stored procedure entity - using an actual stored procedure from test schema + Entity storedProcEntity = new( + Health: new(enabled: true), + Source: new("get_books", EntitySourceType.StoredProcedure, null, null), + Fields: null, + Rest: new(Enabled: true), + GraphQL: new("executeGetBooks", "executeGetBooksList", true), + Permissions: new[] { ConfigurationTests.GetMinimalPermissionConfig(AuthorizationResolver.ROLE_ANONYMOUS) }, + Relationships: null, + Mappings: null); + + Dictionary entityMap = new() + { + { "Book", tableEntity }, + { "GetBooks", storedProcEntity } + }; + + RuntimeConfig runtimeConfig = CreateRuntimeConfig( + entityMap, + enableGlobalRest: true, + enableGlobalGraphql: true, + enabledGlobalMcp: true, + enableGlobalHealth: true, + enableDatasourceHealth: true, + hostMode: HostMode.Development); + + WriteToCustomConfigFile(runtimeConfig); + + string[] args = new[] + { + $"--ConfigFileName={CUSTOM_CONFIG_FILENAME}" + }; + + using (TestServer server = new(Program.CreateWebHostBuilder(args))) + using (HttpClient client = server.CreateClient()) + { + HttpRequestMessage healthRequest = new(HttpMethod.Get, $"{BASE_DAB_URL}/health"); + HttpResponseMessage response = await client.SendAsync(healthRequest); + + Assert.AreEqual(HttpStatusCode.OK, response.StatusCode, "Health endpoint should return OK"); + + string responseBody = await response.Content.ReadAsStringAsync(); + Dictionary responseProperties = JsonSerializer.Deserialize>(responseBody); + + // Get the checks array + Assert.IsTrue(responseProperties.TryGetValue("checks", out JsonElement checksElement), "Response should contain 'checks' property"); + Assert.AreEqual(JsonValueKind.Array, checksElement.ValueKind, "Checks should be an array"); + + // Get all entity names from the health check results + List entityNamesInHealthCheck = new(); + foreach (JsonElement check in checksElement.EnumerateArray()) + { + if (check.TryGetProperty("name", out JsonElement nameElement)) + { + entityNamesInHealthCheck.Add(nameElement.GetString()); + } + } + + // Verify that the table entity (Book) appears in health checks + Assert.IsTrue(entityNamesInHealthCheck.Contains("Book"), + "Table entity 'Book' should be included in health check results"); + + // Verify that the stored procedure entity (GetBooks) does NOT appear in health checks + Assert.IsFalse(entityNamesInHealthCheck.Contains("GetBooks"), + "Stored procedure entity 'GetBooks' should be excluded from health check results"); + } + } + private static void WriteToCustomConfigFile(RuntimeConfig runtimeConfig) { File.WriteAllText( diff --git a/src/Service/HealthCheck/HealthCheckHelper.cs b/src/Service/HealthCheck/HealthCheckHelper.cs index 9b7bbd272c..ab19756195 100644 --- a/src/Service/HealthCheck/HealthCheckHelper.cs +++ b/src/Service/HealthCheck/HealthCheckHelper.cs @@ -199,10 +199,11 @@ private async Task UpdateDataSourceHealthCheckResultsAsync(ComprehensiveHealthCh // Updates the Entity Health Check Results in the response. // Goes through the entities one by one and executes the rest and graphql checks (if enabled). + // Stored procedures are excluded from health checks because they require parameters and are not guaranteed to be deterministic. private async Task UpdateEntityHealthCheckResultsAsync(ComprehensiveHealthCheckReport report, RuntimeConfig runtimeConfig) { List> enabledEntities = runtimeConfig.Entities.Entities - .Where(e => e.Value.IsEntityHealthEnabled) + .Where(e => e.Value.IsEntityHealthEnabled && e.Value.Source.Type != EntitySourceType.StoredProcedure) .ToList(); if (enabledEntities.Count == 0) From 9bbe25238205246bb578f2033933fbfb0963a41a Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Tue, 23 Dec 2025 07:33:55 -0800 Subject: [PATCH 23/36] Add schema and serialization support for autoentities wildcard enhancement (#2973) ## Why make this change? We need to add the new 'autoentities' properties that will later be used to allow the user to add multiple entities at the same time. The properties need to have all the necessary components to be serialized and deserialized from the config file. ## What is this change? - This change adds the `autoentities` property to the schema file with proper structure using `additionalProperties` to allow user-defined autoentity definition names. - Created new files that allow for the properties to be serialized and deserialized: - `AutoentityConverter.cs` - `AutoentityPatternsConverter.cs` - `AutoentityTemplateConverter.cs` - `RuntimeAutoentitiesConverter.cs` - Created new files where deserialized properties are turned to usable objects: - `Autoentity.cs` - `AutoentityPatterns.cs` - `AutoentityTemplate.cs` - `RuntimeAutoentities.cs` - Added entity-level MCP configuration support: - `EntityMcpOptions.cs` - Supports both boolean shorthand and object format for MCP configuration - `EntityMcpOptionsConverterFactory.cs` - Handles serialization/deserialization of MCP options - Registered autoentity converters in `RuntimeConfigLoader.cs` for proper deserialization. - Updated schema description to "Defines automatic entity generation rules for MSSQL tables based on include/exclude patterns and defaults." - Added `required: ["permissions"]` constraint to enforce at least one permission per specification. **Schema Structure**: The autoentities object uses `additionalProperties` to allow any string key as a user-defined autoentity definition name (e.g., "public-tables", "admin-tables", etc.), consistent with how the "entities" section works. **MCP Configuration**: Supports two formats: - Boolean shorthand: `"mcp": true` or `"mcp": false` - Object format: `"mcp": { "dml-tools": true}` Example configuration: ```json { "autoentities": { "": { // This property name is decided by the user to show a group of autoentities "patterns": { "include": ["dbo.%"], "exclude": ["dbo.internal_%"], "name": "{object}" }, "template": { "mcp": { "dml-tools": true }, "rest": { "enabled": true }, "graphql": { "enabled": true }, "health": { "enabled": true }, "cache": { "enabled": false } }, "permissions": [ { "role": "anonymous", "actions": ["read"] } ] } } } ``` ## How was this tested? - [ ] Integration Tests - [x] Unit Tests --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: RubenCerna2079 <32799214+RubenCerna2079@users.noreply.github.com> Co-authored-by: Ruben Cerna --- schemas/dab.draft.schema.json | 160 +++++++++++++++++ src/Config/Converters/AutoentityConverter.cs | 110 ++++++++++++ .../Converters/AutoentityPatternsConverter.cs | 151 ++++++++++++++++ .../Converters/AutoentityTemplateConverter.cs | 133 ++++++++++++++ ...zureLogAnalyticsOptionsConverterFactory.cs | 2 +- .../EntityRestOptionsConverterFactory.cs | 1 + .../RuntimeAutoentitiesConverter.cs | 40 +++++ src/Config/ObjectModel/Autoentity.cs | 32 ++++ src/Config/ObjectModel/AutoentityPatterns.cs | 99 ++++++++++ src/Config/ObjectModel/AutoentityTemplate.cs | 153 ++++++++++++++++ src/Config/ObjectModel/RuntimeAutoentities.cs | 28 +++ src/Config/ObjectModel/RuntimeConfig.cs | 4 + src/Config/RuntimeConfigLoader.cs | 3 + .../DabCacheServiceIntegrationTests.cs | 13 +- .../Configuration/ConfigurationTests.cs | 170 ++++++++++++++++++ 15 files changed, 1092 insertions(+), 7 deletions(-) create mode 100644 src/Config/Converters/AutoentityConverter.cs create mode 100644 src/Config/Converters/AutoentityPatternsConverter.cs create mode 100644 src/Config/Converters/AutoentityTemplateConverter.cs create mode 100644 src/Config/Converters/RuntimeAutoentitiesConverter.cs create mode 100644 src/Config/ObjectModel/Autoentity.cs create mode 100644 src/Config/ObjectModel/AutoentityPatterns.cs create mode 100644 src/Config/ObjectModel/AutoentityTemplate.cs create mode 100644 src/Config/ObjectModel/RuntimeAutoentities.cs diff --git a/schemas/dab.draft.schema.json b/schemas/dab.draft.schema.json index 61ab7474b7..b6a7f66268 100644 --- a/schemas/dab.draft.schema.json +++ b/schemas/dab.draft.schema.json @@ -693,6 +693,166 @@ } } }, + "autoentities": { + "type": "object", + "description": "Defines automatic entity generation rules for MSSQL tables based on include/exclude patterns and defaults.", + "patternProperties": { + "^.*$": { + "type": "object", + "additionalProperties": false, + "properties": { + "patterns": { + "type": "object", + "description": "Pattern matching rules for including/excluding database objects", + "additionalProperties": false, + "properties": { + "include": { + "type": "array", + "description": "MSSQL LIKE pattern for objects to include (e.g., '%.%'). Null includes all.", + "items": { + "type": "string" + }, + "default": [ "%.%" ] + }, + "exclude": { + "type": "array", + "description": "MSSQL LIKE pattern for objects to exclude (e.g., 'sales.%'). Null excludes none.", + "items": { + "type": "string" + }, + "default": null + }, + "name": { + "type": "string", + "description": "Entity name interpolation pattern using {schema} and {object}. Null defaults to {object}. Must be unique for every entity inside the pattern", + "default": "{object}" + } + } + }, + "template": { + "type": "object", + "description": "Template configuration for generated entities", + "additionalProperties": false, + "properties": { + "mcp": { + "type": "object", + "description": "MCP endpoint configuration", + "additionalProperties": false, + "properties": { + "dml-tools": { + "type": "boolean", + "description": "Enable/disable all DML tools with default settings." + } + } + }, + "rest": { + "type": "object", + "description": "REST endpoint configuration", + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean", + "description": "Enable/disable REST endpoint", + "default": true + } + } + }, + "graphql": { + "type": "object", + "description": "GraphQL endpoint configuration", + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean", + "description": "Enable/disable GraphQL endpoint", + "default": true + } + } + }, + "health": { + "type": "object", + "description": "Health check configuration", + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean", + "description": "Enable/disable health check endpoint", + "default": true + } + } + }, + "cache": { + "type": "object", + "description": "Cache configuration", + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean", + "description": "Enable/disable caching", + "default": false + }, + "ttl-seconds": { + "type": [ "integer", "null" ], + "description": "Time-to-live for cached responses in seconds", + "default": null, + "minimum": 1 + }, + "level": { + "type": "string", + "description": "Cache level (L1 or L1L2)", + "enum": [ "L1", "L1L2", null ], + "default": "L1L2" + } + } + } + } + }, + "permissions": { + "type": "array", + "description": "Permissions assigned to this object", + "items": { + "type": "object", + "additionalProperties": false, + "properties": { + "role": { + "type": "string" + }, + "actions": { + "oneOf": [ + { + "type": "string", + "pattern": "[*]" + }, + { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/$defs/action" + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "action": { + "$ref": "#/$defs/action" + } + } + } + ] + }, + "uniqueItems": true + } + ] + } + } + }, + "required": [ "role", "actions" ] + } + } + } + } + }, "entities": { "type": "object", "description": "Entities that will be exposed via REST, GraphQL and/or MCP", diff --git a/src/Config/Converters/AutoentityConverter.cs b/src/Config/Converters/AutoentityConverter.cs new file mode 100644 index 0000000000..5c09ed8e7b --- /dev/null +++ b/src/Config/Converters/AutoentityConverter.cs @@ -0,0 +1,110 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json; +using System.Text.Json.Serialization; +using Azure.DataApiBuilder.Config.ObjectModel; + +namespace Azure.DataApiBuilder.Config.Converters; + +internal class AutoentityConverter : JsonConverter +{ + // Settings for variable replacement during deserialization. + private readonly DeserializationVariableReplacementSettings? _replacementSettings; + + /// Settings for variable replacement during deserialization. + /// If null, no variable replacement will be performed. + public AutoentityConverter(DeserializationVariableReplacementSettings? replacementSettings = null) + { + _replacementSettings = replacementSettings; + } + + /// + public override Autoentity? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType is JsonTokenType.StartObject) + { + // Initialize all sub-properties to null. + AutoentityPatterns? patterns = null; + AutoentityTemplate? template = null; + EntityPermission[]? permissions = null; + + while (reader.Read()) + { + if (reader.TokenType == JsonTokenType.EndObject) + { + return new Autoentity(patterns, template, permissions); + } + + string? propertyName = reader.GetString(); + + reader.Read(); + switch (propertyName) + { + case "patterns": + AutoentityPatternsConverter patternsConverter = new(_replacementSettings); + patterns = patternsConverter.Read(ref reader, typeof(AutoentityPatterns), options); + break; + + case "template": + AutoentityTemplateConverter templateConverter = new(_replacementSettings); + template = templateConverter.Read(ref reader, typeof(AutoentityTemplate), options); + break; + + case "permissions": + permissions = JsonSerializer.Deserialize(ref reader, options) + ?? throw new JsonException("The 'permissions' property must contain at least one permission."); + break; + + default: + throw new JsonException($"Unexpected property {propertyName}"); + } + } + } + + throw new JsonException("Failed to read the Autoentities"); + } + + /// + /// When writing the autoentities back to a JSON file, only write the properties + /// if they are user provided. This avoids polluting the written JSON file with properties + /// the user most likely omitted when writing the original DAB runtime config file. + /// This Write operation is only used when a RuntimeConfig object is serialized to JSON. + /// + /// + public override void Write(Utf8JsonWriter writer, Autoentity value, JsonSerializerOptions options) + { + writer.WriteStartObject(); + + AutoentityPatterns? patterns = value?.Patterns; + if (patterns?.UserProvidedIncludeOptions is true + || patterns?.UserProvidedExcludeOptions is true + || patterns?.UserProvidedNameOptions is true) + { + AutoentityPatternsConverter autoentityPatternsConverter = options.GetConverter(typeof(AutoentityPatterns)) as AutoentityPatternsConverter ?? + throw new JsonException("Failed to get autoentities.patterns options converter"); + writer.WritePropertyName("patterns"); + autoentityPatternsConverter.Write(writer, patterns, options); + } + + AutoentityTemplate? template = value?.Template; + if (template?.UserProvidedRestOptions is true + || template?.UserProvidedGraphQLOptions is true + || template?.UserProvidedHealthOptions is true + || template?.UserProvidedCacheOptions is true) + { + AutoentityTemplateConverter autoentityTemplateConverter = options.GetConverter(typeof(AutoentityTemplate)) as AutoentityTemplateConverter ?? + throw new JsonException("Failed to get autoentities.template options converter"); + writer.WritePropertyName("template"); + autoentityTemplateConverter.Write(writer, template, options); + } + + if (value?.Permissions is not null) + { + writer.WritePropertyName("permissions"); + JsonSerializer.Serialize(writer, value.Permissions, options); + } + + writer.WriteEndObject(); + } +} diff --git a/src/Config/Converters/AutoentityPatternsConverter.cs b/src/Config/Converters/AutoentityPatternsConverter.cs new file mode 100644 index 0000000000..d8029ff033 --- /dev/null +++ b/src/Config/Converters/AutoentityPatternsConverter.cs @@ -0,0 +1,151 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json; +using System.Text.Json.Serialization; +using Azure.DataApiBuilder.Config.ObjectModel; + +namespace Azure.DataApiBuilder.Config.Converters; + +internal class AutoentityPatternsConverter : JsonConverter +{ + // Settings for variable replacement during deserialization. + private readonly DeserializationVariableReplacementSettings? _replacementSettings; + + /// Settings for variable replacement during deserialization. + /// If null, no variable replacement will be performed. + public AutoentityPatternsConverter(DeserializationVariableReplacementSettings? replacementSettings = null) + { + _replacementSettings = replacementSettings; + } + + /// + public override AutoentityPatterns? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType is JsonTokenType.StartObject) + { + string[]? include = null; + string[]? exclude = null; + string? name = null; + + while (reader.Read()) + { + if (reader.TokenType == JsonTokenType.EndObject) + { + return new AutoentityPatterns(include, exclude, name); + } + + string? propertyName = reader.GetString(); + + reader.Read(); + switch (propertyName) + { + case "include": + if (reader.TokenType is not JsonTokenType.Null) + { + List includeList = new(); + + if (reader.TokenType == JsonTokenType.StartArray) + { + while (reader.Read() && reader.TokenType != JsonTokenType.EndArray) + { + string? value = reader.DeserializeString(_replacementSettings); + if (value is not null) + { + includeList.Add(value); + } + } + + include = includeList.ToArray(); + } + else + { + throw new JsonException("Expected array for 'include' property."); + } + } + + break; + + case "exclude": + if (reader.TokenType is not JsonTokenType.Null) + { + List excludeList = new(); + + if (reader.TokenType == JsonTokenType.StartArray) + { + while (reader.Read() && reader.TokenType != JsonTokenType.EndArray) + { + string? value = reader.DeserializeString(_replacementSettings); + if (value is not null) + { + excludeList.Add(value); + } + } + + exclude = excludeList.ToArray(); + } + else + { + throw new JsonException("Expected array for 'exclude' property."); + } + } + + break; + + case "name": + name = reader.DeserializeString(_replacementSettings); + break; + + default: + throw new JsonException($"Unexpected property {propertyName}"); + } + } + } + + throw new JsonException("Failed to read the Autoentities Pattern Options"); + } + + /// + /// When writing the autoentities.patterns back to a JSON file, only write the properties + /// if they are user provided. This avoids polluting the written JSON file with properties + /// the user most likely omitted when writing the original DAB runtime config file. + /// This Write operation is only used when a RuntimeConfig object is serialized to JSON. + /// + /// + public override void Write(Utf8JsonWriter writer, AutoentityPatterns value, JsonSerializerOptions options) + { + writer.WriteStartObject(); + + if (value?.UserProvidedIncludeOptions is true) + { + writer.WritePropertyName("include"); + writer.WriteStartArray(); + foreach (string? include in value.Include) + { + JsonSerializer.Serialize(writer, include, options); + } + + writer.WriteEndArray(); + } + + if (value?.UserProvidedExcludeOptions is true) + { + writer.WritePropertyName("exclude"); + writer.WriteStartArray(); + foreach (string? exclude in value.Exclude) + { + JsonSerializer.Serialize(writer, exclude, options); + } + + writer.WriteEndArray(); + } + + if (value?.UserProvidedNameOptions is true) + { + writer.WritePropertyName("name"); + JsonSerializer.Serialize(writer, value.Name, options); + } + + writer.WriteEndObject(); + } +} diff --git a/src/Config/Converters/AutoentityTemplateConverter.cs b/src/Config/Converters/AutoentityTemplateConverter.cs new file mode 100644 index 0000000000..275cfc4314 --- /dev/null +++ b/src/Config/Converters/AutoentityTemplateConverter.cs @@ -0,0 +1,133 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json; +using System.Text.Json.Serialization; +using Azure.DataApiBuilder.Config.ObjectModel; + +namespace Azure.DataApiBuilder.Config.Converters; + +internal class AutoentityTemplateConverter : JsonConverter +{ + // Settings for variable replacement during deserialization. + private readonly DeserializationVariableReplacementSettings? _replacementSettings; + + /// Settings for variable replacement during deserialization. + /// If null, no variable replacement will be performed. + public AutoentityTemplateConverter(DeserializationVariableReplacementSettings? replacementSettings = null) + { + _replacementSettings = replacementSettings; + } + + /// + public override AutoentityTemplate? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType is JsonTokenType.StartObject) + { + // Create converters for each of the sub-properties. + EntityRestOptionsConverterFactory restOptionsConverterFactory = new(_replacementSettings); + JsonConverter restOptionsConverter = (JsonConverter)(restOptionsConverterFactory.CreateConverter(typeof(EntityRestOptions), options) + ?? throw new JsonException("Unable to create converter for EntityRestOptions")); + + EntityGraphQLOptionsConverterFactory graphQLOptionsConverterFactory = new(_replacementSettings); + JsonConverter graphQLOptionsConverter = (JsonConverter)(graphQLOptionsConverterFactory.CreateConverter(typeof(EntityGraphQLOptions), options) + ?? throw new JsonException("Unable to create converter for EntityGraphQLOptions")); + + EntityMcpOptionsConverterFactory mcpOptionsConverterFactory = new(); + JsonConverter mcpOptionsConverter = (JsonConverter)(mcpOptionsConverterFactory.CreateConverter(typeof(EntityMcpOptions), options) + ?? throw new JsonException("Unable to create converter for EntityMcpOptions")); + + EntityHealthOptionsConvertorFactory healthOptionsConverterFactory = new(); + JsonConverter healthOptionsConverter = (JsonConverter)(healthOptionsConverterFactory.CreateConverter(typeof(EntityHealthCheckConfig), options) + ?? throw new JsonException("Unable to create converter for EntityHealthCheckConfig")); + + EntityCacheOptionsConverterFactory cacheOptionsConverterFactory = new(_replacementSettings); + JsonConverter cacheOptionsConverter = (JsonConverter)(cacheOptionsConverterFactory.CreateConverter(typeof(EntityCacheOptions), options) + ?? throw new JsonException("Unable to create converter for EntityCacheOptions")); + + // Initialize all sub-properties to null. + EntityRestOptions? rest = null; + EntityGraphQLOptions? graphQL = null; + EntityMcpOptions? mcp = null; + EntityHealthCheckConfig? health = null; + EntityCacheOptions? cache = null; + + while (reader.Read()) + { + if (reader.TokenType == JsonTokenType.EndObject) + { + return new AutoentityTemplate(rest, graphQL, mcp, health, cache); + } + + string? propertyName = reader.GetString(); + + reader.Read(); + switch (propertyName) + { + case "rest": + rest = restOptionsConverter.Read(ref reader, typeof(EntityRestOptions), options); + break; + + case "graphql": + graphQL = graphQLOptionsConverter.Read(ref reader, typeof(EntityGraphQLOptions), options); + break; + + case "mcp": + mcp = mcpOptionsConverter.Read(ref reader, typeof(EntityMcpOptions), options); + break; + + case "health": + health = healthOptionsConverter.Read(ref reader, typeof(EntityHealthCheckConfig), options); + break; + + case "cache": + cache = cacheOptionsConverter.Read(ref reader, typeof(EntityCacheOptions), options); + break; + + default: + throw new JsonException($"Unexpected property {propertyName}"); + } + } + } + + throw new JsonException("Failed to read the Autoentities Template Options"); + } + + /// + /// When writing the autoentities.template back to a JSON file, only write the properties + /// if they are user provided. This avoids polluting the written JSON file with properties + /// the user most likely omitted when writing the original DAB runtime config file. + /// This Write operation is only used when a RuntimeConfig object is serialized to JSON. + /// + /// + public override void Write(Utf8JsonWriter writer, AutoentityTemplate value, JsonSerializerOptions options) + { + writer.WriteStartObject(); + + if (value?.UserProvidedRestOptions is true) + { + writer.WritePropertyName("rest"); + JsonSerializer.Serialize(writer, value.Rest, options); + } + + if (value?.UserProvidedGraphQLOptions is true) + { + writer.WritePropertyName("graphql"); + JsonSerializer.Serialize(writer, value.GraphQL, options); + } + + if (value?.UserProvidedHealthOptions is true) + { + writer.WritePropertyName("health"); + JsonSerializer.Serialize(writer, value.Health, options); + } + + if (value?.UserProvidedCacheOptions is true) + { + writer.WritePropertyName("cache"); + JsonSerializer.Serialize(writer, value.Cache, options); + } + + writer.WriteEndObject(); + } +} diff --git a/src/Config/Converters/AzureLogAnalyticsOptionsConverterFactory.cs b/src/Config/Converters/AzureLogAnalyticsOptionsConverterFactory.cs index fc7c72d655..608c8e79e3 100644 --- a/src/Config/Converters/AzureLogAnalyticsOptionsConverterFactory.cs +++ b/src/Config/Converters/AzureLogAnalyticsOptionsConverterFactory.cs @@ -142,7 +142,7 @@ public override void Write(Utf8JsonWriter writer, AzureLogAnalyticsOptions value if (value?.Auth is not null && (value.Auth.UserProvidedCustomTableName || value.Auth.UserProvidedDcrImmutableId || value.Auth.UserProvidedDceEndpoint)) { AzureLogAnalyticsAuthOptionsConverter authOptionsConverter = options.GetConverter(typeof(AzureLogAnalyticsAuthOptions)) as AzureLogAnalyticsAuthOptionsConverter ?? - throw new JsonException("Failed to get azure-log-analytics.auth options converter"); + throw new JsonException("Failed to get azure-log-analytics.auth options converter"); writer.WritePropertyName("auth"); authOptionsConverter.Write(writer, value.Auth, options); diff --git a/src/Config/Converters/EntityRestOptionsConverterFactory.cs b/src/Config/Converters/EntityRestOptionsConverterFactory.cs index f8c9096673..7f8fc87a05 100644 --- a/src/Config/Converters/EntityRestOptionsConverterFactory.cs +++ b/src/Config/Converters/EntityRestOptionsConverterFactory.cs @@ -90,6 +90,7 @@ public EntityRestOptionsConverter(DeserializationVariableReplacementSettings? re restOptions = restOptions with { Methods = methods.ToArray() }; break; + case "enabled": reader.Read(); restOptions = restOptions with { Enabled = reader.GetBoolean() }; diff --git a/src/Config/Converters/RuntimeAutoentitiesConverter.cs b/src/Config/Converters/RuntimeAutoentitiesConverter.cs new file mode 100644 index 0000000000..b65bcb9989 --- /dev/null +++ b/src/Config/Converters/RuntimeAutoentitiesConverter.cs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.ObjectModel; +using System.Text.Json; +using System.Text.Json.Serialization; +using Azure.DataApiBuilder.Config.ObjectModel; + +namespace Azure.DataApiBuilder.Config.Converters; + +/// +/// This converter is used to convert all the autoentities defined in the configuration file +/// each into a object. The resulting collection is then wrapped in the +/// object. +/// +class RuntimeAutoentitiesConverter : JsonConverter +{ + /// + public override RuntimeAutoentities? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + Dictionary autoEntities = + JsonSerializer.Deserialize>(ref reader, options) ?? + throw new JsonException("Failed to read autoentities"); + + return new RuntimeAutoentities(new ReadOnlyDictionary(autoEntities)); + } + + /// + public override void Write(Utf8JsonWriter writer, RuntimeAutoentities value, JsonSerializerOptions options) + { + writer.WriteStartObject(); + foreach ((string key, Autoentity autoEntity) in value.AutoEntities) + { + writer.WritePropertyName(key); + JsonSerializer.Serialize(writer, autoEntity, options); + } + + writer.WriteEndObject(); + } +} diff --git a/src/Config/ObjectModel/Autoentity.cs b/src/Config/ObjectModel/Autoentity.cs new file mode 100644 index 0000000000..45fca68642 --- /dev/null +++ b/src/Config/ObjectModel/Autoentity.cs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json.Serialization; + +namespace Azure.DataApiBuilder.Config.ObjectModel; + +/// +/// Defines an individual auto-entity definition with patterns, template, and permissions. +/// +/// Pattern matching rules for including/excluding database objects +/// Template configuration for generated entities +/// Permissions configuration for generated entities (at least one required) +public record Autoentity +{ + public AutoentityPatterns Patterns { get; init; } + public AutoentityTemplate Template { get; init; } + public EntityPermission[] Permissions { get; init; } + + [JsonConstructor] + public Autoentity( + AutoentityPatterns? Patterns, + AutoentityTemplate? Template, + EntityPermission[]? Permissions) + { + this.Patterns = Patterns ?? new AutoentityPatterns(); + + this.Template = Template ?? new AutoentityTemplate(); + + this.Permissions = Permissions ?? Array.Empty(); + } +} diff --git a/src/Config/ObjectModel/AutoentityPatterns.cs b/src/Config/ObjectModel/AutoentityPatterns.cs new file mode 100644 index 0000000000..d287c56109 --- /dev/null +++ b/src/Config/ObjectModel/AutoentityPatterns.cs @@ -0,0 +1,99 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Diagnostics.CodeAnalysis; +using System.Text.Json.Serialization; + +namespace Azure.DataApiBuilder.Config.ObjectModel; + +/// +/// Defines the pattern matching rules for auto-entities. +/// +/// T-SQL LIKE pattern to include database objects +/// T-SQL LIKE pattern to exclude database objects +/// Interpolation syntax for entity naming (must be unique for each generated entity) +public record AutoentityPatterns +{ + public string[] Include { get; init; } + public string[] Exclude { get; init; } + public string Name { get; init; } + + [JsonConstructor] + public AutoentityPatterns( + string[]? Include = null, + string[]? Exclude = null, + string? Name = null) + { + if (Include is not null) + { + this.Include = Include; + UserProvidedIncludeOptions = true; + } + else + { + this.Include = ["%.%"]; + } + + if (Exclude is not null) + { + this.Exclude = Exclude; + UserProvidedExcludeOptions = true; + } + else + { + this.Exclude = []; + } + + if (!string.IsNullOrWhiteSpace(Name)) + { + this.Name = Name; + UserProvidedNameOptions = true; + } + else + { + this.Name = "{object}"; + } + } + + /// + /// Flag which informs CLI and JSON serializer whether to write include + /// property and value to the runtime config file. + /// When user doesn't provide the include property/value, which signals DAB to use the default, + /// the DAB CLI should not write the default value to a serialized config. + /// This is because the user's intent is to use DAB's default value which could change + /// and DAB CLI writing the property and value would lose the user's intent. + /// This is because if the user were to use the CLI created config, a include + /// property/value specified would be interpreted by DAB as "user explicitly set include." + /// + [JsonIgnore(Condition = JsonIgnoreCondition.Always)] + [MemberNotNullWhen(true, nameof(Include))] + public bool UserProvidedIncludeOptions { get; init; } = false; + + /// + /// Flag which informs CLI and JSON serializer whether to write exclude + /// property and value to the runtime config file. + /// When user doesn't provide the exclude property/value, which signals DAB to use the default, + /// the DAB CLI should not write the default value to a serialized config. + /// This is because the user's intent is to use DAB's default value which could change + /// and DAB CLI writing the property and value would lose the user's intent. + /// This is because if the user were to use the CLI created config, a exclude + /// property/value specified would be interpreted by DAB as "user explicitly set exclude." + /// + [JsonIgnore(Condition = JsonIgnoreCondition.Always)] + [MemberNotNullWhen(true, nameof(Exclude))] + public bool UserProvidedExcludeOptions { get; init; } = false; + + /// + /// Flag which informs CLI and JSON serializer whether to write name + /// property and value to the runtime config file. + /// When user doesn't provide the name property/value, which signals DAB to use the default, + /// the DAB CLI should not write the default value to a serialized config. + /// This is because the user's intent is to use DAB's default value which could change + /// and DAB CLI writing the property and value would lose the user's intent. + /// This is because if the user were to use the CLI created config, a name + /// property/value specified would be interpreted by DAB as "user explicitly set name." + /// + [JsonIgnore(Condition = JsonIgnoreCondition.Always)] + [MemberNotNullWhen(true, nameof(Name))] + public bool UserProvidedNameOptions { get; init; } = false; +} diff --git a/src/Config/ObjectModel/AutoentityTemplate.cs b/src/Config/ObjectModel/AutoentityTemplate.cs new file mode 100644 index 0000000000..78a804d0a4 --- /dev/null +++ b/src/Config/ObjectModel/AutoentityTemplate.cs @@ -0,0 +1,153 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Diagnostics.CodeAnalysis; +using System.Text.Json.Serialization; + +namespace Azure.DataApiBuilder.Config.ObjectModel; + +/// +/// Template used by auto-entities to configure all entities it generates. +/// +/// MCP endpoint configuration +/// REST endpoint configuration +/// GraphQL endpoint configuration +/// Health check configuration +/// Cache configuration +public record AutoentityTemplate +{ + public EntityMcpOptions? Mcp { get; init; } + public EntityRestOptions Rest { get; init; } + public EntityGraphQLOptions GraphQL { get; init; } + public EntityHealthCheckConfig Health { get; init; } + public EntityCacheOptions Cache { get; init; } + + [JsonConstructor] + public AutoentityTemplate( + EntityRestOptions? Rest = null, + EntityGraphQLOptions? GraphQL = null, + EntityMcpOptions? Mcp = null, + EntityHealthCheckConfig? Health = null, + EntityCacheOptions? Cache = null) + { + if (Rest is not null) + { + this.Rest = Rest; + UserProvidedRestOptions = true; + } + else + { + this.Rest = new EntityRestOptions(); + } + + if (GraphQL is not null) + { + this.GraphQL = GraphQL; + UserProvidedGraphQLOptions = true; + } + else + { + this.GraphQL = new EntityGraphQLOptions(string.Empty, string.Empty); + } + + if (Mcp is not null) + { + this.Mcp = Mcp; + UserProvidedMcpOptions = true; + } + else + { + this.Mcp = new EntityMcpOptions(null, null); + } + + if (Health is not null) + { + this.Health = Health; + UserProvidedHealthOptions = true; + } + else + { + this.Health = new EntityHealthCheckConfig(); + } + + if (Cache is not null) + { + this.Cache = Cache; + UserProvidedCacheOptions = true; + } + else + { + this.Cache = new EntityCacheOptions(); + } + } + + /// + /// Flag which informs CLI and JSON serializer whether to write rest + /// property and value to the runtime config file. + /// When user doesn't provide the rest property/value, which signals DAB to use the default, + /// the DAB CLI should not write the default value to a serialized config. + /// This is because the user's intent is to use DAB's default value which could change + /// and DAB CLI writing the property and value would lose the user's intent. + /// This is because if the user were to use the CLI created config, a rest + /// property/value specified would be interpreted by DAB as "user explicitly set rest." + /// + [JsonIgnore(Condition = JsonIgnoreCondition.Always)] + [MemberNotNullWhen(true, nameof(Rest))] + public bool UserProvidedRestOptions { get; init; } = false; + + /// + /// Flag which informs CLI and JSON serializer whether to write graphql + /// property and value to the runtime config file. + /// When user doesn't provide the graphql property/value, which signals DAB to use the default, + /// the DAB CLI should not write the default value to a serialized config. + /// This is because the user's intent is to use DAB's default value which could change + /// and DAB CLI writing the property and value would lose the user's intent. + /// This is because if the user were to use the CLI created config, a graphql + /// property/value specified would be interpreted by DAB as "user explicitly set graphql." + /// + [JsonIgnore(Condition = JsonIgnoreCondition.Always)] + [MemberNotNullWhen(true, nameof(GraphQL))] + public bool UserProvidedGraphQLOptions { get; init; } = false; + + /// + /// Flag which informs CLI and JSON serializer whether to write mcp + /// property and value to the runtime config file. + /// When user doesn't provide the mcp property/value, which signals DAB to use the default, + /// the DAB CLI should not write the default value to a serialized config. + /// This is because the user's intent is to use DAB's default value which could change + /// and DAB CLI writing the property and value would lose the user's intent. + /// This is because if the user were to use the CLI created config, a mcp + /// property/value specified would be interpreted by DAB as "user explicitly set mcp." + /// + [JsonIgnore(Condition = JsonIgnoreCondition.Always)] + [MemberNotNullWhen(true, nameof(Mcp))] + public bool UserProvidedMcpOptions { get; init; } = false; + + /// + /// Flag which informs CLI and JSON serializer whether to write health + /// property and value to the runtime config file. + /// When user doesn't provide the health property/value, which signals DAB to use the default, + /// the DAB CLI should not write the default value to a serialized config. + /// This is because the user's intent is to use DAB's default value which could change + /// and DAB CLI writing the property and value would lose the user's intent. + /// This is because if the user were to use the CLI created config, a health + /// property/value specified would be interpreted by DAB as "user explicitly set health." + /// + [JsonIgnore(Condition = JsonIgnoreCondition.Always)] + [MemberNotNullWhen(true, nameof(Health))] + public bool UserProvidedHealthOptions { get; init; } = false; + + /// + /// Flag which informs CLI and JSON serializer whether to write cache + /// property and value to the runtime config file. + /// When user doesn't provide the cache property/value, which signals DAB to use the default, + /// the DAB CLI should not write the default value to a serialized config. + /// This is because the user's intent is to use DAB's default value which could change + /// and DAB CLI writing the property and value would lose the user's intent. + /// This is because if the user were to use the CLI created config, a cache + /// property/value specified would be interpreted by DAB as "user explicitly set cache." + /// + [JsonIgnore(Condition = JsonIgnoreCondition.Always)] + [MemberNotNullWhen(true, nameof(Cache))] + public bool UserProvidedCacheOptions { get; init; } = false; +} diff --git a/src/Config/ObjectModel/RuntimeAutoentities.cs b/src/Config/ObjectModel/RuntimeAutoentities.cs new file mode 100644 index 0000000000..0fec45f5a1 --- /dev/null +++ b/src/Config/ObjectModel/RuntimeAutoentities.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json.Serialization; +using Azure.DataApiBuilder.Config.Converters; + +namespace Azure.DataApiBuilder.Config.ObjectModel; + +/// +/// Represents a collection of available from the RuntimeConfig. +/// +[JsonConverter(typeof(RuntimeAutoentitiesConverter))] +public record RuntimeAutoentities +{ + /// + /// The collection of available from the RuntimeConfig. + /// + public IReadOnlyDictionary AutoEntities { get; init; } + + /// + /// Creates a new instance of the class using a collection of entities. + /// + /// The collection of auto-entities to map to RuntimeAutoentities. + public RuntimeAutoentities(IReadOnlyDictionary autoEntities) + { + AutoEntities = autoEntities; + } +} diff --git a/src/Config/ObjectModel/RuntimeConfig.cs b/src/Config/ObjectModel/RuntimeConfig.cs index 6896d82161..b5957b5e46 100644 --- a/src/Config/ObjectModel/RuntimeConfig.cs +++ b/src/Config/ObjectModel/RuntimeConfig.cs @@ -25,6 +25,8 @@ public record RuntimeConfig [JsonPropertyName("azure-key-vault")] public AzureKeyVaultOptions? AzureKeyVault { get; init; } + public RuntimeAutoentities? Autoentities { get; init; } + public virtual RuntimeEntities Entities { get; init; } public DataSourceFiles? DataSourceFiles { get; init; } @@ -246,6 +248,7 @@ public RuntimeConfig( string? Schema, DataSource DataSource, RuntimeEntities Entities, + RuntimeAutoentities? Autoentities = null, RuntimeOptions? Runtime = null, DataSourceFiles? DataSourceFiles = null, AzureKeyVaultOptions? AzureKeyVault = null) @@ -255,6 +258,7 @@ public RuntimeConfig( this.Runtime = Runtime; this.AzureKeyVault = AzureKeyVault; this.Entities = Entities; + this.Autoentities = Autoentities; this.DefaultDataSourceName = Guid.NewGuid().ToString(); if (this.DataSource is null) diff --git a/src/Config/RuntimeConfigLoader.cs b/src/Config/RuntimeConfigLoader.cs index 6d6cf6d51b..5996a902ed 100644 --- a/src/Config/RuntimeConfigLoader.cs +++ b/src/Config/RuntimeConfigLoader.cs @@ -314,6 +314,9 @@ public static JsonSerializerOptions GetSerializationOptions( options.Converters.Add(new EntityActionConverterFactory()); options.Converters.Add(new DataSourceFilesConverter()); options.Converters.Add(new EntityCacheOptionsConverterFactory(replacementSettings)); + options.Converters.Add(new AutoentityConverter(replacementSettings)); + options.Converters.Add(new AutoentityPatternsConverter(replacementSettings)); + options.Converters.Add(new AutoentityTemplateConverter(replacementSettings)); options.Converters.Add(new EntityMcpOptionsConverterFactory()); options.Converters.Add(new RuntimeCacheOptionsConverterFactory()); options.Converters.Add(new RuntimeCacheLevel2OptionsConverterFactory()); diff --git a/src/Service.Tests/Caching/DabCacheServiceIntegrationTests.cs b/src/Service.Tests/Caching/DabCacheServiceIntegrationTests.cs index 02b7ca6492..68c9225b96 100644 --- a/src/Service.Tests/Caching/DabCacheServiceIntegrationTests.cs +++ b/src/Service.Tests/Caching/DabCacheServiceIntegrationTests.cs @@ -760,12 +760,13 @@ private static Mock CreateMockRuntimeConfigProvider(strin }); Mock mockRuntimeConfig = new( - string.Empty, - dataSource, - entities, - null, - null, - null + string.Empty, // Schema + dataSource, // DataSource + entities, // Entities + null, // Autoentities + null, // Runtime + null, // DataSourceFiles + null // AzureKeyVault ); mockRuntimeConfig .Setup(c => c.GetDataSourceFromDataSourceName(It.IsAny())) diff --git a/src/Service.Tests/Configuration/ConfigurationTests.cs b/src/Service.Tests/Configuration/ConfigurationTests.cs index 9728c27ee7..6078d8f364 100644 --- a/src/Service.Tests/Configuration/ConfigurationTests.cs +++ b/src/Service.Tests/Configuration/ConfigurationTests.cs @@ -4304,6 +4304,176 @@ public void FileSinkSerialization( } } + /// + /// Test validates that autoentities section can be deserialized and serialized correctly. + /// + [DataTestMethod] + [TestCategory(TestCategory.MSSQL)] + [DataRow(null, null, null, null, null, null, null, null, null, "anonymous", EntityActionOperation.Read)] + [DataRow(new[] { "%.%" }, new[] { "%.%" }, "{object}", true, true, true, false, 5, EntityCacheLevel.L1L2, "anonymous", EntityActionOperation.Read)] + [DataRow(new[] { "books.%" }, new[] { "books.pages.%" }, "books_{object}", false, false, false, true, 2147483647, EntityCacheLevel.L1, "test-user", EntityActionOperation.Delete)] + [DataRow(new[] { "books.%" }, null, "books_{object}", false, null, false, null, 2147483647, null, "test-user", EntityActionOperation.Delete)] + [DataRow(null, new[] { "books.pages.%" }, null, null, false, null, true, null, EntityCacheLevel.L1, "test-user", EntityActionOperation.Delete)] + [DataRow(new[] { "title.%", "books.%", "names.%" }, new[] { "names.%", "%.%" }, "{schema}.{object}", true, false, false, true, 1, null, "second-test-user", EntityActionOperation.Create)] + public void TestAutoEntitiesSerializationDeserialization( + string[]? include, + string[]? exclude, + string? name, + bool? restEnabled, + bool? graphqlEnabled, + bool? healthCheckEnabled, + bool? cacheEnabled, + int? cacheTTL, + EntityCacheLevel? cacheLevel, + string role, + EntityActionOperation entityActionOp) + { + TestHelper.SetupDatabaseEnvironment(MSSQL_ENVIRONMENT); + + Dictionary createdAutoentity = new(); + createdAutoentity.Add("test-entity", + new Autoentity( + Patterns: new AutoentityPatterns(include, exclude, name), + Template: new AutoentityTemplate( + Rest: restEnabled == null ? null : new EntityRestOptions(Enabled: (bool)restEnabled), + GraphQL: graphqlEnabled == null ? null : new EntityGraphQLOptions(Singular: string.Empty, Plural: string.Empty, Enabled: (bool)graphqlEnabled), + Health: healthCheckEnabled == null ? null : new EntityHealthCheckConfig(healthCheckEnabled), + Cache: (cacheEnabled == null && cacheTTL == null && cacheLevel == null) ? null : new EntityCacheOptions(Enabled: cacheEnabled, TtlSeconds: cacheTTL, Level: cacheLevel) + ), + Permissions: new EntityPermission[1])); + + EntityAction[] entityActions = new EntityAction[] { new(entityActionOp, null, null) }; + createdAutoentity["test-entity"].Permissions[0] = new EntityPermission(role, entityActions); + RuntimeAutoentities autoentities = new(createdAutoentity); + + FileSystemRuntimeConfigLoader baseLoader = TestHelper.GetRuntimeConfigLoader(); + baseLoader.TryLoadKnownConfig(out RuntimeConfig? baseConfig); + + RuntimeConfig config = new( + Schema: baseConfig!.Schema, + DataSource: baseConfig.DataSource, + Runtime: new( + Rest: new(), + GraphQL: new(), + Mcp: new(), + Host: new(null, null), + Telemetry: new() + ), + Entities: baseConfig.Entities, + Autoentities: autoentities + ); + + string configWithCustomJson = config.ToJson(); + Assert.IsTrue(RuntimeConfigLoader.TryParseConfig(configWithCustomJson, out RuntimeConfig? deserializedRuntimeConfig)); + + string serializedConfig = deserializedRuntimeConfig.ToJson(); + + using (JsonDocument parsedDocument = JsonDocument.Parse(serializedConfig)) + { + JsonElement root = parsedDocument.RootElement; + JsonElement autoentitiesElement = root.GetProperty("autoentities"); + + bool entityExists = autoentitiesElement.TryGetProperty("test-entity", out JsonElement entityElement); + Assert.AreEqual(expected: true, actual: entityExists); + + // Validate patterns properties and their values exists in autoentities + bool expectedPatternsExist = include != null || exclude != null || name != null; + bool patternsExists = entityElement.TryGetProperty("patterns", out JsonElement patternsElement); + Assert.AreEqual(expected: expectedPatternsExist, actual: patternsExists); + + if (patternsExists) + { + bool includeExists = patternsElement.TryGetProperty("include", out JsonElement includeElement); + Assert.AreEqual(expected: (include != null), actual: includeExists); + if (includeExists) + { + CollectionAssert.AreEqual(expected: include, actual: includeElement.EnumerateArray().Select(e => e.GetString()).ToArray()); + } + + bool excludeExists = patternsElement.TryGetProperty("exclude", out JsonElement excludeElement); + Assert.AreEqual(expected: (exclude != null), actual: excludeExists); + if (excludeExists) + { + CollectionAssert.AreEqual(expected: exclude, actual: excludeElement.EnumerateArray().Select(e => e.GetString()).ToArray()); + } + + bool nameExists = patternsElement.TryGetProperty("name", out JsonElement nameElement); + Assert.AreEqual(expected: (name != null), actual: nameExists); + if (nameExists) + { + Assert.AreEqual(expected: name, actual: nameElement.GetString()); + } + } + + // Validate template properties and their values exists in autoentities + bool expectedTemplateExist = restEnabled != null || graphqlEnabled != null || healthCheckEnabled != null + || cacheEnabled != null || cacheLevel != null || cacheTTL != null; + bool templateExists = entityElement.TryGetProperty("template", out JsonElement templateElement); + Assert.AreEqual(expected: expectedTemplateExist, actual: templateExists); + + if (templateExists) + { + bool restPropertyExists = templateElement.TryGetProperty("rest", out JsonElement restElement); + Assert.AreEqual(expected: (restEnabled != null), actual: restPropertyExists); + if (restPropertyExists) + { + Assert.IsTrue(restElement.TryGetProperty("enabled", out JsonElement restEnabledElement)); + Assert.AreEqual(expected: restEnabled, actual: restEnabledElement.GetBoolean()); + } + + bool graphqlPropertyExists = templateElement.TryGetProperty("graphql", out JsonElement graphqlElement); + Assert.AreEqual(expected: (graphqlEnabled != null), actual: graphqlPropertyExists); + if (graphqlPropertyExists) + { + Assert.IsTrue(graphqlElement.TryGetProperty("enabled", out JsonElement graphqlEnabledElement)); + Assert.AreEqual(expected: graphqlEnabled, actual: graphqlEnabledElement.GetBoolean()); + } + + bool healthPropertyExists = templateElement.TryGetProperty("health", out JsonElement healthElement); + Assert.AreEqual(expected: (healthCheckEnabled != null), actual: healthPropertyExists); + if (healthPropertyExists) + { + Assert.IsTrue(healthElement.TryGetProperty("enabled", out JsonElement healthEnabledElement)); + Assert.AreEqual(expected: healthCheckEnabled, actual: healthEnabledElement.GetBoolean()); + } + + bool expectedCacheExist = cacheEnabled != null || cacheTTL != null || cacheLevel != null; + bool cachePropertyExists = templateElement.TryGetProperty("cache", out JsonElement cacheElement); + Assert.AreEqual(expected: expectedCacheExist, actual: cachePropertyExists); + if (cacheEnabled != null) + { + Assert.IsTrue(cacheElement.TryGetProperty("enabled", out JsonElement cacheEnabledElement)); + Assert.AreEqual(expected: cacheEnabled, actual: cacheEnabledElement.GetBoolean()); + } + + if (cacheTTL != null) + { + Assert.IsTrue(cacheElement.TryGetProperty("ttl-seconds", out JsonElement cacheTtlElement)); + Assert.AreEqual(expected: cacheTTL, actual: cacheTtlElement.GetInt32()); + } + + if (cacheLevel != null) + { + Assert.IsTrue(cacheElement.TryGetProperty("level", out JsonElement cacheLevelElement)); + Assert.IsTrue(string.Equals(cacheLevel.ToString(), cacheLevelElement.GetString(), StringComparison.OrdinalIgnoreCase)); + } + } + + // Validate permissions properties and their values exists in autoentities + JsonElement permissionsElement = entityElement.GetProperty("permissions"); + + bool roleExists = permissionsElement[0].TryGetProperty("role", out JsonElement roleElement); + Assert.AreEqual(expected: true, actual: roleExists); + Assert.AreEqual(expected: role, actual: roleElement.GetString()); + + bool entityActionsExists = permissionsElement[0].TryGetProperty("actions", out JsonElement entityActionsElement); + Assert.AreEqual(expected: true, actual: entityActionsExists); + bool entityActionOpExists = entityActionsElement[0].TryGetProperty("action", out JsonElement entityActionOpElement); + Assert.AreEqual(expected: true, actual: entityActionOpExists); + Assert.IsTrue(string.Equals(entityActionOp.ToString(), entityActionOpElement.GetString(), StringComparison.OrdinalIgnoreCase)); + } + } + #nullable disable /// From 0e1b3c27ef0fafe572013712702baf15fb45f2d3 Mon Sep 17 00:00:00 2001 From: Anusha Kolan Date: Wed, 24 Dec 2025 12:32:25 -0800 Subject: [PATCH 24/36] Changed the default auth provider from SWA to AppService (#2943) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Why make this change? - Closes [#2943](https://github.com/Azure/data-api-builder/issues/2644) Change default auth provider to AppService from StaticWebApps. Azure Static Web Apps EasyAuth is being deprecated, so DAB should no longer default to [StaticWebApps](vscode-file://vscode-app/c:/Program%20Files/Microsoft%20VS%20Code/resources/app/out/vs/code/electron-browser/workbench/workbench.html) as its authentication provider. - Moving the default to `AppService` aligns DAB with the long‑term supported `EasyAuth` path while keeping behavior equivalent for existing workloads. `StaticWebApps` remains supported when explicitly configured, but new configurations and `dab init` flows should guide users toward `AppService` instead of a deprecated option. ## What is this change? -Config and runtime behavior - Changed the default authentication provider from `Static Web Apps` to `App Service` in the core configuration model and JSON schema. - Added validation that logs a warning when Static Web Apps is explicitly selected (since it’s deprecated as a default). -CLI and `dab init` - Updated `dab init` so that, when no auth provider is specified, it now generates configs using App Service as the provider instead of Static Web Apps. - Adjusted CLI configuration generation and option handling so any “default provider” usage now points to App Service. - Updated end-to-end CLI tests and initialization tests so their expected configurations and arguments reference App Service as the default. -Schema, samples, and built‑in configs - Updated the JSON schema to set the default of the `authentication.provider` property to `AppService`. - Updated sample configuration snippets in the main documentation to show App Service as the provider. - Updated the built‑in `dab-config` JSON files (for all supported databases and multi‑DAB scenarios) so their runtime host sections use App Service. -Engine tests and helpers - Updated test helpers to generate EasyAuth principals appropriate for the configured provider, and to treat App Service as the default in REST and GraphQL integration tests. - Adjusted configuration and health‑endpoint tests to no longer assume Static Web Apps as the implicit provider and to accept App Service as the default. -Snapshots and expected outputs - updated a large set of snapshot files (CLI snapshots, configuration snapshots, entity update/add snapshots) so that anywhere the authentication section previously showed Static Web Apps as the provider, it now shows App Service. -Note We updated `AddEnvDetectedEasyAuth` so that it always registers both the `App Service` and `Static Web Apps` `EasyAuth` schemes in development mode, instead of only adding App Service when certain environment variables are present. This aligns with the new default of using App Service as the primary `EasyAuth` provider and makes dev/test/CI behavior deterministic, while still letting configuration (runtime.host.authentication.provider) choose which scheme is actually used. ## How was this tested? - [x] Integration Tests - [x] Unit Tests ## Sample Request(s) `dab init --database-type mssql --connection-string ""` Generates, `"runtime": { "host": { "authentication": { "provider": "AppService" } } }` Users who still want Static Web Apps can override: `dab init --database-type mssql --connection-string "" --auth.provider StaticWebApps` --------- Co-authored-by: Aniruddh Munde Co-authored-by: Souvik Ghosh Co-authored-by: aaronburtle <93220300+aaronburtle@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Anusha Kolan --- README.md | 2 +- schemas/dab.draft.schema.json | 2 +- .../BuiltInTools/DescribeEntitiesTool.cs | 24 ++++++ src/Cli.Tests/AddOpenTelemetryTests.cs | 2 +- src/Cli.Tests/AddTelemetryTests.cs | 2 +- src/Cli.Tests/ConfigGeneratorTests.cs | 4 +- src/Cli.Tests/EndToEndTests.cs | 2 +- src/Cli.Tests/InitTests.cs | 30 +++---- src/Cli.Tests/ModuleInitializer.cs | 2 + ...stingNameButWithDifferentCase.verified.txt | 2 +- ...s.AddEntityWithCachingEnabled.verified.txt | 2 +- ...ldProperties_70de36ebf1478d0d.verified.txt | 2 +- ...ldProperties_9f612e68879149a3.verified.txt | 2 +- ...ldProperties_bea2d26f3e5462d8.verified.txt | 2 +- ...AddNewEntityWhenEntitiesEmpty.verified.txt | 2 +- ...NewEntityWhenEntitiesNotEmpty.verified.txt | 2 +- ...esWithSourceAsStoredProcedure.verified.txt | 2 +- ...rocedureWithBothMcpProperties.verified.txt | 2 +- ...eWithBothMcpPropertiesEnabled.verified.txt | 2 +- ...edureWithMcpCustomToolEnabled.verified.txt | 2 +- ...DmlTools=false_source=authors.verified.txt | 2 +- ...mcpDmlTools=true_source=books.verified.txt | 2 +- ...aphQLOptions_0c9cbb8942b4a4e5.verified.txt | 2 +- ...aphQLOptions_286d268a654ece27.verified.txt | 2 +- ...aphQLOptions_3048323e01b42681.verified.txt | 2 +- ...aphQLOptions_3440d150a2282b9c.verified.txt | 2 +- ...aphQLOptions_381c28d25063be0c.verified.txt | 2 +- ...aphQLOptions_458373311f6ed4ed.verified.txt | 2 +- ...aphQLOptions_66799c963a6306ae.verified.txt | 2 +- ...aphQLOptions_66f598295b8682fd.verified.txt | 2 +- ...aphQLOptions_73f95f7e2cd3ed71.verified.txt | 2 +- ...aphQLOptions_79d59edde7f6a272.verified.txt | 2 +- ...aphQLOptions_7ec82512a1df5293.verified.txt | 2 +- ...aphQLOptions_cbb6e5548e4d3535.verified.txt | 2 +- ...aphQLOptions_dc629052f38cea32.verified.txt | 2 +- ...aphQLOptions_e4a97c7e3507d2c6.verified.txt | 2 +- ...aphQLOptions_f8d0d0c2a38bd3b8.verified.txt | 2 +- ...stMethodsAndGraphQLOperations.verified.txt | 2 +- ...stMethodsAndGraphQLOperations.verified.txt | 2 +- ...tyWithSourceAsStoredProcedure.verified.txt | 2 +- ...tityWithSourceWithDefaultType.verified.txt | 2 +- ...dingEntityWithoutIEnumerables.verified.txt | 2 +- ...ests.TestInitForCosmosDBNoSql.verified.txt | 2 +- ...toredProcedureWithRestMethods.verified.txt | 2 +- ...stMethodsAndGraphQLOperations.verified.txt | 2 +- ...itTests.CosmosDbNoSqlDatabase.verified.txt | 2 +- ...ts.CosmosDbPostgreSqlDatabase.verified.txt | 2 +- ...tStartingSlashWillHaveItAdded.verified.txt | 2 +- .../InitTests.MsSQLDatabase.verified.txt | 2 +- ...tStartingSlashWillHaveItAdded.verified.txt | 2 +- ...ConfigWithoutConnectionString.verified.txt | 2 +- ...lCharactersInConnectionString.verified.txt | 2 +- ...ationOptions_0546bef37027a950.verified.txt | 2 +- ...ationOptions_0ac567dd32a2e8f5.verified.txt | 2 +- ...ationOptions_0c06949221514e77.verified.txt | 2 +- ...ationOptions_18667ab7db033e9d.verified.txt | 2 +- ...ationOptions_2f42f44c328eb020.verified.txt | 2 +- ...ationOptions_3243d3f3441fdcc1.verified.txt | 2 +- ...ationOptions_53350b8b47df2112.verified.txt | 2 +- ...ationOptions_6584e0ec46b8a11d.verified.txt | 2 +- ...ationOptions_81cc88db3d4eecfb.verified.txt | 2 +- ...ationOptions_8ea187616dbb5577.verified.txt | 2 +- ...ationOptions_905845c29560a3ef.verified.txt | 2 +- ...ationOptions_b2fd24fab5b80917.verified.txt | 2 +- ...ationOptions_bd7cd088755287c9.verified.txt | 2 +- ...ationOptions_d2eccba2f836b380.verified.txt | 2 +- ...ationOptions_d463eed7fe5e4bbe.verified.txt | 2 +- ...ationOptions_d5520dd5c33f7b8d.verified.txt | 2 +- ...ationOptions_eab4a6010e602b59.verified.txt | 2 +- ...ationOptions_ecaa688829b4030e.verified.txt | 2 +- ...SourceObject_036a859f50ce167c.verified.txt | 2 +- ...SourceObject_103655d39b48d89f.verified.txt | 2 +- ...SourceObject_442649c7ef2176bd.verified.txt | 2 +- ...SourceObject_7f2338fdc84aafc3.verified.txt | 2 +- ...SourceObject_a70c086a74142c82.verified.txt | 2 +- ...SourceObject_c26902b0e44f97cd.verified.txt | 2 +- ...EntityByAddingNewRelationship.verified.txt | 2 +- ...EntityByModifyingRelationship.verified.txt | 2 +- ...Tests.TestUpdateEntityCaching.verified.txt | 2 +- ...ts.TestUpdateEntityPermission.verified.txt | 2 +- ...tityPermissionByAddingNewRole.verified.txt | 2 +- ...ermissionHavingWildcardAction.verified.txt | 2 +- ...yPermissionWithExistingAction.verified.txt | 2 +- ...yPermissionWithWildcardAction.verified.txt | 2 +- ....TestUpdateEntityWithMappings.verified.txt | 2 +- ...ldProperties_088d6237033e0a7c.verified.txt | 2 +- ...ldProperties_3ea32fdef7aed1b4.verified.txt | 2 +- ...ldProperties_4d25c2c012107597.verified.txt | 2 +- ...ithSpecialCharacterInMappings.verified.txt | 2 +- ...ts.TestUpdateExistingMappings.verified.txt | 2 +- ...eEntityTests.TestUpdatePolicy.verified.txt | 2 +- ...edProcedures_10ea92e3b25ab0c9.verified.txt | 2 +- ...edProcedures_127bb81593f835fe.verified.txt | 2 +- ...edProcedures_386efa1a113fac6b.verified.txt | 2 +- ...edProcedures_53db4712d83be8e6.verified.txt | 2 +- ...edProcedures_5e9ddd8c7c740efd.verified.txt | 2 +- ...edProcedures_6c5b3bfc72e5878a.verified.txt | 2 +- ...edProcedures_8398059a743d7027.verified.txt | 2 +- ...edProcedures_a49380ce6d1fd8ba.verified.txt | 2 +- ...edProcedures_c9b12fe27be53878.verified.txt | 2 +- ...edProcedures_d19603117eb8b51b.verified.txt | 2 +- ...edProcedures_d770d682c5802737.verified.txt | 2 +- ...edProcedures_ef8cc721c9dfc7e4.verified.txt | 2 +- ...edProcedures_f3897e2254996db0.verified.txt | 2 +- ...edProcedures_f4cadb897fc5b0fe.verified.txt | 2 +- ...edProcedures_f59b2a65fc1e18a3.verified.txt | 2 +- ...SourceObject_574e1995f787740f.verified.txt | 2 +- ...SourceObject_a13a9ca73b21f261.verified.txt | 2 +- ...SourceObject_a5ce76c8bea25cc8.verified.txt | 2 +- ...SourceObject_bba111332a1f973f.verified.txt | 2 +- ...rocedureWithBothMcpProperties.verified.txt | 2 +- ...eWithBothMcpPropertiesEnabled.verified.txt | 2 +- ...edureWithMcpCustomToolEnabled.verified.txt | 2 +- ...DmlTools_newMcpDmlTools=false.verified.txt | 2 +- ...pDmlTools_newMcpDmlTools=true.verified.txt | 2 +- ...UpdateDatabaseSourceKeyFields.verified.txt | 2 +- ...ests.UpdateDatabaseSourceName.verified.txt | 2 +- ...pdateDatabaseSourceParameters.verified.txt | 2 +- src/Cli.Tests/TestHelper.cs | 18 ++-- src/Cli.Tests/UpdateEntityTests.cs | 2 +- src/Cli/Commands/ConfigureOptions.cs | 2 +- src/Cli/Commands/InitOptions.cs | 2 +- src/Cli/ConfigGenerator.cs | 3 +- .../ObjectModel/AuthenticationOptions.cs | 4 +- src/Config/ObjectModel/RuntimeConfig.cs | 11 +++ ...lientRoleHeaderAuthenticationMiddleware.cs | 2 +- ...EasyAuthAuthenticationBuilderExtensions.cs | 28 +++--- .../Configurations/RuntimeConfigValidator.cs | 8 ++ .../Helpers/WebHostBuilderHelper.cs | 1 + .../Caching/CachingConfigProcessingTests.cs | 2 +- .../Caching/HealthEndpointCachingTests.cs | 2 +- .../AuthenticationConfigValidatorUnitTests.cs | 2 +- .../Configuration/ConfigurationTests.cs | 86 +++++++++++++++---- .../Configuration/HealthEndpointRolesTests.cs | 21 +++-- .../Configuration/HealthEndpointTests.cs | 2 +- .../HotReload/ConfigurationHotReloadTests.cs | 2 +- .../CosmosTests/MutationTests.cs | 51 +++++++++-- .../CosmosTests/QueryFilterTests.cs | 62 ++++++++++--- src/Service.Tests/CosmosTests/QueryTests.cs | 2 +- src/Service.Tests/ModuleInitializer.cs | 2 + src/Service.Tests/Multidab-config.MsSql.json | 2 +- src/Service.Tests/Multidab-config.MySql.json | 2 +- .../Multidab-config.PostgreSql.json | 2 +- ...ReadingRuntimeConfigForCosmos.verified.txt | 2 +- ...tReadingRuntimeConfigForMsSql.verified.txt | 2 +- ...tReadingRuntimeConfigForMySql.verified.txt | 2 +- ...ingRuntimeConfigForPostgreSql.verified.txt | 2 +- ...s.TestCorsConfigReadCorrectly.verified.txt | 2 +- src/Service.Tests/SqlTests/SqlTestBase.cs | 69 ++++++++++++++- src/Service.Tests/SqlTests/SqlTestHelper.cs | 2 +- src/Service.Tests/TestHelper.cs | 38 +++++--- .../UnitTests/ConfigFileWatcherUnitTests.cs | 2 +- ...untimeConfigLoaderJsonDeserializerTests.cs | 2 +- .../dab-config.CosmosDb_NoSql.json | 2 +- src/Service.Tests/dab-config.DwSql.json | 2 +- src/Service.Tests/dab-config.MsSql.json | 2 +- src/Service.Tests/dab-config.MySql.json | 2 +- src/Service.Tests/dab-config.PostgreSql.json | 2 +- src/Service/Startup.cs | 33 ++++--- 159 files changed, 516 insertions(+), 259 deletions(-) diff --git a/README.md b/README.md index d8edb22a50..ec9d83ca73 100644 --- a/README.md +++ b/README.md @@ -169,7 +169,7 @@ The file `dab-config.json` is automatically created through this process. These "allow-credentials": false }, "authentication": { - "provider": "StaticWebApps" + "provider": "AppService" }, "mode": "development" } diff --git a/schemas/dab.draft.schema.json b/schemas/dab.draft.schema.json index b6a7f66268..b684cc28ac 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/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 a0c882ae74..a03dcddd10 100644 --- a/src/Cli.Tests/ModuleInitializer.cs +++ b/src/Cli.Tests/ModuleInitializer.cs @@ -81,6 +81,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 index d7d3ed0056..c5772388d6 100644 --- a/src/Cli.Tests/Snapshots/AddEntityTests.AddStoredProcedureWithBothMcpProperties.verified.txt +++ b/src/Cli.Tests/Snapshots/AddEntityTests.AddStoredProcedureWithBothMcpProperties.verified.txt @@ -18,7 +18,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps + Provider: AppService } } }, diff --git a/src/Cli.Tests/Snapshots/AddEntityTests.AddStoredProcedureWithBothMcpPropertiesEnabled.verified.txt b/src/Cli.Tests/Snapshots/AddEntityTests.AddStoredProcedureWithBothMcpPropertiesEnabled.verified.txt index aa30025561..f0c74a20b7 100644 --- a/src/Cli.Tests/Snapshots/AddEntityTests.AddStoredProcedureWithBothMcpPropertiesEnabled.verified.txt +++ b/src/Cli.Tests/Snapshots/AddEntityTests.AddStoredProcedureWithBothMcpPropertiesEnabled.verified.txt @@ -18,7 +18,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps + Provider: AppService } } }, diff --git a/src/Cli.Tests/Snapshots/AddEntityTests.AddStoredProcedureWithMcpCustomToolEnabled.verified.txt b/src/Cli.Tests/Snapshots/AddEntityTests.AddStoredProcedureWithMcpCustomToolEnabled.verified.txt index 576e84f6d8..50b1b5c571 100644 --- a/src/Cli.Tests/Snapshots/AddEntityTests.AddStoredProcedureWithMcpCustomToolEnabled.verified.txt +++ b/src/Cli.Tests/Snapshots/AddEntityTests.AddStoredProcedureWithMcpCustomToolEnabled.verified.txt @@ -18,7 +18,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps + Provider: AppService } } }, 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 index 51a278d2a3..e1ba348224 100644 --- 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 @@ -18,7 +18,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps + Provider: AppService } } }, 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 index 4dc41a4d45..82287b53aa 100644 --- 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 @@ -18,7 +18,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps + Provider: AppService } } }, 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 index 627e8e9e01..a2aa5c0f40 100644 --- a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateStoredProcedureWithBothMcpProperties.verified.txt +++ b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateStoredProcedureWithBothMcpProperties.verified.txt @@ -18,7 +18,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps, + Provider: AppService, Jwt: { Audience: , Issuer: diff --git a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateStoredProcedureWithBothMcpPropertiesEnabled.verified.txt b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateStoredProcedureWithBothMcpPropertiesEnabled.verified.txt index 47d181d59d..6526bd5e38 100644 --- a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateStoredProcedureWithBothMcpPropertiesEnabled.verified.txt +++ b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateStoredProcedureWithBothMcpPropertiesEnabled.verified.txt @@ -18,7 +18,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps, + Provider: AppService, Jwt: { Audience: , Issuer: diff --git a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateStoredProcedureWithMcpCustomToolEnabled.verified.txt b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateStoredProcedureWithMcpCustomToolEnabled.verified.txt index 4cb7fb45ef..b74749dcf6 100644 --- a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateStoredProcedureWithMcpCustomToolEnabled.verified.txt +++ b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateStoredProcedureWithMcpCustomToolEnabled.verified.txt @@ -18,7 +18,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps, + Provider: AppService, Jwt: { Audience: , Issuer: diff --git a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateTableEntityWithMcpDmlTools_newMcpDmlTools=false.verified.txt b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateTableEntityWithMcpDmlTools_newMcpDmlTools=false.verified.txt index 42cb419190..9efb911f99 100644 --- a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateTableEntityWithMcpDmlTools_newMcpDmlTools=false.verified.txt +++ b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateTableEntityWithMcpDmlTools_newMcpDmlTools=false.verified.txt @@ -18,7 +18,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps, + Provider: AppService, Jwt: { Audience: , Issuer: diff --git a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateTableEntityWithMcpDmlTools_newMcpDmlTools=true.verified.txt b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateTableEntityWithMcpDmlTools_newMcpDmlTools=true.verified.txt index 4084b29397..8ffa3b4893 100644 --- a/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateTableEntityWithMcpDmlTools_newMcpDmlTools=true.verified.txt +++ b/src/Cli.Tests/Snapshots/UpdateEntityTests.TestUpdateTableEntityWithMcpDmlTools_newMcpDmlTools=true.verified.txt @@ -18,7 +18,7 @@ AllowCredentials: false }, Authentication: { - Provider: StaticWebApps, + Provider: AppService, Jwt: { Audience: , Issuer: 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 2cc03dd8f8..cac2db0a83 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/ConfigGenerator.cs b/src/Cli/ConfigGenerator.cs index 8fcc7cae31..648edc1950 100644 --- a/src/Cli/ConfigGenerator.cs +++ b/src/Cli/ConfigGenerator.cs @@ -1273,7 +1273,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) { 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 b5957b5e46..1e567da1cd 100644 --- a/src/Config/ObjectModel/RuntimeConfig.cs +++ b/src/Config/ObjectModel/RuntimeConfig.cs @@ -100,6 +100,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/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 6078d8f364..1faa8eb08a 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}" @@ -2691,12 +2703,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 = @" @@ -2721,7 +2751,8 @@ public async Task ValidateErrorMessageForMutationWithoutReadPermission() query: graphQLMutation, queryName: "createStock", variables: null, - clientRoleHeader: null + authToken: null, + clientRoleHeader: AuthorizationResolver.ROLE_ANONYMOUS ); Assert.IsNotNull(mutationResponse); @@ -3023,7 +3054,7 @@ public async Task ValidateInheritanceOfReadPermissionFromAnonymous() query: graphQLMutation, queryName: "createStock", variables: null, - authToken: AuthTestHelper.CreateStaticWebAppsEasyAuthToken(), + authToken: AuthTestHelper.CreateAppServiceEasyAuthToken(), clientRoleHeader: AuthorizationResolver.ROLE_AUTHENTICATED ); @@ -3590,7 +3621,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")] @@ -3624,8 +3655,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 { @@ -5450,10 +5483,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, RestRuntimeOptions rest) { - // 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, rest); } @@ -5711,7 +5763,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 84ea40a5a9..a1a8bf4223 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 f0c3984a72..89b7dbc3c4 100644 --- a/src/Service.Tests/ModuleInitializer.cs +++ b/src/Service.Tests/ModuleInitializer.cs @@ -81,6 +81,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/SqlTestBase.cs b/src/Service.Tests/SqlTests/SqlTestBase.cs index bb8ed0628d..5e90e77b85 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 + var 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 b990b96368..2a0e49cf00 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/Startup.cs b/src/Service/Startup.cs index bb164d18e7..333bf57234 100644 --- a/src/Service/Startup.cs +++ b/src/Service/Startup.cs @@ -796,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()) { @@ -831,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); } } @@ -1023,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(); } /// From 725bf8c12b8488525b7d8464d9f662ea297e60dd Mon Sep 17 00:00:00 2001 From: Anusha Kolan Date: Tue, 30 Dec 2025 10:53:04 -0800 Subject: [PATCH 25/36] Added documentation for DAB MCP and AI Foundry integration setup. (#2971) ## Why make this change? This change adds documented instructions on integrating the DAB MCP with AI Foundry using an Azure Container Instance. image --------- Co-authored-by: Aniruddh Munde Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- docs/Testing/ai-foundry-integration.md | 273 +++++++++++++++++++++++++ 1 file changed, 273 insertions(+) create mode 100644 docs/Testing/ai-foundry-integration.md diff --git a/docs/Testing/ai-foundry-integration.md b/docs/Testing/ai-foundry-integration.md new file mode 100644 index 0000000000..c43434a793 --- /dev/null +++ b/docs/Testing/ai-foundry-integration.md @@ -0,0 +1,273 @@ +# 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**. + +## 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. From 090e68e1f640724e977051201ff8f1554b71d865 Mon Sep 17 00:00:00 2001 From: Alekhya-Polavarapu Date: Tue, 13 Jan 2026 09:41:36 -0800 Subject: [PATCH 26/36] Fix serialization for StoredProcedureDefinition inheritance (#3045) ## Why make this change? - To apply correct serialization and deserialization logic for stored procedures. With the previous changes, serialization was not working correctly for the StoredProcedureDefinition type, which extends SourceDefinition. When the value type was passed explicitly for serialization, the parent type was used instead, causing some child-type properties to be omitted. ## What is this change? Instead of manually specifying the value type during serialization, this change allows the library to infer the type automatically and perform the correct serialization. ## How was this tested? - [x] Unit Tests --------- Co-authored-by: Aniruddh Munde --- .../Converters/DatabaseObjectConverter.cs | 16 +++++------ .../SerializationDeserializationTests.cs | 28 +++++++++++++++---- 2 files changed, 30 insertions(+), 14 deletions(-) diff --git a/src/Core/Services/MetadataProviders/Converters/DatabaseObjectConverter.cs b/src/Core/Services/MetadataProviders/Converters/DatabaseObjectConverter.cs index d1894c7c45..6d625c7f9d 100644 --- a/src/Core/Services/MetadataProviders/Converters/DatabaseObjectConverter.cs +++ b/src/Core/Services/MetadataProviders/Converters/DatabaseObjectConverter.cs @@ -34,7 +34,7 @@ public override DatabaseObject Read(ref Utf8JsonReader reader, Type typeToConver DatabaseObject objA = (DatabaseObject)JsonSerializer.Deserialize(document, concreteType, options)!; - foreach (PropertyInfo prop in objA.GetType().GetProperties().Where(IsSourceDefinitionProperty)) + foreach (PropertyInfo prop in objA.GetType().GetProperties().Where(IsSourceDefinitionOrDerivedClassProperty)) { SourceDefinition? sourceDef = (SourceDefinition?)prop.GetValue(objA); if (sourceDef is not null) @@ -73,25 +73,23 @@ public override void Write(Utf8JsonWriter writer, DatabaseObject value, JsonSeri writer.WritePropertyName(prop.Name); object? propVal = prop.GetValue(value); - Type propType = prop.PropertyType; - // Only escape columns for properties whose type is exactly SourceDefinition (not subclasses). - // This is because, we do not want unnecessary mutation of subclasses of SourceDefinition unless needed. - if (IsSourceDefinitionProperty(prop) && propVal is SourceDefinition sourceDef && propVal.GetType() == typeof(SourceDefinition)) + // Only escape columns for properties whose type(derived type) is SourceDefinition. + if (IsSourceDefinitionOrDerivedClassProperty(prop) && propVal is SourceDefinition sourceDef) { EscapeDollaredColumns(sourceDef); } - JsonSerializer.Serialize(writer, propVal, propType, options); + JsonSerializer.Serialize(writer, propVal, options); } writer.WriteEndObject(); } - private static bool IsSourceDefinitionProperty(PropertyInfo prop) + private static bool IsSourceDefinitionOrDerivedClassProperty(PropertyInfo prop) { - // Only return true for properties whose type is exactly SourceDefinition (not subclasses) - return prop.PropertyType == typeof(SourceDefinition); + // Return true for properties whose type is SourceDefinition or any class derived from SourceDefinition + return typeof(SourceDefinition).IsAssignableFrom(prop.PropertyType); } /// diff --git a/src/Service.Tests/UnitTests/SerializationDeserializationTests.cs b/src/Service.Tests/UnitTests/SerializationDeserializationTests.cs index a4cb2848ad..12ae3ee993 100644 --- a/src/Service.Tests/UnitTests/SerializationDeserializationTests.cs +++ b/src/Service.Tests/UnitTests/SerializationDeserializationTests.cs @@ -299,8 +299,11 @@ public void TestDictionaryDatabaseObjectSerializationDeserialization_WithDollarC Dictionary dict = new() { { "person", _databaseTable } }; string serializedDict = JsonSerializer.Serialize(dict, _options); - Dictionary deserializedDict = JsonSerializer.Deserialize>(serializedDict, _options)!; + // Assert that the serialized JSON contains the escaped dollar sign in column name + Assert.IsTrue(serializedDict.Contains("DAB_ESCAPE$FirstName"), + "Serialized JSON should contain the dollar-prefixed column name in SourceDefinition's Columns."); + Dictionary deserializedDict = JsonSerializer.Deserialize>(serializedDict, _options)!; DatabaseTable deserializedDatabaseTable = (DatabaseTable)deserializedDict["person"]; Assert.AreEqual(deserializedDatabaseTable.SourceType, _databaseTable.SourceType); @@ -322,14 +325,22 @@ public void TestDatabaseViewSerializationDeserialization_WithDollarColumn() TestTypeNameChanges(_databaseView, "DatabaseView"); + Dictionary dict = new(); + dict.Add("person", _databaseView); + // Test to catch if there is change in number of properties/fields // Note: On Addition of property make sure it is added in following object creation _databaseView and include in serialization // and deserialization test. int fields = typeof(DatabaseView).GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance).Length; Assert.AreEqual(fields, 6); - string serializedDatabaseView = JsonSerializer.Serialize(_databaseView, _options); - DatabaseView deserializedDatabaseView = JsonSerializer.Deserialize(serializedDatabaseView, _options)!; + string serializedDatabaseView = JsonSerializer.Serialize(dict, _options); + // Assert that the serialized JSON contains the escaped dollar sign in column name + Assert.IsTrue(serializedDatabaseView.Contains("DAB_ESCAPE$FirstName"), + "Serialized JSON should contain the dollar-prefixed column name in SourceDefinition's Columns."); + Dictionary deserializedDict = JsonSerializer.Deserialize>(serializedDatabaseView, _options)!; + + DatabaseView deserializedDatabaseView = (DatabaseView)deserializedDict["person"]; Assert.AreEqual(deserializedDatabaseView.SourceType, _databaseView.SourceType); deserializedDatabaseView.Equals(_databaseView); @@ -349,14 +360,21 @@ public void TestDatabaseStoredProcedureSerializationDeserialization_WithDollarCo TestTypeNameChanges(_databaseStoredProcedure, "DatabaseStoredProcedure"); + Dictionary dict = new(); + dict.Add("person", _databaseStoredProcedure); + // Test to catch if there is change in number of properties/fields // Note: On Addition of property make sure it is added in following object creation _databaseStoredProcedure and include in serialization // and deserialization test. int fields = typeof(DatabaseStoredProcedure).GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance).Length; Assert.AreEqual(fields, 6); - string serializedDatabaseSP = JsonSerializer.Serialize(_databaseStoredProcedure, _options); - DatabaseStoredProcedure deserializedDatabaseSP = JsonSerializer.Deserialize(serializedDatabaseSP, _options)!; + string serializedDatabaseSP = JsonSerializer.Serialize(dict, _options); + // Assert that the serialized JSON contains the escaped dollar sign in column name + Assert.IsTrue(serializedDatabaseSP.Contains("DAB_ESCAPE$FirstName"), + "Serialized JSON should contain the dollar-prefixed column name in SourceDefinition's Columns."); + Dictionary deserializedDict = JsonSerializer.Deserialize>(serializedDatabaseSP, _options)!; + DatabaseStoredProcedure deserializedDatabaseSP = (DatabaseStoredProcedure)deserializedDict["person"]; Assert.AreEqual(deserializedDatabaseSP.SourceType, _databaseStoredProcedure.SourceType); deserializedDatabaseSP.Equals(_databaseStoredProcedure); From 5b43e2bf50e482df18b000736cb97eb5dad49147 Mon Sep 17 00:00:00 2001 From: Anusha Kolan Date: Wed, 14 Jan 2026 11:20:31 -0800 Subject: [PATCH 27/36] Added architecture diagram to the AI Foundry Integration doc (#3036) ## Why make this change? This is a minor PR to add architecture diagram to the AI Foundry Integration documentation. --------- Co-authored-by: Anusha Kolan --- docs/media/dab-aifoundry-architecture.png | Bin 0 -> 1173328 bytes .../ai-foundry-integration.md | 4 +++- .../mcp-inspector-testing.md | 0 3 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 docs/media/dab-aifoundry-architecture.png rename docs/{Testing => testing-guide}/ai-foundry-integration.md (97%) rename docs/{Testing => testing-guide}/mcp-inspector-testing.md (100%) diff --git a/docs/media/dab-aifoundry-architecture.png b/docs/media/dab-aifoundry-architecture.png new file mode 100644 index 0000000000000000000000000000000000000000..6823ef6418806394f4184d13b2fe20d3d2dcef93 GIT binary patch literal 1173328 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 {z84w?bk$F(*K1lV~Xzmr$;rJ{cmzYJA*pMgQrL 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

-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�|Ys%aCmp(Z7m_8zLQ#& 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

P$TjKD+;UR5%OD0h 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≪6xK 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 z%0D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