diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index d72a2e1..f8e28ef 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,6 +1,6 @@ -# Update the NODE_VERSION arg in docker-compose.yml to pick a Node version: 18, 16, 14 -ARG NODE_VERSION=16 -FROM mcr.microsoft.com/vscode/devcontainers/javascript-node:0-${NODE_VERSION} +# Update the NODE_VERSION arg in docker-compose.yml to pick a Node version: 22, 20, 18 +ARG NODE_VERSION=20 +FROM mcr.microsoft.com/vscode/devcontainers/javascript-node:1-${NODE_VERSION}-bookworm # VARIANT can be either 'hugo' for the standard version or 'hugo_extended' for the extended version. ARG VARIANT=hugo diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 7a4c703..1b142f7 100755 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -13,8 +13,8 @@ // Example versions: latest, 0.73.0, 0,71.1 // Rebuild the container if it already exists to update. "VERSION": "latest", - // Update NODE_VERSION to pick the Node.js version: 12, 14 - "NODE_VERSION": "14" + // Update NODE_VERSION to pick the Node.js version: 22, 20, 18 + "NODE_VERSION": "20" } }, diff --git a/blog/content/post/2018/first-user-group-presentation-i-survived/index.md b/blog/content/post/2018/first-user-group-presentation-i-survived/index.md index 5c4a3cd..6f8b9de 100644 --- a/blog/content/post/2018/first-user-group-presentation-i-survived/index.md +++ b/blog/content/post/2018/first-user-group-presentation-i-survived/index.md @@ -15,7 +15,7 @@ Well tonight marks three weeks since I gave my first user group presentation and TL;DR I didn’t die, the SQL Server community is fantastic and I have amazing supportive friends. -{{< tweet user="Pittfurg" id="1004125722082934784" >}} +{{< x user="Pittfurg" id="1004125722082934784" >}} ## Why Present? diff --git a/blog/content/post/2018/t-sql-tuesday-108/index.md b/blog/content/post/2018/t-sql-tuesday-108/index.md index 8fe578d..ba16602 100644 --- a/blog/content/post/2018/t-sql-tuesday-108/index.md +++ b/blog/content/post/2018/t-sql-tuesday-108/index.md @@ -33,7 +33,7 @@ The side goal is docker/containers/kubernetes (maybe), I’m wondering if I coul I saw the tweet below last week from the beard, [Rob Sewell](https://twitter.com/sqldbawithbeard), that quoted [Bob Ward’s](https://twitter.com/bobwardms) thoughts on learning directions.  Feels like this is probably solid advice to justify my side goal. -{{< tweet user="sqldbawithbeard" id="1061032613979267072" >}} +{{< x user="sqldbawithbeard" id="1061032613979267072" >}} ## Learning Plan diff --git a/blog/content/post/2019/multiple-triggers/index.md b/blog/content/post/2019/multiple-triggers/index.md index ae59583..ddd8816 100644 --- a/blog/content/post/2019/multiple-triggers/index.md +++ b/blog/content/post/2019/multiple-triggers/index.md @@ -13,7 +13,7 @@ Well it has been a little quiet here recently. I just (or it’s been two weeks This is also going to be a quick post. I asked a question on Twitter last week about what happens when you have multiple triggers on a table. I got the answer (Thanks Aaron!), but figured this would be a good thing to demonstrate. -{{< tweet user="AaronBertrand" id="1121436026956861445" >}} +{{< x user="AaronBertrand" id="1121436026956861445" >}} I have also been playing with Azure Data Studio and the new notebook feature, so I answered this question with a step-by-step example in a notebook. I also found that you can easily store these notebooks on GitHub so I have uploaded it to my demos repo for you to follow along. diff --git a/blog/content/post/2020/t-sql-tuesday-127/index.md b/blog/content/post/2020/t-sql-tuesday-127/index.md index e540d33..ca7d7d0 100644 --- a/blog/content/post/2020/t-sql-tuesday-127/index.md +++ b/blog/content/post/2020/t-sql-tuesday-127/index.md @@ -32,7 +32,7 @@ Whether it’s chrome or SSMS, I cannot help myself when it comes to opening new I even got called out by my good friend Andrew ([B](https://awickham.com/)|[T](https://twitter.com/awickham)) this last week: -{{< tweet user="awickham" id="1267503576571674624" >}} +{{< x user="awickham" id="1267503576571674624" >}} My tips & tricks are focused around managing tabs, and they work in all browsers (at least all that I have on my laptop. Chrome, Edge, IE11). diff --git a/blog/content/post/2025/dab-api-container/index.md b/blog/content/post/2025/dab-api-container/index.md index 9ef809e..1d83eab 100644 --- a/blog/content/post/2025/dab-api-container/index.md +++ b/blog/content/post/2025/dab-api-container/index.md @@ -1,7 +1,7 @@ --- -title: "Running dab in an Azure Container Instance" +title: "Running DAB in an Azure Container Instance" slug: "dab-api-container" -description: "In part 2 of my series on dab, we'll run dab in an Azure Container Instance and wire it up to create an API on top of an Azure SQL Database." +description: "In part 2 of my series on DAB, we'll run DAB in an Azure Container Instance and wire it up to create an API on top of an Azure SQL Database." date: 2025-11-06T11:00:00Z categories: - dab @@ -15,7 +15,7 @@ image: header.png draft: false --- -This is post two in my series about the Data API Builder (dab), the first post, [Data API Builder](/dab-api-builder/), covers what dab is and how to test it locally against SQL Server in running in a container. This was great for testing, but now we want to start to productionise this, and the first step is to get it running somewhere other than my laptop. +This is post two in my series about the Data API Builder (DAB), the first post, [Data API Builder](/dab-api-builder/), covers what DAB is and how to test it locally against SQL Server in running in a container. This was great for testing, but now we want to start to productionise this, and the first step is to get it running somewhere other than my laptop. There are several deployment options available, I recommend you review the Microsoft docs here: [Deployment guidance for Data API builder](https://learn.microsoft.com/en-us/azure/data-api-builder/deployment/). diff --git a/blog/content/post/2025/dbatools-backup-replacename/index.md b/blog/content/post/2025/dbatools-backup-replacename/index.md index e4500e2..1571a50 100644 --- a/blog/content/post/2025/dbatools-backup-replacename/index.md +++ b/blog/content/post/2025/dbatools-backup-replacename/index.md @@ -1,94 +1,94 @@ ---- -title: "dbatools Backup-DbaDatabase -ReplaceInName" -slug: "dbatools-backup-replacename" -description: "Recently I was reading the docs for `Backup-DbaDatabase` and found a parameter I didn't realise existed, but is so useful when you want to automate backups, but keep control of the file names." -date: 2025-04-18T12:00:00Z -categories: - - dbatools - - backups -tags: - - dbatools - - backups -image: guillaume-auceps-vffNjorpNrg-unsplash.jpg -draft: false ---- - -I use [dbatools](https://dbatools.io) all the time, I'm talking literally every workday, but how often do I read the docs? Probably not often enough! - -## Get-Help - -Whenever I'm teaching PowerShell and especially dbatools, I talk about `Get-Help` because without leaving your console, you can review the full documentation of the commands. Now, this documentation has to have been added by the author of the command, but we guarantee that for every dbatools command with built in Pester tests! - -## Backup step for code release - -So, not too long ago, I was writing a script that would be part of a release pipeline. The client wanted to do a quick `COPY_ONLY` backup of all the databases on a few separate instances, before they deployed new application code, which could be making schema changes. Basically, creating an easy rollback plan if the deployment went bad. - -Not a problem with `Backup-DbaDatabase`, I already know we can pass in multiple instances, and specify `-CopyOnly` to not interfere with the LSN chain of the regular backups. - -The difference was, there was already a specified naming convention for the backups, and we wanted to match that naming with our new automated script. - -If I start with the following script, it will perform a `COPY_ONLY` backup of all databases on the `mssql1` instance, to the specified folder `/shared/release/v7`, which in my case I was passing the version folder name through from my Azure DevOps pipeline. - -```PowerShell -$backupParams = @{ - SqlInstance = "mssql1" - Path = "/shared/release/v7" - CopyOnly = $true -} -Backup-DbaDatabase @backupParams -``` - -You can see we got the files we needed, but using the standard dbatools naming convention of `databaseName_timestamp.bak`. - -{{< - figure src="backupFiles.png" - alt="ls of backup directory showing backup files" ->}} - -For most situations, this is fine and I leave it at that, but how would I manage to change the location to be `/shared/release/v7/mssql1/MSSQLSERVER/pubs.bak`. So the server and instance names are folders, and then the file is just named after the database name? - -You could do this in PowerShell, get the list of databases, loop through them setting the full name of the backup. But instead, if we check the documentation for `Backup-DbaDatabase` we can see this functionality is already built in with the `-ReplaceInName` parameter. - -## -ReplaceInName Parameter - -By checking the help we can see which values can be replaced on the fly. - -```PowerShell -Get-Help Backup-DbaDatabase -``` - -This is straight from the docs, and if you want to read it in the online version you can head to [docs.dbatools.io](https://docs.dbatools.io/Backup-DbaDatabase.html). - -```text --ReplaceInName [] - If this switch is set, the following list of strings will be replaced in the FilePath and Path strings: - instancename - will be replaced with the instance Name - servername - will be replaced with the server name - dbname - will be replaced with the database name - timestamp - will be replaced with the timestamp (either the default, or the format provided) - backuptype - will be replaced with Full, Log or Differential as appropriate -``` - -So, with this information, we can change the script to look like this. Using the keywords `servername`, `instancename` and `dbname` in the parameter values, and including the `ReplaceInName` switch. - -```PowerShell -$backupParams = @{ - SqlInstance = "mssql1" - Path = "/shared/release/v7/servername/instancename" - FilePath = "dbname.bak" - ReplaceInName = $true - CopyOnly = $true -} -Backup-DbaDatabase @backupParams -``` - -The results look like this, which is exactly what we needed. Now obviously, depending on what you want the file names to be, these keywords might not be enough, but they give you a decent amount of flexibility to customise the output. - -{{< - figure src="backupFilesNames.png" - alt="ls of backup folder after using -ReplaceInName param" ->}} - -Hope you find this one useful, and it's a good reminder for all of us to read the docs! There is so much functionality in dbatools, none of us know everything this module is capable of! - -Header image by [Guillaume Auceps](https://unsplash.com/@gauceps?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash) on [Unsplash](https://unsplash.com/photos/a-row-of-boats-floating-on-top-of-a-body-of-water-vffNjorpNrg?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash). +--- +title: "dbatools Backup-DbaDatabase -ReplaceInName" +slug: "dbatools-backup-replacename" +description: "Recently I was reading the docs for `Backup-DbaDatabase` and found a parameter I didn't realise existed, but is so useful when you want to automate backups, but keep control of the file names." +date: 2025-04-18T12:00:00Z +categories: + - dbatools + - backups +tags: + - dbatools + - backups +image: guillaume-auceps-vffNjorpNrg-unsplash.jpg +draft: false +--- + +I use [dbatools](https://dbatools.io) all the time, I'm talking literally every workday, but how often do I read the docs? Probably not often enough! + +## Get-Help + +Whenever I'm teaching PowerShell and especially dbatools, I talk about `Get-Help` because without leaving your console, you can review the full documentation of the commands. Now, this documentation has to have been added by the author of the command, but we guarantee that for every dbatools command with built in Pester tests! + +## Backup step for code release + +So, not too long ago, I was writing a script that would be part of a release pipeline. The client wanted to do a quick `COPY_ONLY` backup of all the databases on a few separate instances, before they deployed new application code, which could be making schema changes. Basically, creating an easy rollback plan if the deployment went bad. + +Not a problem with `Backup-DbaDatabase`, I already know we can pass in multiple instances, and specify `-CopyOnly` to not interfere with the LSN chain of the regular backups. + +The difference was, there was already a specified naming convention for the backups, and we wanted to match that naming with our new automated script. + +If I start with the following script, it will perform a `COPY_ONLY` backup of all databases on the `mssql1` instance, to the specified folder `/shared/release/v7`, which in my case I was passing the version folder name through from my Azure DevOps pipeline. + +```PowerShell +$backupParams = @{ + SqlInstance = "mssql1" + Path = "/shared/release/v7" + CopyOnly = $true +} +Backup-DbaDatabase @backupParams +``` + +You can see we got the files we needed, but using the standard dbatools naming convention of `databaseName_timestamp.bak`. + +{{< + figure src="backupFiles.png" + alt="ls of backup directory showing backup files" +>}} + +For most situations, this is fine and I leave it at that, but how would I manage to change the location to be `/shared/release/v7/mssql1/MSSQLSERVER/pubs.bak`. So the server and instance names are folders, and then the file is just named after the database name? + +You could do this in PowerShell, get the list of databases, loop through them setting the full name of the backup. But instead, if we check the documentation for `Backup-DbaDatabase` we can see this functionality is already built in with the `-ReplaceInName` parameter. + +## -ReplaceInName Parameter + +By checking the help we can see which values can be replaced on the fly. + +```PowerShell +Get-Help Backup-DbaDatabase +``` + +This is straight from the docs, and if you want to read it in the online version you can head to [docs.dbatools.io](https://docs.dbatools.io/Backup-DbaDatabase.html). + +```text +-ReplaceInName [] + If this switch is set, the following list of strings will be replaced in the FilePath and Path strings: + instancename - will be replaced with the instance Name + servername - will be replaced with the server name + dbname - will be replaced with the database name + timestamp - will be replaced with the timestamp (either the default, or the format provided) + backuptype - will be replaced with Full, Log or Differential as appropriate +``` + +So, with this information, we can change the script to look like this. Using the keywords `servername`, `instancename` and `dbname` in the parameter values, and including the `ReplaceInName` switch. + +```PowerShell +$backupParams = @{ + SqlInstance = "mssql1" + Path = "/shared/release/v7/servername/instancename" + FilePath = "dbname.bak" + ReplaceInName = $true + CopyOnly = $true +} +Backup-DbaDatabase @backupParams +``` + +The results look like this, which is exactly what we needed. Now obviously, depending on what you want the file names to be, these keywords might not be enough, but they give you a decent amount of flexibility to customise the output. + +{{< + figure src="backupFilesNames.png" + alt="ls of backup folder after using -ReplaceInName param" +>}} + +Hope you find this one useful, and it's a good reminder for all of us to read the docs! There is so much functionality in dbatools, none of us know everything this module is capable of! + +Header image by [Guillaume Auceps](https://unsplash.com/@gauceps?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash) on [Unsplash](https://unsplash.com/photos/a-row-of-boats-floating-on-top-of-a-body-of-water-vffNjorpNrg?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash). diff --git a/blog/content/post/2026/dab-api-add-auth/DABAccessRole.png b/blog/content/post/2026/dab-api-add-auth/DABAccessRole.png new file mode 100644 index 0000000..12d4276 Binary files /dev/null and b/blog/content/post/2026/dab-api-add-auth/DABAccessRole.png differ diff --git a/blog/content/post/2026/dab-api-add-auth/header.png b/blog/content/post/2026/dab-api-add-auth/header.png new file mode 100644 index 0000000..55f30d8 Binary files /dev/null and b/blog/content/post/2026/dab-api-add-auth/header.png differ diff --git a/blog/content/post/2026/dab-api-add-auth/index.md b/blog/content/post/2026/dab-api-add-auth/index.md new file mode 100644 index 0000000..cc95f92 --- /dev/null +++ b/blog/content/post/2026/dab-api-add-auth/index.md @@ -0,0 +1,257 @@ +--- +title: "DAB API - Authenticated API Endpoints" +slug: "dab-api-auth" +description: "This post covers configuring our DAB API to only accept authenticated requests for data. It is part of a series of posts on using DAB." +date: 2026-04-08T10:00:00Z +categories: + - DAB + - api + - PowerShell +tags: + - DAB + - api + - PowerShell +image: header.png +draft: true +--- + +This is post three in my series about the Data API Builder (DAB), the first post, [Data API Builder](/dab-api-builder/), covers what DAB is and how to test it locally against SQL Server in running in a container. The second post, [Running DAB in an Azure Container Instance](/dab-api-container/), starts to productionise this, moving it into the cloud, but with no auth required to hit the endpoints. + +In the next three posts we'll cover Authentication, which is slightly complex, hence the three posts: + +- DAB API - Add Authentication <-- You are here +- DAB API - User Authentication from Azure CLI +- DAB API - Azure Function Authentication + +If you're looking to follow along you need to have the infra we built in the previous post, [Running DAB in an Azure Container Instance](/dab-api-container): + +- an Azure SQL Database which is the source +- a Storage Account hosting the `dab-config.json` file +- an Azure Container App running DAB + +At this point I can call the DAB API endpoint without authentication and get my data. + +```PowerShell +$data = Invoke-RestMethod -Uri 'http://ci-dab-prod-001.uksouth.azurecontainer.io:5000/api/dbo_BuildVersion' +$data.value +``` + +## The Goal + +My end goal for this series is to be able to insert data into my Azure SQL Database from PowerShell code that's running in an Azure Function. The first step of this is to protect my data, by requiring authentication. + +Let's get straight into it. + +## Entra App Registration + +First step here is to create an Entra App Registration. This will define what a valid token is. So when an authenticated user or service requests a token, Entra issues one with an audience and issuer that matches what the DAB API container is configured to expect. + +Let's create that App Registration using the Azure CLI. We'll set the identifier to the App Registration ID. This will become the audience on our token, and will need to match what our DAB config file has set, we'll get to that shortly. + + ```Powershell + # Create the App Registration and capture the app ID + $app_id = (az ad app create --display-name "DAB-API-Access" --sign-in-audience "AzureADMyOrg" | ConvertFrom-Json).appId + + # Set the identifier URI using the app ID + az ad app update --id $app_id --identifier-uris "api://$app_id" + + # Create a service principal #TODO: still accurate? + az ad sp create --id $app_id + ``` + +We also need to create an app role which allows us to control authorization, or what authenticated users can do. You can create multiple app roles to separate endpoints, or actions - for today we're going to create one role, `DAB.Access` using the Microsoft Graph API. + + ```powershell + $existingApp = az ad app show --id $app_id | ConvertFrom-Json + + $roleId = [guid]::NewGuid().ToString() + $newRole = @{ + allowedMemberTypes = @("Application") + description = "Allow access to DAB API" + displayName = "DAB.Access" + id = $roleId + isEnabled = $true + value = "DAB.Access" + } + $existingApp.appRoles += $newRole + + # Compress avoids newlines; escape double quotes for the shell + $bodyJson = (@{ appRoles = $existingApp.appRoles } | ConvertTo-Json -Depth 10 -Compress) -replace '"', '\"' + + az rest --method PATCH ` + --uri "https://graph.microsoft.com/v1.0/applications/$($existingApp.id)" ` + --headers "Content-Type=application/json" ` + --body "$bodyJson" + ``` + +You can see this in portal under App roles for your App Registration. + +![DAB.Access Role shown in Entra](DABAccessRole.png) + +## DAB Config File + +We also need to update the `dab-config.json` file to configure it for Entra Authentication, the audience and issuer will need to match the token from the App Registration we created in the previous step. + + ```PowerShell + $tenantId = ($(az account show) | ConvertFrom-Json).tenantId + + dab configure --runtime.host.authentication.provider EntraID + dab configure --runtime.host.authentication.jwt.audience "api://$app_id" + dab configure --runtime.host.authentication.jwt.issuer "https://sts.windows.net/$tenantId" + ``` + +You'll end up with a section of your `dab-config.json` file that looks like this, the issuer will contain your tenant id, and the audience of the token contains the app id of the Entra App Registration. + + ```json + "authentication": { + "provider": "EntraID", + "jwt": { + "audience": "api://***-app-id-****", + "issuer": "https://sts.windows.net/****-tenant-id****" + } + }, + ``` + +We also need to update our entities, which create the API endpoints, from anonymous access to only allow authenticated users. + +> NOTE! +> There is a `dab update` command with a `--permissions` parameter that you can use to update the entities in the config. However, I found this appends the permissions and ended with me allowing authenticated or anonymous access to my endpoints - not ideal. + +Since PowerShell is good at manipulating json we can read the config file in and for each entity, change the permission role to `Authenticated`. I'm also adding the `create` action which will allow us to `POST` data, which will insert it into our SQL Database. + + ```powershell + # Load the DAB config file + $configPath = "C:\Temp\dab\dab-config.json" + $config = Get-Content -Path $configPath -Raw | ConvertFrom-Json + + # Loop through all entities and update permissions + foreach ($entityName in $config.entities.PSObject.Properties.Name) { + $entity = $config.entities.$entityName + foreach ($permission in $entity.permissions) { + if ($permission.role -eq "anonymous") { + $permission.role = "Authenticated" + } + + # Add create action to Authenticated role if not already present + if ($permission.role -eq "Authenticated") { + $hasCreate = $permission.actions | Where-Object { $_.action -eq "create" } + if (-not $hasCreate) { + $permission.actions += @{ action = "create" } + } + } + } + } + + # Convert back to JSON and save + $config | ConvertTo-Json -Depth 10 | Set-Content -Path $configPath + ``` + +If you view the `dab-config.json` file now your entities should each look like this with both read and create permissions, and the role set to `Authenticated`. + + ```json + "dbo_BuildVersion": { + "source": { + "object": "dbo.BuildVersion", + "type": "table" + }, + "graphql": { + "enabled": true, + "type": { + "singular": "dbo_BuildVersion", + "plural": "dbo_BuildVersions" + } + }, + "rest": { + "enabled": true + }, + "permissions": [ + { + "role": "Authenticated", + "actions": [ + { + "action": "read" + }, + { + "action": "create" + } + ] + } + ] + }, + ``` + +Upload the updated `dab-config.json` file to the Azure Storage account for our container instance to use. + + ```PowerShell + $storageKey = az storage account keys list ` + --resource-group $resourceGroup ` + --account-name $storageAccount ` + --query "[0].value" ` + --output tsv + + az storage file upload --account-key $storageKey ` + --account-name $storageAccount ` + --share-name $fileShareName ` + --source dab-config.json + ``` + +Once that is there, restart the container app, so that it picks up the latest configuration. + + ```PowerShell + az container restart ` + --resource-group $resourceGroup ` + --name $containerName + ``` + +## Test + +Now if I try and get data from the same endpoint that worked earlier, we should get a 403 unauthorized response code. + +```PowerShell +Invoke-RestMethod -Uri 'http://ci-dab-prod-001.uksouth.azurecontainer.io:5000/api/dbo_BuildVersion' +``` + +Something like this: + +```text +Invoke-RestMethod: +{ + "error": { + "code": "AuthorizationCheckFailed", + "message": "Authorization Failure: Access Not Allowed.", + "status": 403 + } +} +``` + +![PowerShell Console showing the above error code - 403 Authorization Failure](unauthorized.png) + +## Up Next + +Great - now our API endpoints, and our data is secure we can move onto how to send authenticated user and service requests. In the next post we'll get to the point where we can call these endpoints from Azure CLI again, this time with a token, as an authenticated user. + +## Tidy Up + +If you've been following along you can tidy up and remove all the resources. First, delete the Azure resource group: + +```PowerShell +# Delete all Azure resources (SQL DB, Storage, Container, Function App) +az group delete --name $resourceGroup --yes --no-wait +``` + +Then clean up the Entra ID resources (these are not deleted with the resource group): + +```PowerShell +# Delete the app registration (this also removes app roles and assignments) +az ad app delete --id $app_id +``` + +## DAB Blog Series + +Here are all the links to the DAB blog series: + +1. [Data API Builder](/dab-api-builder/) +2. [Running DAB in an Azure Container Instance](/dab-api-container/) +3. More coming soon... + +Or you can view all posts about DAB using the [DAB](/categories/dab/) category. diff --git a/blog/content/post/2026/dab-api-add-auth/unauthorized.png b/blog/content/post/2026/dab-api-add-auth/unauthorized.png new file mode 100644 index 0000000..5633778 Binary files /dev/null and b/blog/content/post/2026/dab-api-add-auth/unauthorized.png differ diff --git a/blog/content/post/2026/dab-api-azure-func-auth/image-2.png b/blog/content/post/2026/dab-api-azure-func-auth/image-2.png deleted file mode 100644 index cea624a..0000000 Binary files a/blog/content/post/2026/dab-api-azure-func-auth/image-2.png and /dev/null differ diff --git a/blog/content/post/2026/dab-api-azure-func-auth/image.png b/blog/content/post/2026/dab-api-azure-func-auth/image.png deleted file mode 100644 index ccc6ece..0000000 Binary files a/blog/content/post/2026/dab-api-azure-func-auth/image.png and /dev/null differ diff --git a/blog/content/post/2026/dab-api-azure-func-auth/TokenPermissions.png b/blog/content/post/2026/dab-api-func-auth/TokenPermissions.png similarity index 100% rename from blog/content/post/2026/dab-api-azure-func-auth/TokenPermissions.png rename to blog/content/post/2026/dab-api-func-auth/TokenPermissions.png diff --git a/blog/content/post/2026/dab-api-azure-func-auth/image-4.png b/blog/content/post/2026/dab-api-func-auth/image-4.png similarity index 100% rename from blog/content/post/2026/dab-api-azure-func-auth/image-4.png rename to blog/content/post/2026/dab-api-func-auth/image-4.png diff --git a/blog/content/post/2026/dab-api-azure-func-auth/index.md b/blog/content/post/2026/dab-api-func-auth/index.md similarity index 55% rename from blog/content/post/2026/dab-api-azure-func-auth/index.md rename to blog/content/post/2026/dab-api-func-auth/index.md index 8425933..a350a5a 100644 --- a/blog/content/post/2026/dab-api-azure-func-auth/index.md +++ b/blog/content/post/2026/dab-api-func-auth/index.md @@ -1,178 +1,20 @@ --- -title: "Dab Api Azure Func Auth" -slug: "dab-api-azure-func-auth" +title: "DAB API - Azure Function Authentication" +slug: "dab-api-func-auth" description: "ENTER YOUR DESCRIPTION" date: 2025-08-21T09:14:40Z categories: + - DAB + - api + - PowerShell tags: -image: + - DAB + - api + - PowerShell +image: header.png draft: true --- -This is post three in my series about the Data API Builder (dab), the first post, [Data API Builder](/dab-api-builder/), covers what dab is and how to test it locally against SQL Server in running in a container. The second post, [Running dab in an Azure Container Instance](/dab-api-container/), starts to productionise this, moving it into the cloud, but with no auth required to hit the endpoints. - -In this post we'll look to fix this, specifically by enabling other Azure services to use these endpoints with authentication. If you're looking to follow along you need to have the infra we built in the previous post: - -- an Azure SQL Database which is the source -- a Storage Account hosting the `dab-config.json` file -- an Azure Container App running dab - -My end goal is to be able to insert data into my Azure SQL Database from PowerShell code that's running in an Azure Function. Let's get straight into it. - -## Entra App Registration - -If the goal is to create an Azure Function (or another Azure service) that can access the data in the SQL Database via the API endpoints we need to give the Function App a way of authenticating with these endpoints. We'll do this with an App Registation in Entra. - -Let's create that app registration. - - ```powershell - # Create the App Registration - az ad app create --display-name "DAB-API-Access" --sign-in-audience "AzureADMyOrg" - - # Get the App ID (Client ID) - you'll need this - $app_id = $(az ad app list --display-name "DAB-API-Access" --query "[0].appId" -o tsv) - - # Add the default user_impersonation scope (this is often sufficient) - az ad app update --id $app_id --identifier-uris "api://$APP_ID" - #that last line didn't work but added in the portal - - # Create a service principal for your app registration - az ad sp create --id $app_id - ``` - -need a service prinipal so we can grant admin consent on the API permissions page - -## dab Config File - -Update the dab config to azure auth and cors - - ```PowerShell - # Set the authentication provider - dab configure --runtime.host.authentication.provider EntraID - - # Set the expected audience (Application ID URI) - dab configure --runtime.host.authentication.jwt.audience "api://$APP_ID" - - # Set the expected issuer (your tenant) - $tenantId = ($(az account show) | ConvertFrom-Json).id - dab configure --runtime.host.authentication.jwt.issuer "https://login.microsoftonline.com/$tenantId$/v2.0" - ``` - - ``` - # Update DAB config with the correct values from the token -dab configure --runtime.host.authentication.provider EntraID -dab configure --runtime.host.authentication.jwt.audience "api://$APP_ID" -dab configure --runtime.host.authentication.jwt.issuer "https://sts.windows.net/f98042ad-9bbc-499d-adb4-17193696b9a3/" -``` - -this is left over json does it match? - - ```json - "host": { - "cors": { - "origins": ["https://azqr-func-jp-dev-e5gxevfjgbhfhmdk.westeurope-01.azurewebsites.net"], - "allow-credentials": true - }, - "authentication": { - "provider": "AzureAD", - "jwt": { - "audience": "ffa9ce65-fe37-4958-849c-8747e106577d", - "issuer": "https://sts.windows.net/8f5c8fb3-b610-4233-8284-63a7254f4029/" - } - }, - "mode": "development" - } - ``` - -We also need to update our entities from anonymous access to only allow authenticated users - - ```powershell - $adminUser ="databaseadmin" - $securePassword = ConvertTo-SecureString "dbatools.IO!" -AsPlainText -Force - $cred = [pscredential]::new($adminUser, $securePassword) - - $server = 'sqlsvr-dab-prod-001' - $database = 'sqldb-dab-prod-001' - - $conn = Connect-DbaInstance -SqlInstance ('{0}.database.windows.net' -f $server) -SqlCredential $cred - $conn.databases[$database].Tables.ForEach{ - dab update ('{0}_{1}' -f $psitem.schema, $psitem.Name) --permissions "Authenticated:read" - } - ``` - -WARNING -dab update - adds to the entitiy... : -This will change the entities to require auth? does it? still has the anonomous in there too? - -So let's manipulate the json with powershell - - ```powershell - # Load the DAB config file - $configPath = "C:\Temp\dab\dab-config.json" - $config = Get-Content -Path $configPath -Raw | ConvertFrom-Json - - # Loop through all entities and update permissions - foreach ($entityName in $config.entities.PSObject.Properties.Name) { - $entity = $config.entities.$entityName - foreach ($permission in $entity.permissions) { - if ($permission.role -eq "anonymous") { - $permission.role = "Authenticated" - } - } - - # Add create action to Authenticated role for POST operations - $authPerm = $entity.permissions | Where-Object { $_.role -eq "Authenticated" } - if ($authPerm) { - $hasCreate = $authPerm.actions | Where-Object { $_.action -eq "create" } - if (-not $hasCreate) { - $authPerm.actions += @{ action = "create" } - } - } - } - - # Convert back to JSON and save - $config | ConvertTo-Json -Depth 10 | Set-Content -Path $configPath - ``` - -Should look like this with both read and create permissions: - - ```json - "dbo_BuildVersion": { - "source": { - "object": "dbo.BuildVersion", - "type": "table" - }, - "graphql": { - "enabled": true, - "type": { - "singular": "dbo_BuildVersion", - "plural": "dbo_BuildVersions" - } - }, - "rest": { - "enabled": true - }, - "permissions": [ - { - "role": "Authenticated", - "actions": [ - { - "action": "read" - }, - { - "action": "create" - } - ] - } - ] - }, - ``` - -TODO why mode dev? gives us swagger and other dev tools - -any authenticated user can access - there is a way of doing roles like `DAB.Read` to make it more granular - -this changes the entity from anon access to authenticated 1. create function @@ -290,10 +132,10 @@ Step 1: Add an App Role to the DAB API App Registration $newRole = @{ allowedMemberTypes = @("Application") description = "Allow access to DAB API" - displayName = "DAB.Read" + displayName = "DAB.Access" id = $roleId isEnabled = $true - value = "DAB.Read" + value = "DAB.Access" } $existingApp.appRoles += $newRole @@ -312,7 +154,7 @@ Step 1: Add an App Role to the DAB API App Registration ## add api permissions -for the DAB-API-Access - DAB.Read role +for the DAB-API-Access - DAB.Access role TODO: do I need to grant admin consent? NO Why Not? @@ -332,8 +174,8 @@ $functionSPId = az ad sp show --id $clientId --query "id" -o tsv # Step 1: Get the service principal for the API app registration $apiServicePrincipalId = az ad sp list --filter "appId eq '$app_id'" --query "[0].id" -o tsv -# Step 2: Get the App Role ID we just created (DAB.Read) -$appRoleId = az ad sp show --id $apiServicePrincipalId --query "appRoles[?value=='DAB.Read'].id" -o tsv +# Step 2: Get the App Role ID we just created (DAB.Access) +$appRoleId = az ad sp show --id $apiServicePrincipalId --query "appRoles[?value=='DAB.Access'].id" -o tsv # Step 3: Get the service principal ID of the function app's managed identity $functionSPId = az ad sp show --id $clientId --query "id" -o tsv @@ -374,6 +216,7 @@ output from Step 6: Assign the app role using the file "resourceId": "56844302-ec84-45c1-8ac1-3f02a58970e9" } ``` + ![alt text](image-3.png) @@ -392,32 +235,34 @@ in entra ## upload new dab config +TODO: add $storageAccount definition to the top? - ```PowerShell - $storageKey = az storage account keys list ` - --resource-group $resourceGroup ` - --account-name $storageAccount ` - --query "[0].value" ` - --output tsv - - az storage file upload --account-key $storageKey ` - --account-name $storageAccount ` - --share-name $fileShareName ` - --source dab-config.json - ``` + ```PowerShell + $storageKey = az storage account keys list ` + --resource-group $resourceGroup ` + --account-name $storageAccount ` + --query "[0].value" ` + --output tsv + az storage file upload --account-key $storageKey ` + --account-name $storageAccount ` + --share-name $fileShareName ` + --source dab-config.json + ``` ## restart container -``` +Restart the dab container app so that it picks up our new config file. + +```PowerShell az container restart ` --resource-group $resourceGroup ` --name $containerName ``` +## testing - -## testinng +At this point if The function app has two functions- the testHttp function is the standard test template from VSCode when you create a HTTP function. First grab your function key: @@ -478,6 +323,7 @@ $newCustomer = @{ PasswordHash = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqr=" PasswordSalt = "TestSalt" ModifiedDate = "2026-04-03T00:00:00" + rowguid = [guid]::NewGuid().ToString() } Invoke-RestMethod -Uri ('https://func-dab-prod-001.azurewebsites.net/api/dabHttp?code={0}&schema={1}&entity={2}' -f $funcKey, $schema, $entity) ` @@ -517,6 +363,31 @@ Invoke-RestMethod: } ``` +```powershell +Invoke-RestMethod -Uri ('https://func-dab-prod-001.azurewebsites.net/api/dabHttp?code={0}&schema={1}&entity={2}' -f $funcKey, $schema, $entity) ` +> -Method Post ` +> -Body ($newCustomer | ConvertTo-Json) ` +> -ContentType 'application/json' +Invoke-RestMethod: +{ + "error": "DAB API call failed: Response status code does not indicate success: 400 (Bad Request). | DAB Response: \r\n{\r\n \u0022error\u0022: {\r\n \u0022code\u0022: \u0022BadRequest\u0022,\r\n \u0022message\u0022: \u0022Invalid request body. Missing field in body: rowguid.\u0022,\r\n \u0022status\u0022: 400\r\n }\r\n}", + "troubleshooting": [], + "configuration": { + "dabEndpoint": "http://ci-dab-prod-001.uksouth.azurecontainer.io:5000", + "clientId": "04392f6f-3770-4fb3-ac5d-5940b014e7c1" + }, + "nextSteps": [], + "environment": { + "isAzure": true, + "websiteInstanceId": "ffed6eb0ac2ed3b4d26f6dd8b5a14d9d055d89c12c95e3a44bd50c99549c8da9", + "hasDABEndpoint": true, + "hasClientId": true, + "msiEndpoint": "http://127.0.0.1:41410/msi/token/" + }, + "timestamp": "2026-04-03T11:04:54Z" +} +``` + The flow should be: @@ -538,22 +409,21 @@ need to give the container app MI access to the database ## Tidy Up -If you've been following along you can tidy up and remove the whole resource group with the following command +If you've been following along you can tidy up and remove all the resources. First, delete the Azure resource group: ```PowerShell -az group delete --name $resourceGroup +# Delete all Azure resources (SQL DB, Storage, Container, Function App) +az group delete --name $resourceGroup --yes --no-wait ``` -## Up Next - -troubleshooting? +Then clean up the Entra ID resources (these are not deleted with the resource group): -## dab Blog Series - -Here are all the links to the dab blog series: - -1. [Data API Builder](/dab-api-builder/) -2. [Running dab in an Azure Container Instance](/dab-api-container/) -3. More coming soon... +```PowerShell +# Delete the app registration (this also removes app roles and assignments) +az ad app delete --id $app_id -Or you can view all posts about dab using the [dab](/categories/dab/) category. +# The service principal is automatically deleted when the app registration is deleted +# But if you need to delete it separately: +# $apiServicePrincipalId = az ad sp list --filter "appId eq '$app_id'" --query "[0].id" -o tsv +# az ad sp delete --id $apiServicePrincipalId +``` diff --git a/blog/content/post/2026/dab-api-azure-func-auth/image-1.png b/blog/content/post/2026/dab-api-user-auth/image-1.png similarity index 100% rename from blog/content/post/2026/dab-api-azure-func-auth/image-1.png rename to blog/content/post/2026/dab-api-user-auth/image-1.png diff --git a/blog/content/post/2026/dab-api-azure-func-auth/image-3.png b/blog/content/post/2026/dab-api-user-auth/image-3.png similarity index 100% rename from blog/content/post/2026/dab-api-azure-func-auth/image-3.png rename to blog/content/post/2026/dab-api-user-auth/image-3.png diff --git a/blog/content/post/2026/dab-api-user-auth/image-9.png b/blog/content/post/2026/dab-api-user-auth/image-9.png new file mode 100644 index 0000000..f85a945 Binary files /dev/null and b/blog/content/post/2026/dab-api-user-auth/image-9.png differ diff --git a/blog/content/post/2026/dab-api-user-auth/index.md b/blog/content/post/2026/dab-api-user-auth/index.md new file mode 100644 index 0000000..ec2b7df --- /dev/null +++ b/blog/content/post/2026/dab-api-user-auth/index.md @@ -0,0 +1,124 @@ +--- +title: "DAB API - User Authentication from Azure CLI" +slug: "dab-api-user-auth" +description: "ENTER YOUR DESCRIPTION" +date: 2025-08-21T09:14:40Z +categories: + - DAB + - api + - PowerShell +tags: + - DAB + - api + - PowerShell +image: header.png +draft: true +--- + +This is post four in my series about the Data API Builder (DAB), the first post, [Data API Builder](/dab-api-builder/), covers what DAB is and how to test it locally against SQL Server in running in a container. The second post, [Running DAB in an Azure Container Instance](/dab-api-container/), starts to productionise this, moving it into the cloud, but with no auth required to hit the endpoints. Then in the last post, [DAB API - Authenticated API Endpoints](/dab-api-add-auth), we locked down the endpoints, they now require authentication to interact with the data. + +In this post we'll walk through how to authenticate with Entra from the Microsoft Azure CLI and then access our API endpoints to get some data. + +If you're looking to follow along you need to have the infra we built in the previous two posts, [Running DAB in an Azure Container Instance](/dab-api-container) and [DAB API - Authenticated API Endpoints](/dab-api-add-auth): + +- an Azure SQL Database which is the source +- a Storage Account hosting the `dab-config.json` file +- an Azure Container App running DAB +- an Entra App Registration & Enterprise Application + +At this point your containerized instance of DAB should be running, and healthy. However, if I call the DAB API endpoint without authentication I get a 401 error. + +```PowerShell +$data = Invoke-RestMethod -Uri 'http://ci-dab-prod-001.uksouth.azurecontainer.io:5000/api/dbo_BuildVersion' +$data.value +``` + +## Entra User Authentication + +Let's look at how I can authenticate with my Entra user to get a token and access the DAB endpoints to retrieve data. There is one more setup step to configure this to work properly. + +### Update Entra App registration for Authentication + +We do already have an app role configured, but those only get us application tokens — that's the route we'll use in the next post when the function calls the API as itself. But Azure CLI is signing in as me, so Entra issues a user token instead, and user permissions use a different claim: `scp` rather than roles. Therefore we need to expose at least one delegated scope on the API before the CLI has anything valid to ask for. + +We also need to pre-authorize the Azure CLI to request tokens for our DAB API. The authentication flow we'll use will require the client to be authorized, and the Azure CLI doesn't provide a consent UI. Since we already know the scope ID we're creating, we can do both in a single PATCH to the Graph API. + +This looks complicated but lets break down the body variable, which is the JSON of our Graph API request into three pieces: + +1. `oauth2PermissionScopes` - adding the user authentication +2. `preAuthorizedApplications` - preauthorising the Azure CLI +3. `requestedAccessTokenVersion` is set to 1 because the function uses 1 and we have to choose the same token version + +```PowerShell +$azureCliAppId = "04b07795-8ddb-461a-bbee-02f9e1bf7b46" # Microsoft Azure CLI +$existingApp = az ad app show --id $app_id | ConvertFrom-Json + +$newScopeId = [guid]::NewGuid().ToString() +$body = @{ + api = @{ + oauth2PermissionScopes = @( + @{ + id = $newScopeId + value = "user_impersonation" + type = "User" + isEnabled = $true + adminConsentDisplayName = "Access DAB API as user" + adminConsentDescription = "Allow the application to access DAB API on behalf of the signed-in user" + userConsentDisplayName = "Access DAB API as you" + userConsentDescription = "Allow the application to access DAB API on your behalf" + } + ) + preAuthorizedApplications = @( + @{ + appId = $azureCliAppId + delegatedPermissionIds = @($newScopeId) + } + ) + requestedAccessTokenVersion = 1 + } +} + +# Compress avoids newlines; escape double quotes for the shell +$bodyJson = ($body | ConvertTo-Json -Depth 10 -Compress) -replace '"', '\"' + +az rest --method PATCH ` + --uri "https://graph.microsoft.com/v1.0/applications/$($existingApp.id)" ` + --headers "Content-Type=application/json" ` + --body "$bodyJson" +``` + +You can verify this in the portal, within Entra on our Enterprise Application (you might need to search by `$APP_ID` since this was newly created), under `Expose an API` you should see a scope and an authorized client application. + +![alt text](image-9.png) + +If you didn't do this step, or something above isn't set correctly you'll get the following error, which is quite helpful in that it tells you the scope (your app id) and the Azure CLI App ID you need to solve the problem. + +> Authentication failed +> invalid_client: AADSTS650057: Invalid resource. The client has requested access to a resource which is not listed in the requested permissions in the client's application registration. Client app ID: 04b07795-8ddb-461a-bbee-02f9e1bf7b46(Microsoft Azure CLI). Resource value from request: api://4834e86a-398b-4992-bd46-f8f827c02560. Resource app ID: 4834e86a-398b-4992-bd46-f8f827c02560. List of valid resources from app registration: . Trace ID: 0d114f90-7a24-40d4-bf84-10d7b2522300 Correlation ID: 211a8fae-9ed4-4d35-a086-f37aba4f6522 Timestamp: 2026-04-08 05:58:45Z. ($error_uri) + +## Test + +Now everything is in place we should be able to login to Azure using the Azure CLI, get a token, and then use that token for our API call to our DAB endpoint. Let's test it out. + +```PowerShell +az login --tenant $tenantId --scope "api://$app_ID/.default" + +# Get a new token (should now be v1.0) +#$app_id = (az ad app create --display-name "DAB-API-Access" --sign-in-audience "AzureADMyOrg" | ConvertFrom-Json).appId +$token = (az account get-access-token --resource "api://$app_id" | ConvertFrom-Json).accessToken + +# Verify it's v1.0 +$tokenParts = $token.Split('.') +$payload = $tokenParts[1] +while ($payload.Length % 4 -ne 0) { $payload += '=' } +$decodedBytes = [System.Convert]::FromBase64String($payload) +$decodedJson = [System.Text.Encoding]::UTF8.GetString($decodedBytes) +$tokenClaims = $decodedJson | ConvertFrom-Json +Write-Host "Issuer: $($tokenClaims.iss)" # Should be https://sts.windows.net/tenant-id/ + +# Test the API +$headers = @{ 'Authorization' = "Bearer $token" } +Invoke-RestMethod -Uri 'http://ci-dab-prod-001.uksouth.azurecontainer.io:5000/api/dbo_BuildVersion' -Headers $headers +``` + +![alt text](image-5.png) \ No newline at end of file