visit
Benefits this brings:
The solution consists of two Azure Functions which interact with the Microsoft Graph API and the SwaggerHub User Management API. The first function, SubscriptionManager
, manages subscriptions to Microsoft Graph change notifications.
The second function, SwaggerHubUserManager
, receives notifications (or events) from the Microsoft Graph and parses them. When appropriate, it will interact with the SwaggerHub User Management API to create, update, or delete users.
To deploy or run the solution locally, you need to have an Azure subscription and an Azure AD tenant setup.
//localhost
)Be sure to record your Application (client) ID and Directory (tenant) ID from the application overview section as you'll need them later.
You want this application to receive change notifications from the Microsoft Graph when the Groups resource changes, and to be able to retrieve information on the users added/removed from a group. To do this, the following permissions are required:
Group.Read.All
- Read all groupsUser.Read.All
- Read all users' full profiles
Group.Read.All
User.Read.All
SwaggerHub Organization |
SwaggerHub Role |
AD Group Name (example only - follow your own naming conventions) |
Description |
---|---|---|---|
Org 1 | CONSUMER | ADG-SWAGGERHUB-CONSUMERS | AD Group for consumers that will get access to SwaggerHub |
Org 1 | DESIGNERS | ADG-SWAGGERHUB-DESIGNERS | AD Group for designers that will get access to SwaggerHub |
Org 1 | OWNERS | ADG-SWAGGERHUB-OWNERS | AD Group for owners that will get access to SwaggerHub |
If you have multiple organizations within SwaggerHub, you have a few options for structuring your organizational groups. You can linearly extend the examples above and be very clear about setting up your groups per organizations - for instance, including the SwaggerHub organization name in the Azure AD Group name (e.g., ADG-SwaggerHub-<ORG NAME>-<ROLE NAME>).
SwaggerHub Organization |
SwaggerHub Role |
AD Group Name (example only - follow your own naming conventions) |
Description |
---|---|---|---|
Org 1 | CONSUMER | ADG-SWAGGERHUB-ORG1-CONSUMERS | AD Group for consumers that will get access to SwaggerHub |
Org 1 | DESIGNERS | ADG-SWAGGERHUB-ORG1-DESIGNERS | AD Group for designers that will get access to SwaggerHub |
Org 1 | OWNERS | ADG-SWAGGERHUB-ORG1-OWNERS | AD Group for owners that will get access to SwaggerHub |
Org 2 | CONSUMER | ADG-SWAGGERHUB-ORG2-CONSUMERS | AD Group for consumers that will get access to SwaggerHub |
Org 2 | DESIGNERS | ADG-SWAGGERHUB-ORG2-DESIGNERS | AD Group for designers that will get access to SwaggerHub |
Org 2 | OWNERS | ADG-SWAGGERHUB-ORG2-OWNERS | AD Group for owners that will get access to SwaggerHub |
The .NET 5
solution, predominately consists of two Azure functions with some Models
and supporting Services
for interacting with the Microsoft Graph API and the SwaggerHub User Management API.
This is a TimerTrigger
function which runs daily at noon based on the configured ‘0 0 12 * * *’
. It manages the change notification subscription lifecycle with the Microsoft Graph. It creates a new subscription if one does not exist; otherwise, it will renew an existing subscription if it’s due to expire within seven days, which means our SwaggerHubUserManager
function won’t miss any change notifications.
Interaction with the Microsoft Graph API is through the class, which is a trimmed version of MS Graph SDK capabilities for the needs of the solution. It initializes a GraphServiceClient
instance and takes care of obtaining the OAuth 2.0 token which is required to invoke the MS Graph endpoints.
As visible in Figure 4 above, the service offers methods to manage the lifecycle of the subscription, and the SubscriptionManager
function interacts with the service methods accordingly (see snippet below):
try
{
// Get subscriptions
var longestToLiveSubscription = await _graphService.GetSubscriptionWithLongestTimeToLive();
// if no subscription then create subscription
if(longestToLiveSubscription == null)
{
logger.LogInformation($"No subscription found >> creating new subscription...");
await _graphService.CreateChangeNotificationSubscription();
}
else
{
// if subscription will expire in less than a week renew the subscription
if(DateTime.UtcNow.AddDays(7) > longestToLiveSubscription.ExpirationDateTime)
{
logger.LogInformation($"Subscription {longestToLiveSubscription.Id} will expire within 7 days >> renewing subscription...");
await _graphService.RenewSubscription(longestToLiveSubscription);
}
else
{
logger.LogInformation($"Subscription {longestToLiveSubscription.Id} will expire at [{longestToLiveSubscription.ExpirationDateTime}] >> no need to renew at this time...");
}
}
}
catch(Exception ex)
{
logger.LogError($"Exception thrown: {ex.Message}");
}
Get Subscriptions
curl --location --request GET '//graph.microsoft.com/v1.0/subscriptions/' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer PUT_YOUR_TOKEN_HERE'
Create Subscriptions
curl --location --request GET '//graph.microsoft.com/v1.0/subscriptions/' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer PUT_YOUR_TOKEN_HERE' \
--data-raw '{
"changeType": "updated,deleted",
"notificationUrl": "URL to endpoint that will receive events",
"resource": "groups",
"expirationDateTime": "2022-12-30T22:19:41.561Z",
"clientState": "*someSecretKnownByPubandSub*"
}'
Update Subscriptions
curl --location -g --request GET '//graph.microsoft.com/v1.0/subscriptions/{id}' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer PUT_YOUR_TOKEN_HERE' \
--data-raw '{
"expirationDateTime": "2022-12-01T22:19:41.561Z"
}'
This HTTPTrigger
function is the heart of the solution and is called by the Microsoft Graph webhook anytime there is a change to group memberships within the Azure AD tenant. The function parses the received change notification and determines if it’s required to process the change notification. To decide, it leverages the tenant specific configuration that’s set up in the GroupConfiguration.json
file. If the received change notification relates to a group referenced in the configuration file, it goes back to the Microsoft Graph to retrieve specific details on the users which were added to or removed from the AD group membership. Then it calls the SwaggerHub User Management API to ensure that changes are automatically reflected in the appropriate SwaggerHub organizations.
Interaction with the SwaggerHub User Management API is through the class. This service prepares the requests to, and handles the responses from, the class. This class is a wrapper around an HTTPClient
exposing generic GET
, POST
, PATCH
, and DELETE
methods and sets some base SwaggerHub settings like apiKey, base URL, API path, and API version. This repository class simplifies the API calls for the upper service layer, so that it doesn’t need to negotiate an HTTP request.
The following snippet demonstrates how the SwaggerHubUserManagerFunction
acknowledges the verification request based on the existence of a validationToken
query parameter. The validationToken
received must be returned in plain text.
// basic validation of request
var query = System.Web.HttpUtility.ParseQueryString(req.Url.Query);
string token = query["validationToken"];
if(!string.IsNullOrEmpty(token))
{
//acknowledge back with validation token
logger.LogInformation($"Validation Token: {token}");
var ackResponse = req.CreateResponse(HttpStatusCode.OK);
ackResponse.Headers.Add("Content-Type", "text/plain; charset=utf-8");
ackResponse.WriteString(token);
return ackResponse;
}
The solution can support single SwaggerHub organization configurations, as well as complex multi-organizational setups as discussed in the “Azure AD Group Structures” section above. Once you have finalized the appropriate setup, you need to populate the GroupConfiguration.json
file to reflect the setup. This acts as a link between your Azure AD tenant and your SwaggerHub organization, and it’s leveraged by the Azure function to:
{
"GroupConfiguration": {
"activeDirectoryGroups": [
{
"objectId": "bc9ae6d5-1ccf-45dd-b56f-9ca020348802",
"name": "ADG-SWAGGERHUB-DESIGNERS",
"swaggerHubRole": "DESIGNER",
"organizations": [
{
"name": "frank-kilcommins"
}
]
},
{
"objectId": "afd8b023-331d-4fc4-84f7-f5a4654cbbbd",
"name": "ADG-SWAGGERHUB-CONSUMERS",
"swaggerHubRole": "CONSUMER",
"organizations": [
{
"name": "frank-kilcommins"
}
]
},
{
"objectId": "126f28b7-a2b6-4a87-af23-edb61af80317",
"name": "ADG-SWAGGERHUB-OWNERS",
"swaggerHubRole": "CONSUMER",
"organizations": [
{
"name": "frank-kilcommins"
}
]
}
]
}
}
The main object configured is the activeDirectoryGroup
object which stores the mapping details, allowing the solution to manage SwaggerHub organization(s) based on a particular Azure AD Group.
Property |
Property (nested) |
Required |
Description |
---|---|---|---|
objectId |
| yes | The objectId of the AD Group as autogenerated by the tenant |
name |
| no | The name of the AD Group as set within the tenant |
swaggerHubRole |
| yes | The role that will be set in SwaggerHub for users receiving membership to the AD Group referenced by the objectId. Allowed options are CONSUMER, DESIGNER or OWNER |
organizations |
| yes | The SwaggerHub organizations mapped or linked to the AD Group referenced by the objectId |
| name | yes |
The name of the organization as created within SwaggerHub |
The solution comes with a built-in to validate the GroupConfiguration.json
and the configuration is validated against the schema during startup.
Option 1
Leverage the “Deploy to Azure” button to deploy the required Azure infrastructure to your Azure subscription.
Note: You still need to publish the code manually to your resources. Follow the Setting Up Locally section in the GitHub repo and deploy your code to your Azure environment.
Option 2
Fork the source code to your own repo and create the following GitHub action:on: [workflow_dispatch] # change the triggering mechanism to suit your needs (manual run by default)
name: AzureAD-SwaggerHub-UserManagement-Setup-and-Deploy
env:
AZURE_FUNCTIONAPP_PACKAGE_PATH: 'src' # set this to the path to your web app project, defaults to the repository root
DOTNET_VERSION: '5.0.402' # set this to the dotnet version to use
jobs:
# use ARM templates to set up the Azure Infra
deploy-infrastructure:
runs-on: ubuntu-latest
# set outputs needed by subsequent jobs
outputs:
azFunctionAppName: ${{ steps.armdeploy.outputs.functionAppName }}
steps:
# check out code
- uses: actions/checkout@main
# login to Azure
- uses: azure/login@v1
with:
creds: ${{ secrets.AzureAD_SwaggerHub_CREDENTIALS }}
# deploy ARM template to setup azure resources (group & sub defined in credentials)
- name: Run ARM deploy
id: armdeploy
uses: azure/arm-deploy@v1
with:
subscriptionId: ${{ secrets.AZURE_SUBSCRIPTION }}
resourceGroupName: ${{ secrets.AZURE_RG }}
template: ./azuredeploy.json
parameters: ./azuredeploy.parameters.json
# build and deploy our Azure functions for SwaggerHub + Azure AD user mgmt
build-and-deploy:
needs: [deploy-infrastructure]
runs-on: windows-latest
environment: prd
steps:
# check out code
- name: 'Checkout code'
uses: actions/checkout@main
# login to Azure
- uses: azure/login@v1
with:
creds: ${{ secrets.AzureAD_SwaggerHub_CREDENTIALS }}
enable-AzPSSession: true
# get publish profile
- name: Get publish profile
id: fncapp
uses: azure/powershell@v1
with:
inlineScript: |
$profile = ""
$profile = Get-AzWebAppPublishingProfile -ResourceGroupName ${{ secrets.AZURE_RG }} -Name ${{ needs.deploy-infrastructure.outputs.azFunctionAppName }}
$profile = $profile.Replace("`r", "").Replace("`n", "")
Write-Output "::set-output name=profile::$profile"
azPSVersion: "latest"
# setup donet environments
- name: Setup DotNet Environments
uses: actions/setup-dotnet@v1
with:
dotnet-version: |
3.1.x
${{ env.DOTNET_VERSION }}
# build project
- name: 'Resolve dependencies and build'
shell: pwsh
run: |
pushd './${{ env.AZURE_FUNCTIONAPP_PACKAGE_PATH }}'
dotnet restore
dotnet build --configuration Release --output ./output
popd
# publish azure function
- name: 'Run Azure Functions Action'
uses: Azure/functions-action@v1
id: fa
with:
app-name: ${{ needs.deploy-infrastructure.outputs.azFunctionAppName }}
package: '${{ env.AZURE_FUNCTIONAPP_PACKAGE_PATH }}/output'
publish-profile: ${{ steps.fncapp.outputs.profile }}
The GitHub action above, needs the following secrets configured:
Azure_SUBSCRIPTION
- your Azure subscription IDAzure_RG
- the name of the Azure Resource Group that you want to deploy this solution intoAZUREAD_SWAGGERHUB_CREDENTIALS
- the credentials for a Service Principal with contributor access for the resource group. See how to get the .