From 96f6740384e971f1ac94d3af6610af6ab15d6148 Mon Sep 17 00:00:00 2001 From: Timothy Trowbridge Date: Thu, 17 Sep 2026 12:47:10 -0300 Subject: [PATCH 1/6] Fix the Demo container and harden the Demo app The Demo's Dockerfile could not build from any context (it copied *.csproj from a folder holding only the Demo project, whose ProjectReference points outside that context) and its ENTRYPOINT named an assembly that does not exist. It is now a multi-stage build whose context is the repository root: it copies the shared build files, both project files and their lock files, restores the Demo in locked mode, publishes it on the .NET 10 SDK image and runs it on aspnet:10.0 as the non-root app user on port 8080. A root .dockerignore keeps host build output and repository metadata out of the context. CI gains a docker job that builds the image and checks that the container serves "/" (with placeholder settings, since the library validates them at startup), so neither breakage can regress unnoticed. AppDbContextFactory drops the sync-over-async CreateDbContext() and the unused IDbContextFactory implementation, treats a blank connection string as missing, and receives the request's cancellation token from the /test endpoint. /test logs failures and returns a generic problem response instead of echoing exception messages to anonymous callers. The Demo README explains ADC on Cloud Run and on a workstation, the four settings with their user-secrets and environment-variable keys, the SQL to create TestTable (the app's identity has no DDL rights, so the app never creates schema), and the docker build/run commands. The package itself is unchanged, hence the empty changeset. Co-Authored-By: Claude Fable 5.1 --- .changeset/demo-fixes.md | 4 + .dockerignore | 20 +++++ .github/workflows/ci.yml | 25 ++++++ .../Database/AppDbContextFactory.cs | 24 ++--- .../Dockerfile | 32 +++---- .../Program.cs | 13 +-- Csag.AzureSqlFederatedIdentity.Demo/README.md | 87 +++++++++++++++++++ 7 files changed, 173 insertions(+), 32 deletions(-) create mode 100644 .changeset/demo-fixes.md create mode 100644 .dockerignore create mode 100644 Csag.AzureSqlFederatedIdentity.Demo/README.md diff --git a/.changeset/demo-fixes.md b/.changeset/demo-fixes.md new file mode 100644 index 0000000..aeada26 --- /dev/null +++ b/.changeset/demo-fixes.md @@ -0,0 +1,4 @@ +--- +--- + +Fix the Demo's Dockerfile and container, stop its `/test` endpoint from leaking exception details, and document how to run it. diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..764002b --- /dev/null +++ b/.dockerignore @@ -0,0 +1,20 @@ +# Context for Csag.AzureSqlFederatedIdentity.Demo/Dockerfile, which builds from the repository root. +**/bin/ +**/obj/ +**/TestResults/ +.git +.github/ +.vs/ +.idea/ +node_modules/ +nupkgs/ +artifacts/ +.changeset/ +docs/ +scripts/ +**/package.json +package-lock.json +**/*.user +**/*.md +# The library's csproj packs its README into the NuGet package. +!Csag.AzureSqlFederatedIdentity/README.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8776ada..0f9d8d0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -79,3 +79,28 @@ jobs: name: nupkgs path: ./nupkgs retention-days: 7 + + docker: + name: Docker (Demo image) + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v7.0.1 + + - name: Build Demo image + run: docker build -f Csag.AzureSqlFederatedIdentity.Demo/Dockerfile -t csag-demo . + + # The library validates its settings at startup, so the container only reaches "/" with placeholder values. + - name: Smoke test Demo image + run: | + docker run -d -p 8080:8080 --name csag-demo \ + -e Csag.AzureSqlFederatedIdentity__TenantId=placeholder \ + -e Csag.AzureSqlFederatedIdentity__ClientId=placeholder \ + -e Csag.AzureSqlFederatedIdentity__Google__ServiceAccountEmail=placeholder@example.invalid \ + csag-demo + curl --fail --silent --show-error --retry 10 --retry-connrefused --retry-all-errors --retry-delay 1 http://localhost:8080/ + docker stop csag-demo + + - name: Demo container logs + if: failure() + run: docker logs csag-demo diff --git a/Csag.AzureSqlFederatedIdentity.Demo/Database/AppDbContextFactory.cs b/Csag.AzureSqlFederatedIdentity.Demo/Database/AppDbContextFactory.cs index bf5a5a4..29aa03f 100644 --- a/Csag.AzureSqlFederatedIdentity.Demo/Database/AppDbContextFactory.cs +++ b/Csag.AzureSqlFederatedIdentity.Demo/Database/AppDbContextFactory.cs @@ -5,31 +5,33 @@ using Microsoft.Data.SqlClient; using Microsoft.EntityFrameworkCore; - public class AppDbContextFactory : IAppDbContextFactory, IDbContextFactory + public class AppDbContextFactory : IAppDbContextFactory { - private readonly DbContextOptionsBuilder optionsBuilder; + private readonly DbContextOptions options; private readonly IAzureSqlTokenProvider tokenProvider; public AppDbContextFactory(IConfiguration configuration, IAzureSqlTokenProvider tokenProvider) { this.tokenProvider = tokenProvider; - this.optionsBuilder = new DbContextOptionsBuilder(); - var connectionString = configuration.GetConnectionString("DefaultConnection") ?? throw new InvalidOperationException("Connection string 'DefaultConnection' not set."); - this.optionsBuilder.UseSqlServer(connectionString); - } + var connectionString = configuration.GetConnectionString("DefaultConnection"); + if (string.IsNullOrWhiteSpace(connectionString)) + { + throw new InvalidOperationException("Connection string 'DefaultConnection' not set."); + } - public AppDbContext CreateDbContext() - { - return this.CreateDbContextAsync(CancellationToken.None).GetAwaiter().GetResult(); + this.options = new DbContextOptionsBuilder().UseSqlServer(connectionString).Options; } public async Task CreateDbContextAsync(CancellationToken cancellationToken = default) { - var context = new AppDbContext(this.optionsBuilder.Options); + // The connection string carries no credentials; the federated access token authenticates the connection. + var accessToken = await this.tokenProvider.GetAzureSqlAccessTokenAsync(cancellationToken); + + var context = new AppDbContext(this.options); if (context.Database.GetDbConnection() is SqlConnection sqlConnection) { - sqlConnection.AccessToken = await this.tokenProvider.GetAzureSqlAccessTokenAsync(cancellationToken); + sqlConnection.AccessToken = accessToken; } return context; diff --git a/Csag.AzureSqlFederatedIdentity.Demo/Dockerfile b/Csag.AzureSqlFederatedIdentity.Demo/Dockerfile index 55464e8..28d0b76 100644 --- a/Csag.AzureSqlFederatedIdentity.Demo/Dockerfile +++ b/Csag.AzureSqlFederatedIdentity.Demo/Dockerfile @@ -1,24 +1,26 @@ -# Use official .NET SDK image for build -FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +# Build context is the repository root: +# docker build -f Csag.AzureSqlFederatedIdentity.Demo/Dockerfile -t csag-demo . +FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build WORKDIR /src -# Copy csproj and restore as distinct layers -COPY *.csproj ./ -# If you have subfolders, adjust copy commands accordingly: -# e.g., COPY Models/*.csproj or simply copy entire project -RUN dotnet restore +# Project and lock files first, so the restore layer is reused until a dependency changes. +COPY global.json nuget.config Directory.Build.props Directory.Packages.props ./ +COPY Csag.AzureSqlFederatedIdentity/Csag.AzureSqlFederatedIdentity.csproj Csag.AzureSqlFederatedIdentity/packages.lock.json Csag.AzureSqlFederatedIdentity/ +COPY Csag.AzureSqlFederatedIdentity.Demo/Csag.AzureSqlFederatedIdentity.Demo.csproj Csag.AzureSqlFederatedIdentity.Demo/packages.lock.json Csag.AzureSqlFederatedIdentity.Demo/ +RUN dotnet restore Csag.AzureSqlFederatedIdentity.Demo --locked-mode -# Copy everything else and build COPY . . -RUN dotnet publish -c Release -o /app/publish +RUN dotnet publish Csag.AzureSqlFederatedIdentity.Demo -c Release --no-restore -o /app/publish -# Runtime image -FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime +FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS runtime WORKDIR /app -# Ensure ASP.NET listens on port 8080 (Cloud Run default) -ENV ASPNETCORE_URLS=http://+:8080 +# Cloud Run sends requests to the container on $PORT, which defaults to 8080. +ENV ASPNETCORE_HTTP_PORTS=8080 +EXPOSE 8080 -COPY --from=build /app/publish ./ +# Non-root user shipped with the aspnet image. +USER app -ENTRYPOINT ["dotnet", "AspNetEfAzureAdTest.dll"] +COPY --from=build /app/publish . +ENTRYPOINT ["dotnet", "Csag.AzureSqlFederatedIdentity.Demo.dll"] diff --git a/Csag.AzureSqlFederatedIdentity.Demo/Program.cs b/Csag.AzureSqlFederatedIdentity.Demo/Program.cs index b449fc2..72ef268 100644 --- a/Csag.AzureSqlFederatedIdentity.Demo/Program.cs +++ b/Csag.AzureSqlFederatedIdentity.Demo/Program.cs @@ -15,25 +15,26 @@ app.MapGet("/", () => Results.Ok("Cloud Run to Azure SQL via Workload Identity Federation.")); -app.MapGet("/test", async ([FromServices] IAppDbContextFactory dbFactory) => +app.MapGet("/test", async ([FromServices] IAppDbContextFactory dbFactory, [FromServices] ILogger logger, CancellationToken cancellationToken) => { try { - await using var context = await dbFactory.CreateDbContextAsync(); + await using var context = await dbFactory.CreateDbContextAsync(cancellationToken); - var count = await context.TestTable.CountAsync(); + var count = await context.TestTable.CountAsync(cancellationToken); if (count == 0) { return Results.NotFound("No rows found in TestTable."); } - var rows = await context.TestTable.OrderBy(e => e.Id).ToListAsync(); + var rows = await context.TestTable.OrderBy(e => e.Id).ToListAsync(cancellationToken); return Results.Ok(new { count, rows, }); } catch (Exception ex) { - // In production, avoid exposing details; here for debugging: - return Results.Problem(detail: ex.Message); + // The endpoint is unauthenticated, so token-exchange and SQL failures go to the log, not the response. + logger.LogError(ex, "Querying TestTable failed."); + return Results.Problem(); } }); diff --git a/Csag.AzureSqlFederatedIdentity.Demo/README.md b/Csag.AzureSqlFederatedIdentity.Demo/README.md new file mode 100644 index 0000000..9fd4b0a --- /dev/null +++ b/Csag.AzureSqlFederatedIdentity.Demo/README.md @@ -0,0 +1,87 @@ +# Csag.AzureSqlFederatedIdentity.Demo + +A minimal ASP.NET Core app that runs on Google Cloud Run and reads from Azure SQL without a database password. The `Csag.AzureSqlFederatedIdentity` library turns the app's Google identity into an Azure AD access token, and [`Database/AppDbContextFactory.cs`](Database/AppDbContextFactory.cs) attaches that token to each `SqlConnection`. + +| Route | Behaviour | +|---|---| +| `GET /` | Greeting; proves the container is up. | +| `GET /test` | Counts and lists the rows of `dbo.TestTable`: `200` with `{ "count": n, "rows": [...] }`, `404` if the table is empty, `500` (generic problem response) if the token exchange or the SQL connection fails. The reason is in the application log. | + +## Prerequisites + +1. The cloud side described in [docs/cloud-identity-setup.md](../docs/cloud-identity-setup.md): a Google service account, an Azure AD app registration with a federated credential for it, and a database user for that app registration. +2. Google Application Default Credentials (ADC), which the library uses to ask Google for an ID token for the service account: + - **On Cloud Run** there is nothing to configure: set the service's runtime service account to the configured service account (it needs `roles/iam.serviceAccountTokenCreator` on itself). + - **On a workstation** run `gcloud auth application-default login`. Your Google account needs `roles/iam.serviceAccountTokenCreator` on the service account (granted on the service account, not on the project), and the IAM Service Account Credentials API must be enabled in the project (`gcloud services enable iamcredentials.googleapis.com`). +3. The .NET SDK pinned in [global.json](../global.json), and Docker if you want to run the container. + +## Settings + +`appsettings.json` ships with the four values empty. Provide them through user secrets on a workstation or environment variables in a container: + +| Setting | User-secrets / JSON key | Environment variable | +|---|---|---| +| Azure AD tenant ID | `Csag.AzureSqlFederatedIdentity:TenantId` | `Csag.AzureSqlFederatedIdentity__TenantId` | +| Azure AD application (client) ID | `Csag.AzureSqlFederatedIdentity:ClientId` | `Csag.AzureSqlFederatedIdentity__ClientId` | +| Google service account email | `Csag.AzureSqlFederatedIdentity:Google:ServiceAccountEmail` | `Csag.AzureSqlFederatedIdentity__Google__ServiceAccountEmail` | +| Azure SQL connection string | `ConnectionStrings:DefaultConnection` | `ConnectionStrings__DefaultConnection` | + +The first three are validated at startup, and the app refuses to start if any of them is missing. The connection string is checked on the first request to `/test`. + +```shell +dotnet user-secrets set "Csag.AzureSqlFederatedIdentity:TenantId" "" --project Csag.AzureSqlFederatedIdentity.Demo +dotnet user-secrets set "Csag.AzureSqlFederatedIdentity:ClientId" "" --project Csag.AzureSqlFederatedIdentity.Demo +dotnet user-secrets set "Csag.AzureSqlFederatedIdentity:Google:ServiceAccountEmail" "@.iam.gserviceaccount.com" --project Csag.AzureSqlFederatedIdentity.Demo +dotnet user-secrets set "ConnectionStrings:DefaultConnection" "Server=tcp:.database.windows.net,1433;Initial Catalog=;Encrypt=True" --project Csag.AzureSqlFederatedIdentity.Demo +``` + +The connection string must not contain `User ID`, `Password`, `Integrated Security` or `Authentication`: the access token is the credential, and `SqlClient` rejects a connection string that also carries one of those. + +## Schema + +The app never creates schema. The identity it runs as only has `db_datareader` and `db_datawriter` (see the setup guide), so create the table once as the server's Microsoft Entra admin or another login with DDL rights: + +```sql +CREATE TABLE dbo.TestTable +( + Id INT IDENTITY(1, 1) NOT NULL CONSTRAINT PK_TestTable PRIMARY KEY, + Value NVARCHAR(MAX) NOT NULL +); + +INSERT INTO dbo.TestTable (Value) VALUES (N'hello'), (N'world'); +``` + +## Run from source + +```shell +dotnet run --project Csag.AzureSqlFederatedIdentity.Demo +curl http://localhost:5172/test +``` + +## Run in Docker + +The image is built from the **repository root**, because the Demo references the library project: + +```shell +docker build -f Csag.AzureSqlFederatedIdentity.Demo/Dockerfile -t csag-demo . +``` + +The container listens on port 8080 and runs as the non-root `app` user (uid 1654). Outside Cloud Run it has no ADC of its own, so mount your workstation's credential file (it must be readable by that user) and point the Google SDK at it: + +```shell +docker run --rm -p 8080:8080 \ + -e Csag.AzureSqlFederatedIdentity__TenantId="" \ + -e Csag.AzureSqlFederatedIdentity__ClientId="" \ + -e Csag.AzureSqlFederatedIdentity__Google__ServiceAccountEmail="@.iam.gserviceaccount.com" \ + -e ConnectionStrings__DefaultConnection="Server=tcp:.database.windows.net,1433;Initial Catalog=;Encrypt=True" \ + -v "$HOME/.config/gcloud/application_default_credentials.json:/adc.json:ro" \ + -e GOOGLE_APPLICATION_CREDENTIALS=/adc.json \ + csag-demo +curl http://localhost:8080/test +``` + +On Windows the credential file is `%APPDATA%\gcloud\application_default_credentials.json`. + +## Deploy to Cloud Run + +Push the image to Artifact Registry and deploy it with the runtime service account set to the configured Google service account and the four settings supplied as environment variables. Cloud Run sends traffic to port 8080, which is the port the image listens on. Section 4 of the [setup guide](../docs/cloud-identity-setup.md) has the details. From 748129753b72d802f51218d0145b60c3ce871ec1 Mon Sep 17 00:00:00 2001 From: Timothy Trowbridge Date: Thu, 17 Sep 2026 14:00:37 -0300 Subject: [PATCH 2/6] Demo: recommend the OpenID-token-only IAM role; tolerate a missing container in CI logs The library only calls generateIdToken, for which roles/iam.serviceAccountOpenIdTokenCreator bound on the service account is sufficient. Co-Authored-By: Claude Fable 5.1 --- .github/workflows/ci.yml | 2 +- Csag.AzureSqlFederatedIdentity.Demo/README.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0f9d8d0..744ca22 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -103,4 +103,4 @@ jobs: - name: Demo container logs if: failure() - run: docker logs csag-demo + run: docker logs csag-demo || true diff --git a/Csag.AzureSqlFederatedIdentity.Demo/README.md b/Csag.AzureSqlFederatedIdentity.Demo/README.md index 9fd4b0a..eeb2dfc 100644 --- a/Csag.AzureSqlFederatedIdentity.Demo/README.md +++ b/Csag.AzureSqlFederatedIdentity.Demo/README.md @@ -11,8 +11,8 @@ A minimal ASP.NET Core app that runs on Google Cloud Run and reads from Azure SQ 1. The cloud side described in [docs/cloud-identity-setup.md](../docs/cloud-identity-setup.md): a Google service account, an Azure AD app registration with a federated credential for it, and a database user for that app registration. 2. Google Application Default Credentials (ADC), which the library uses to ask Google for an ID token for the service account: - - **On Cloud Run** there is nothing to configure: set the service's runtime service account to the configured service account (it needs `roles/iam.serviceAccountTokenCreator` on itself). - - **On a workstation** run `gcloud auth application-default login`. Your Google account needs `roles/iam.serviceAccountTokenCreator` on the service account (granted on the service account, not on the project), and the IAM Service Account Credentials API must be enabled in the project (`gcloud services enable iamcredentials.googleapis.com`). + - **On Cloud Run** there is nothing to configure: set the service's runtime service account to the configured service account (it needs `roles/iam.serviceAccountOpenIdTokenCreator` on itself). + - **On a workstation** run `gcloud auth application-default login`. Your Google account needs `roles/iam.serviceAccountOpenIdTokenCreator` on the service account (granted on the service account, not on the project), and the IAM Service Account Credentials API must be enabled in the project (`gcloud services enable iamcredentials.googleapis.com`). 3. The .NET SDK pinned in [global.json](../global.json), and Docker if you want to run the container. ## Settings From da926ff9f5961a4ebf05e11469246a9eb7084495 Mon Sep 17 00:00:00 2001 From: Timothy Trowbridge Date: Fri, 18 Sep 2026 12:42:58 -0300 Subject: [PATCH 3/6] Demo: assert the container runs unprivileged; let request cancellation propagate Co-Authored-By: Claude Fable 5.1 --- .github/workflows/ci.yml | 1 + Csag.AzureSqlFederatedIdentity.Demo/Program.cs | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 744ca22..2ede7ec 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -99,6 +99,7 @@ jobs: -e Csag.AzureSqlFederatedIdentity__Google__ServiceAccountEmail=placeholder@example.invalid \ csag-demo curl --fail --silent --show-error --retry 10 --retry-connrefused --retry-all-errors --retry-delay 1 http://localhost:8080/ + test "$(docker exec csag-demo id -u)" != "0" docker stop csag-demo - name: Demo container logs diff --git a/Csag.AzureSqlFederatedIdentity.Demo/Program.cs b/Csag.AzureSqlFederatedIdentity.Demo/Program.cs index 72ef268..664766a 100644 --- a/Csag.AzureSqlFederatedIdentity.Demo/Program.cs +++ b/Csag.AzureSqlFederatedIdentity.Demo/Program.cs @@ -30,6 +30,11 @@ var rows = await context.TestTable.OrderBy(e => e.Id).ToListAsync(cancellationToken); return Results.Ok(new { count, rows, }); } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + // The client went away; there is nobody to answer and nothing worth logging. + throw; + } catch (Exception ex) { // The endpoint is unauthenticated, so token-exchange and SQL failures go to the log, not the response. From 7b89834975c5d04aef5ac3e41cfb470ee220c8f3 Mon Sep 17 00:00:00 2001 From: Timothy Trowbridge Date: Fri, 18 Sep 2026 13:30:47 -0300 Subject: [PATCH 4/6] Demo: check the connection string when the context is created, not when the factory is The factory is constructed while the endpoint's parameters are bound, so a missing connection string escaped the handler's error handling. Co-Authored-By: Claude Fable 5.1 --- .../Database/AppDbContextFactory.cs | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/Csag.AzureSqlFederatedIdentity.Demo/Database/AppDbContextFactory.cs b/Csag.AzureSqlFederatedIdentity.Demo/Database/AppDbContextFactory.cs index 29aa03f..d687cc0 100644 --- a/Csag.AzureSqlFederatedIdentity.Demo/Database/AppDbContextFactory.cs +++ b/Csag.AzureSqlFederatedIdentity.Demo/Database/AppDbContextFactory.cs @@ -7,28 +7,29 @@ public class AppDbContextFactory : IAppDbContextFactory { - private readonly DbContextOptions options; + private readonly string? connectionString; private readonly IAzureSqlTokenProvider tokenProvider; public AppDbContextFactory(IConfiguration configuration, IAzureSqlTokenProvider tokenProvider) { + this.connectionString = configuration.GetConnectionString("DefaultConnection"); this.tokenProvider = tokenProvider; + } - var connectionString = configuration.GetConnectionString("DefaultConnection"); - if (string.IsNullOrWhiteSpace(connectionString)) + public async Task CreateDbContextAsync(CancellationToken cancellationToken = default) + { + // Checked on use: the factory itself is constructed while the endpoint's parameters are bound, which is + // outside the handler's error handling. + if (string.IsNullOrWhiteSpace(this.connectionString)) { throw new InvalidOperationException("Connection string 'DefaultConnection' not set."); } - this.options = new DbContextOptionsBuilder().UseSqlServer(connectionString).Options; - } - - public async Task CreateDbContextAsync(CancellationToken cancellationToken = default) - { // The connection string carries no credentials; the federated access token authenticates the connection. var accessToken = await this.tokenProvider.GetAzureSqlAccessTokenAsync(cancellationToken); - var context = new AppDbContext(this.options); + var options = new DbContextOptionsBuilder().UseSqlServer(this.connectionString).Options; + var context = new AppDbContext(options); if (context.Database.GetDbConnection() is SqlConnection sqlConnection) { sqlConnection.AccessToken = accessToken; From 1c65d45e8c970330698e4a7a225b3c21b5dba1b8 Mon Sep 17 00:00:00 2001 From: Timothy Trowbridge Date: Fri, 18 Sep 2026 14:02:30 -0300 Subject: [PATCH 5/6] Demo README: run the local container as the host user so the mounted ADC file is readable Co-Authored-By: Claude Fable 5.1 --- Csag.AzureSqlFederatedIdentity.Demo/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Csag.AzureSqlFederatedIdentity.Demo/README.md b/Csag.AzureSqlFederatedIdentity.Demo/README.md index eeb2dfc..89cab1f 100644 --- a/Csag.AzureSqlFederatedIdentity.Demo/README.md +++ b/Csag.AzureSqlFederatedIdentity.Demo/README.md @@ -66,10 +66,10 @@ The image is built from the **repository root**, because the Demo references the docker build -f Csag.AzureSqlFederatedIdentity.Demo/Dockerfile -t csag-demo . ``` -The container listens on port 8080 and runs as the non-root `app` user (uid 1654). Outside Cloud Run it has no ADC of its own, so mount your workstation's credential file (it must be readable by that user) and point the Google SDK at it: +The container listens on port 8080 and runs as the non-root `app` user (uid 1654). Outside Cloud Run it has no ADC of its own, so mount your workstation's credential file and point the Google SDK at it. `gcloud` creates that file readable by your user only, so for this local test run the container as your own user (`--user`), which overrides the image's `app` user; on Windows the mounted file is readable regardless and `--user` can be left out. Cloud Run needs none of this: ```shell -docker run --rm -p 8080:8080 \ +docker run --rm -p 8080:8080 --user "$(id -u):$(id -g)" \ -e Csag.AzureSqlFederatedIdentity__TenantId="" \ -e Csag.AzureSqlFederatedIdentity__ClientId="" \ -e Csag.AzureSqlFederatedIdentity__Google__ServiceAccountEmail="@.iam.gserviceaccount.com" \ From a2058321b56a54bff7b5ceb5748c2660433d326c Mon Sep 17 00:00:00 2001 From: Timothy Trowbridge Date: Fri, 18 Sep 2026 14:27:06 -0300 Subject: [PATCH 6/6] Demo README: the run commands stay in the foreground; request the endpoint from a second terminal Co-Authored-By: Claude Fable 5.1 --- Csag.AzureSqlFederatedIdentity.Demo/README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Csag.AzureSqlFederatedIdentity.Demo/README.md b/Csag.AzureSqlFederatedIdentity.Demo/README.md index 89cab1f..ec79246 100644 --- a/Csag.AzureSqlFederatedIdentity.Demo/README.md +++ b/Csag.AzureSqlFederatedIdentity.Demo/README.md @@ -55,6 +55,11 @@ INSERT INTO dbo.TestTable (Value) VALUES (N'hello'), (N'world'); ```shell dotnet run --project Csag.AzureSqlFederatedIdentity.Demo +``` + +The application stays in the foreground; request the endpoint from a second terminal: + +```shell curl http://localhost:5172/test ``` @@ -77,6 +82,11 @@ docker run --rm -p 8080:8080 --user "$(id -u):$(id -g)" \ -v "$HOME/.config/gcloud/application_default_credentials.json:/adc.json:ro" \ -e GOOGLE_APPLICATION_CREDENTIALS=/adc.json \ csag-demo +``` + +The container stays in the foreground as well; from a second terminal: + +```shell curl http://localhost:8080/test ```