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/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..5f722e0 --- /dev/null +++ b/blog/content/post/2026/dab-api-add-auth/index.md @@ -0,0 +1,256 @@ +--- +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..7fd8ac7 --- /dev/null +++ b/blog/content/post/2026/dab-api-user-auth/index.md @@ -0,0 +1,171 @@ +--- +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 +--- + + + + + + + + +Yes! Entra users can authenticate and call the DAB endpoints. You need to: + +Get a token for the DAB API +Be assigned the app role (or have access via delegated permissions) +Include the token in the Authorization header +Here's how to do it with PowerShe + + ```PowerShell + # Get your user's object ID + $userId = az ad signed-in-user show --query "id" -o tsv + + # Get the service principal and app role ID + $apiServicePrincipalId = az ad sp list --filter "appId eq '$app_id'" --query "[0].id" -o tsv + $appRoleId = az ad sp list --filter "appId eq '$app_id'" --query "[0].appRoles[?value=='DAB.Access'].id" -o tsv + + # Assign your user to the app role + $assignmentBody = @{ + principalId = $userId + resourceId = $apiServicePrincipalId + appRoleId = $appRoleId + } + #TODO: change from being a temp file + $tempFile = [System.IO.Path]::GetTempFileName() + $assignmentBody | ConvertTo-Json | Set-Content $tempFile -Encoding UTF8 + + az rest --method POST ` + --uri "https://graph.microsoft.com/v1.0/users/$userId/appRoleAssignments" ` + --headers "Content-Type=application/json" ` + --body "@$tempFile" + + Remove-Item $tempFile + ``` + + +## Add a Delegated Permission Scope + +TODO - why? + +Need this before we authorise Microsoft Azure CLI to get tokens + + ```PowerShell + +# Create a delegated permission scope (use different name than app role) +$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" + } + ) + } +} + +$tempFile = [System.IO.Path]::GetTempFileName() +$body | ConvertTo-Json -Depth 10 | Set-Content $tempFile -Encoding UTF8 + +az rest --method PATCH ` + --uri "https://graph.microsoft.com/v1.0/applications/$($existingApp.id)" ` + --headers "Content-Type=application/json" ` + --body "@$tempFile" + +Remove-Item $tempFile + +``` + +## Authorize the Azure CLI to request tokens + + The issue is that Azure CLI is not pre-authorized to request tokens for your DAB API. You need to add Azure CLI as a known/trusted client application. Here's how: + +> 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) + + ```PowerShell + #$app_id = "691ad7dd-7451-4748-92b1-0da2f288ef0d" + $azureCliAppId = "04b07795-8ddb-461a-bbee-02f9e1bf7b46" # Microsoft Azure CLI + + # Get the app details + $existingApp = az ad app show --id $app_id | ConvertFrom-Json + + # Get your scope ID (the one we just created) + $scopeId = $existingApp.api.oauth2PermissionScopes[0].id + + # Add Azure CLI as a pre-authorized application + $preAuthorizedApp = @{ + appId = $azureCliAppId + delegatedPermissionIds = @($scopeId) + } + + # Build the API object with existing scopes and new pre-authorization + $api = @{ + oauth2PermissionScopes = $existingApp.api.oauth2PermissionScopes + preAuthorizedApplications = @($preAuthorizedApp) + requestedAccessTokenVersion = 1 + } + + # Update the app + $tempFile = [System.IO.Path]::GetTempFileName() + @{ api = $api } | ConvertTo-Json -Depth 10 | Set-Content $tempFile -Encoding UTF8 + + az rest --method PATCH ` + --uri "https://graph.microsoft.com/v1.0/applications/$($existingApp.id)" ` + --headers "Content-Type=application/json" ` + --body "@$tempFile" + + Remove-Item $tempFile + ``` + +`requestedAccessTokenVersion` has to be set to 1 because the function uses 1 and we have to choose the same one + +Now on our Enterprise Application (you might need to search by `$APP_ID` in Entra since this is newly created), under `Expose an API` you should see a scope and an authorized client application. + +![alt text](image-9.png) + +## Test + +```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