Get rid of App registrations and Key Vault (Making good things even better)

Published Mar 27 2022 01:41 PM 1,829 Views

 

 

supermario.jpgI love open-source, because it is a fantastic way to learn and share. I recently saw this tweet by Peter Klapwijk, who built a Logic App to monitor licenses of your Microsoft 365 tenant. The solutions uses

  1. a Logic App with an Office 365 API connection (to send emails with the built-in Outlook connector)
  2. a Key Vault (to protect the secret that is generated for the Azure AD app registration)
  3. manual deployment with a custom template

As much as I love the idea of the solution, I felt this can be improved - and this is of course complaining on a very high level.

I wanted to do two things:

  1. Get rid of the secret and the Key Vault
  2. Automate deployment

Get rid of the app registration

 

You may ask- How does this work? At least that was Peter’s first question :) App registrations in Azure Active Directory (which handles identity and access management) give your app an identity and we can assign (and consent to) permissions in different APIs (like Microsoft Graph). To make things secure, we can protect this app with a secret. Together with the app id (client id) this is username/password for the app.

As we now want to protect this secret, we store can store it in an Azure Key Vault, but to log into Key Vault, we again need credentials - it’s a chicken/egg problem. Also, rotating the secret and taking care when it expires is a tedious task.

But even if you feel that Key Vault is fine - There is still an security hole:

⚠ If you have a contributor role assignment on the app (not on the Key Vault!), you can read the value of the secret that is stored in Key Vault in Kudu.⚠

 

Let’s face it: where there are secrets, there will be leaks.

 

Now there is a solution to this problem: Azure Managed Identities - give your app an identity without an app registration. It’s an amazing example on how Azure abstracts away the complex stuff (creating, storing, rotating secrets) so you can focus on the nicer parts of development. I decided to use a user-assigned Managed Identity, which is a standalone Azure resource and can be shared in case we want to add more resources to an extended version of the solution. This Managed Identity of course will need to have the permissions assigned that the previous app registration had assigned.

Automate deployment

 

I created Bicep :flexed_biceps: files (super cool way to provide an ARM template) and split the template so that I can call every resource (Logic App, Managed Identity, API connection) as a module from the root deployment file and end up with separate files for each resource. That is more convenient to work with during development and also makes debugging easier.

param connections_office365_name string = 'office365'
param workflows_Monitor_main_name string = 'Monitor-LogicApp'
param userAssignedIdentities_Monitor_Identity_name string = 'Monitor-ManagedIdentity'
param ResourceGroupName string
param resourceLocation string


module managedIdentityDeployment 'Monitor-ManagedIdentity.bicep' = {
  name: 'managedIdentityDeployment'
  params: {
    userAssignedIdentities_Monitor_Identity_name: userAssignedIdentities_Monitor_Identity_name
    resourceLocation: resourceLocation
  }
}

module connectionsDeployment 'Monitor-connections.bicep' = {
  name: 'connectionsDeployment'
  params: {
    connections_office365_name: connections_office365_name
    resourceLocation: resourceLocation
    ResourceGroupName: ResourceGroupName

  }
}

module MainDeployment 'Monitor-main.bicep' = {
  name: 'MainDeployment'
  params: {
    resourceLocation: resourceLocation
    userAssignedIdentities_Monitor_Identity_name: userAssignedIdentities_Monitor_Identity_name
    workflows_Monitor_main_name: workflows_Monitor_main_name
    connections_office365_name: connections_office365_name
  }
  dependsOn: [
    connectionsDeployment
    managedIdentityDeployment                                                       
  ]
}

I learned this one the hard way: Providing Infrastructure as Code (even if it is just for a small solution) is a faster, more sustainable and better way to deploy resources. It is painful how often GUIs change and results are not repeatable then. Also, it is more convenient to run a script rather than having to click yourself through a lengthy README file. (Been there as well)

The script will create the resource group that holds the resources and assign the correct Microsoft Graph permission scope to the managed identity:

 

Here is the interesting part that assigns the permission, full script on GitHub :green_heart::

$ManagedIdentity = az identity show --name Monitor-ManagedIdentity --resource-group $ResourceGroupName | ConvertFrom-Json

$principalId = $ManagedIdentity.principalId
# Get current role assignments
$currentRoles = (az rest `
    --method get `
    --uri https://graph.microsoft.com/v1.0/servicePrincipals/$principalId/appRoleAssignments `
    | ConvertFrom-Json).value `
    | ForEach-Object { $_.appRoleId }

$graphResourceId = az ad sp list --display-name "Microsoft Graph" --query [0].objectId
#Get appRoleIds for Organization.Read.All
$graphId = az ad sp list --query "[?appDisplayName=='Microsoft Graph'].appId | [0]" --all
$orgReadAll = az ad sp show --id $graphId --query "appRoles[?value=='Organization.Read.All'].id | [0]" -o tsv

$appRoleIds = $orgReadAll
#Loop over all appRoleIds - in case we later extend and need more than permission
foreach ($appRoleId in $appRoleIds) {
    $roleMatch = $currentRoles -match $appRoleId
    if ($roleMatch.Length -eq 0) {
        # Add the role assignment to the principal
        $body = "{'principalId':'$principalId','resourceId':'$graphResourceId','appRoleId':'$appRoleId'}";
        az rest `
            --method post `
            --uri https://graph.microsoft.com/v1.0/servicePrincipals/$principalId/appRoleAssignments `
            --body $body `
            --headers Content-Type=application/json 
    }
}
Write-Host "🚀 -Deployment completed"

(I re-used the script that we use at ProvisionGenie 🧞)

 

As a result, we can see the the assigned permission scope in Azure Active Directory

(Navigate to Enterprise Applications, then filter by Managed Identities, select the created Managed Identity and select Permissions)

 

I did a PR on Peter Klapwijks repository and added my approach there as well - communityrocks :sparkles:

What do you think?

You like it? Let me know! You don’t like it? Let’s talk!

👉🏻 Find the discussion on twitter

%3CLINGO-SUB%20id%3D%22lingo-sub-3268194%22%20slang%3D%22en-US%22%3EGet%20rid%20of%20App%20registrations%20and%20Key%20Vault%20(Making%20good%20things%20even%20better)%3C%2FLINGO-SUB%3E%3CLINGO-BODY%20id%3D%22lingo-body-3268194%22%20slang%3D%22en-US%22%3E%3CP%3E%26nbsp%3B%3C%2FP%3E%0A%3CP%3E%26nbsp%3B%3C%2FP%3E%0A%3CP%3E%3CSPAN%20class%3D%22lia-inline-image-display-wrapper%20lia-image-align-inline%22%20image-alt%3D%22supermario.jpg%22%20style%3D%22width%3A%20999px%3B%22%3E%3CIMG%20src%3D%22https%3A%2F%2Ftechcommunity.microsoft.com%2Ft5%2Fimage%2Fserverpage%2Fimage-id%2F358879i58156FCD931D6B7D%2Fimage-size%2Flarge%3Fv%3Dv2%26amp%3Bpx%3D999%22%20role%3D%22button%22%20title%3D%22supermario.jpg%22%20alt%3D%22supermario.jpg%22%20%2F%3E%3C%2FSPAN%3EI%20love%20open-source%2C%20because%20it%20is%20a%20fantastic%20way%20to%20learn%20and%20share.%20I%20recently%20saw%3CSPAN%3E%26nbsp%3B%3C%2FSPAN%3E%3CA%20href%3D%22https%3A%2F%2Ftwitter.com%2Finthecloud_247%2Fstatus%2F1500035293684060161%22%20target%3D%22_blank%22%20rel%3D%22noopener%20nofollow%20noreferrer%22%3Ethis%20tweet%20by%20Peter%20Klapwijk%3C%2FA%3E%2C%20who%20built%20a%20Logic%20App%20to%20monitor%20licenses%20of%20your%20Microsoft%20365%20tenant.%20The%20solutions%20uses%3C%2FP%3E%0A%3COL%3E%0A%3CLI%3Ea%20Logic%20App%20with%20an%20Office%20365%20API%20connection%20(to%20send%20emails%20with%20the%20built-in%20Outlook%20connector)%3C%2FLI%3E%0A%3CLI%3Ea%20Key%20Vault%20(to%20protect%20the%20secret%20that%20is%20generated%20for%20the%20Azure%20AD%20app%20registration)%3C%2FLI%3E%0A%3CLI%3Emanual%20deployment%20with%20a%20custom%20template%3C%2FLI%3E%0A%3C%2FOL%3E%0A%3CP%3EAs%20much%20as%20I%20love%20the%20idea%20of%20the%20solution%2C%20I%20felt%20this%20can%20be%20improved%20-%20and%20this%20is%20of%20course%20complaining%20on%20a%20very%20high%20level.%3C%2FP%3E%0A%3CP%3EI%20wanted%20to%20do%20two%20things%3A%3C%2FP%3E%0A%3COL%3E%0A%3CLI%3EGet%20rid%20of%20the%20secret%20and%20the%20Key%20Vault%3C%2FLI%3E%0A%3CLI%3EAutomate%20deployment%3C%2FLI%3E%0A%3C%2FOL%3E%0A%3CH2%20id%3D%22get-rid-of-the-app-registration%22%20id%3D%22toc-hId-392950827%22%20id%3D%22toc-hId-418578712%22%3EGet%20rid%20of%20the%20app%20registration%3C%2FH2%3E%0A%3CP%3E%26nbsp%3B%3C%2FP%3E%0A%3CP%3EYou%20may%20ask-%20How%20does%20this%20work%3F%20At%20least%20that%20was%20Peter%E2%80%99s%20first%20question%20%3A)%3C%2Fimg%3E%20App%20registrations%20in%20Azure%20Active%20Directory%20(which%20handles%20identity%20and%20access%20management)%20give%20your%20app%20an%20identity%20and%20we%20can%20assign%20(and%20consent%20to)%20permissions%20in%20different%20APIs%20(like%20Microsoft%20Graph).%20To%20make%20things%20secure%2C%20we%20can%20protect%20this%20app%20with%20a%20secret.%20Together%20with%20the%20app%20id%20(client%20id)%20this%20is%20username%2Fpassword%20for%20the%20app.%3C%2FP%3E%0A%3CP%3EAs%20we%20now%20want%20to%20protect%20this%20secret%2C%20we%20store%20can%20store%20it%20in%20an%20Azure%20Key%20Vault%2C%20but%20to%20log%20into%20Key%20Vault%2C%20we%20again%20need%20credentials%20-%20it%E2%80%99s%20a%20chicken%2Fegg%20problem.%20Also%2C%20rotating%20the%20secret%20and%20taking%20care%20when%20it%20expires%20is%20a%20tedious%20task.%3C%2FP%3E%0A%3CP%3EBut%20even%20if%20you%20feel%20that%20Key%20Vault%20is%20fine%20-%20There%20is%20still%20an%20security%20hole%3A%3C%2FP%3E%0A%3CP%3E%E2%9A%A0%20If%20you%20have%20a%20contributor%20role%20assignment%20on%20the%20app%20(not%20on%20the%20Key%20Vault!)%2C%3CSPAN%3E%26nbsp%3B%3C%2FSPAN%3E%3CA%20href%3D%22https%3A%2F%2Fgithub.com%2FMicrosoftDocs%2Fazure-docs%2Fissues%2F39518%22%20target%3D%22_blank%22%20rel%3D%22noopener%20noreferrer%22%3Eyou%20can%20read%20the%20value%20of%20the%20secret%20that%20is%20stored%20in%20Key%20Vault%20in%20Kudu%3C%2FA%3E.%E2%9A%A0%3C%2FP%3E%0A%3CP%3E%26nbsp%3B%3C%2FP%3E%0A%3CBLOCKQUOTE%3E%0A%3CP%3ELet%E2%80%99s%20face%20it%3A%20where%20there%20are%20secrets%2C%20there%20will%20be%20leaks.%3C%2FP%3E%0A%3C%2FBLOCKQUOTE%3E%0A%3CP%3E%26nbsp%3B%3C%2FP%3E%0A%3CP%3ENow%20there%20is%20a%20solution%20to%20this%20problem%3A%3CSPAN%3E%26nbsp%3B%3C%2FSPAN%3E%3CA%20href%3D%22https%3A%2F%2Fdocs.microsoft.com%2Fazure%2Factive-directory%2Fmanaged-identities-azure-resources%2Foverview%22%20target%3D%22_blank%22%20rel%3D%22noopener%20noreferrer%22%3EAzure%20Managed%20Identities%3C%2FA%3E%3CSPAN%3E%26nbsp%3B%3C%2FSPAN%3E-%20give%20your%20app%20an%20identity%3CSPAN%3E%26nbsp%3B%3C%2FSPAN%3E%3CEM%3Ewithout%3C%2FEM%3E%3CSPAN%3E%26nbsp%3B%3C%2FSPAN%3Ean%20app%20registration.%20It%E2%80%99s%20an%20amazing%20example%20on%20how%20Azure%20abstracts%20away%20the%20complex%20stuff%20(creating%2C%20storing%2C%20rotating%20secrets)%20so%20you%20can%20focus%20on%20the%20nicer%20parts%20of%20development.%20I%20decided%20to%20use%20a%20user-assigned%20Managed%20Identity%2C%20which%20is%20a%20standalone%20Azure%20resource%20and%20can%20be%20shared%20in%20case%20we%20want%20to%20add%20more%20resources%20to%20an%20extended%20version%20of%20the%20solution.%20This%20Managed%20Identity%20of%20course%20will%20need%20to%20have%20the%20permissions%20assigned%20that%20the%20previous%20app%20registration%20had%20assigned.%3C%2FP%3E%0A%3CH2%20id%3D%22automate-deployment%22%20id%3D%22toc-hId--1414503636%22%20id%3D%22toc-hId--1388875751%22%3EAutomate%20deployment%3C%2FH2%3E%0A%3CP%3E%26nbsp%3B%3C%2FP%3E%0A%3CP%3EI%20created%20Bicep%20%3Aflexed_biceps%3A%20files%20(super%20cool%20way%20to%20provide%20an%20ARM%20template)%20and%20split%20the%20template%20so%20that%20I%20can%20call%20every%20resource%20(Logic%20App%2C%20Managed%20Identity%2C%20API%20connection)%20as%20a%20module%20from%20the%20root%20deployment%20file%20and%20end%20up%20with%20separate%20files%20for%20each%20resource.%20That%20is%20more%20convenient%20to%20work%20with%20during%20development%20and%20also%20makes%20debugging%20easier.%3C%2FP%3E%0A%3CPRE%3E%3CCODE%20class%3D%22language-bicep%22%20data-lang%3D%22bicep%22%3Eparam%20connections_office365_name%20string%20%3D%20'office365'%0Aparam%20workflows_Monitor_main_name%20string%20%3D%20'Monitor-LogicApp'%0Aparam%20userAssignedIdentities_Monitor_Identity_name%20string%20%3D%20'Monitor-ManagedIdentity'%0Aparam%20ResourceGroupName%20string%0Aparam%20resourceLocation%20string%0A%0A%0Amodule%20managedIdentityDeployment%20'Monitor-ManagedIdentity.bicep'%20%3D%20%7B%0A%20%20name%3A%20'managedIdentityDeployment'%0A%20%20params%3A%20%7B%0A%20%20%20%20userAssignedIdentities_Monitor_Identity_name%3A%20userAssignedIdentities_Monitor_Identity_name%0A%20%20%20%20resourceLocation%3A%20resourceLocation%0A%20%20%7D%0A%7D%0A%0Amodule%20connectionsDeployment%20'Monitor-connections.bicep'%20%3D%20%7B%0A%20%20name%3A%20'connectionsDeployment'%0A%20%20params%3A%20%7B%0A%20%20%20%20connections_office365_name%3A%20connections_office365_name%0A%20%20%20%20resourceLocation%3A%20resourceLocation%0A%20%20%20%20ResourceGroupName%3A%20ResourceGroupName%0A%0A%20%20%7D%0A%7D%0A%0Amodule%20MainDeployment%20'Monitor-main.bicep'%20%3D%20%7B%0A%20%20name%3A%20'MainDeployment'%0A%20%20params%3A%20%7B%0A%20%20%20%20resourceLocation%3A%20resourceLocation%0A%20%20%20%20userAssignedIdentities_Monitor_Identity_name%3A%20userAssignedIdentities_Monitor_Identity_name%0A%20%20%20%20workflows_Monitor_main_name%3A%20workflows_Monitor_main_name%0A%20%20%20%20connections_office365_name%3A%20connections_office365_name%0A%20%20%7D%0A%20%20dependsOn%3A%20%5B%0A%20%20%20%20connectionsDeployment%0A%20%20%20%20managedIdentityDeployment%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%20%0A%20%20%5D%0A%7D%0A%3C%2FCODE%3E%3C%2FPRE%3E%0A%3CP%3EI%20learned%20this%20one%20the%20hard%20way%3A%20Providing%20Infrastructure%20as%20Code%20(even%20if%20it%20is%20just%20for%20a%20small%20solution)%20is%20a%20faster%2C%20more%20sustainable%20and%20better%20way%20to%20deploy%20resources.%20It%20is%20painful%20how%20often%20GUIs%20change%20and%20results%20are%20not%20repeatable%20then.%20Also%2C%20it%20is%20more%20convenient%20to%20run%20a%20script%20rather%20than%20having%20to%20click%20yourself%20through%20a%20lengthy%20README%20file.%3CSPAN%3E%26nbsp%3B%3C%2FSPAN%3E%3CEM%3E(Been%20there%20as%20well)%3C%2FEM%3E%3C%2FP%3E%0A%3CP%3EThe%20script%20will%20create%20the%20resource%20group%20that%20holds%20the%20resources%20and%20assign%20the%20correct%20Microsoft%20Graph%20permission%20scope%20to%20the%20managed%20identity%3A%3C%2FP%3E%0A%3CP%3E%26nbsp%3B%3C%2FP%3E%0A%3CP%3E%3CEM%3EHere%20is%20the%20interesting%20part%20that%20assigns%20the%20permission%2C%20full%20script%20on%20GitHub%20%3Agreen_heart%3A%3A%3C%2FEM%3E%3C%2FP%3E%0A%3CPRE%3E%3CCODE%20class%3D%22language-%23%22%20data-lang%3D%22%23%22%3E%24ManagedIdentity%20%3D%20az%20identity%20show%20--name%20Monitor-ManagedIdentity%20--resource-group%20%24ResourceGroupName%20%7C%20ConvertFrom-Json%0A%0A%24principalId%20%3D%20%24ManagedIdentity.principalId%0A%23%20Get%20current%20role%20assignments%0A%24currentRoles%20%3D%20(az%20rest%20%60%0A%20%20%20%20--method%20get%20%60%0A%20%20%20%20--uri%20https%3A%2F%2Fgraph.microsoft.com%2Fv1.0%2FservicePrincipals%2F%24principalId%2FappRoleAssignments%20%60%0A%20%20%20%20%7C%20ConvertFrom-Json).value%20%60%0A%20%20%20%20%7C%20ForEach-Object%20%7B%20%24_.appRoleId%20%7D%0A%0A%24graphResourceId%20%3D%20az%20ad%20sp%20list%20--display-name%20%22Microsoft%20Graph%22%20--query%20%5B0%5D.objectId%0A%23Get%20appRoleIds%20for%20Organization.Read.All%0A%24graphId%20%3D%20az%20ad%20sp%20list%20--query%20%22%5B%3FappDisplayName%3D%3D'Microsoft%20Graph'%5D.appId%20%7C%20%5B0%5D%22%20--all%0A%24orgReadAll%20%3D%20az%20ad%20sp%20show%20--id%20%24graphId%20--query%20%22appRoles%5B%3Fvalue%3D%3D'Organization.Read.All'%5D.id%20%7C%20%5B0%5D%22%20-o%20tsv%0A%0A%24appRoleIds%20%3D%20%24orgReadAll%0A%23Loop%20over%20all%20appRoleIds%20-%20in%20case%20we%20later%20extend%20and%20need%20more%20than%20permission%0Aforeach%20(%24appRoleId%20in%20%24appRoleIds)%20%7B%0A%20%20%20%20%24roleMatch%20%3D%20%24currentRoles%20-match%20%24appRoleId%0A%20%20%20%20if%20(%24roleMatch.Length%20-eq%200)%20%7B%0A%20%20%20%20%20%20%20%20%23%20Add%20the%20role%20assignment%20to%20the%20principal%0A%20%20%20%20%20%20%20%20%24body%20%3D%20%22%7B'principalId'%3A'%24principalId'%2C'resourceId'%3A'%24graphResourceId'%2C'appRoleId'%3A'%24appRoleId'%7D%22%3B%0A%20%20%20%20%20%20%20%20az%20rest%20%60%0A%20%20%20%20%20%20%20%20%20%20%20%20--method%20post%20%60%0A%20%20%20%20%20%20%20%20%20%20%20%20--uri%20https%3A%2F%2Fgraph.microsoft.com%2Fv1.0%2FservicePrincipals%2F%24principalId%2FappRoleAssignments%20%60%0A%20%20%20%20%20%20%20%20%20%20%20%20--body%20%24body%20%60%0A%20%20%20%20%20%20%20%20%20%20%20%20--headers%20Content-Type%3Dapplication%2Fjson%20%0A%20%20%20%20%7D%0A%7D%0AWrite-Host%20%22%3Arocket%3A%3C%2Fimg%3E%20-Deployment%20completed%22%0A%3C%2FCODE%3E%3C%2FPRE%3E%0A%3CP%3E%3CEM%3E(I%20re-used%20the%20script%20that%20we%20use%20at%3CSPAN%3E%26nbsp%3B%3C%2FSPAN%3E%3CA%20href%3D%22https%3A%2F%2Fprovisiongenie.com%2F%22%20target%3D%22_blank%22%20rel%3D%22noopener%20nofollow%20noreferrer%22%3EProvisionGenie%3C%2FA%3E%3CSPAN%3E%26nbsp%3B%3C%2FSPAN%3E%F0%9F%A7%9E)%3C%2FEM%3E%3C%2FP%3E%0A%3CP%3E%26nbsp%3B%3C%2FP%3E%0A%3CP%3EAs%20a%20result%2C%20we%20can%20see%20the%20the%20assigned%20permission%20scope%20in%20Azure%20Active%20Directory%3C%2FP%3E%0A%3CP%3E(Navigate%20to%3CSPAN%3E%26nbsp%3B%3C%2FSPAN%3E%3CSTRONG%3EEnterprise%20Applications%3C%2FSTRONG%3E%2C%20then%20filter%20by%3CSPAN%3E%26nbsp%3B%3C%2FSPAN%3E%3CSTRONG%3EManaged%20Identities%3C%2FSTRONG%3E%2C%20select%20the%20created%20Managed%20Identity%20and%20select%3CSPAN%3E%26nbsp%3B%3C%2FSPAN%3E%3CSTRONG%3EPermissions%3C%2FSTRONG%3E)%3C%2FP%3E%0A%3CP%3E%26nbsp%3B%3C%2FP%3E%0A%3CP%3EI%20did%20a%20PR%20on%3CSPAN%3E%26nbsp%3B%3C%2FSPAN%3E%3CA%20href%3D%22https%3A%2F%2Fgithub.com%2FPeterKlapwijk%2FMicrosoft-Logic-Apps%2Ftree%2Fmain%2FMonitor%2520your%2520Microsoft%2520365%2520licenses%2520with%2520Logic%2520Apps%22%20target%3D%22_blank%22%20rel%3D%22noopener%20noreferrer%22%3EPeter%20Klapwijks%20repository%3C%2FA%3E%3CSPAN%3E%26nbsp%3B%3C%2FSPAN%3Eand%20added%20my%20approach%20there%20as%20well%20-%20communityrocks%20%3Asparkles%3A%3C%2FP%3E%0A%3CH2%20id%3D%22what-do-you-think%22%20id%3D%22toc-hId-1073009197%22%20id%3D%22toc-hId-1098637082%22%3EWhat%20do%20you%20think%3F%3C%2FH2%3E%0A%3CP%3EYou%20like%20it%3F%20Let%20me%20know!%20You%20don%E2%80%99t%20like%20it%3F%20Let%E2%80%99s%20talk!%3C%2FP%3E%0A%3CP%3E%3CIMG%20class%3D%22lia-deferred-image%20lia-image-emoji%22%20src%3D%22https%3A%2F%2Ftechcommunity.microsoft.com%2Fhtml%2F%40074E66BDDB70904A0880861ADCFF1005%2Femoticons%2F1f449_1f3fb.png%22%20alt%3D%22%3Abackhand_index_pointing_right%3A%22%20title%3D%22%3Abackhand_index_pointing_right%3A%22%20%2F%3E%3CSPAN%3E%26nbsp%3B%3C%2FSPAN%3E%3CA%20href%3D%22https%3A%2F%2Ftwitter.com%2FLuiseFreese%2Fstatus%2F1501474516756799488%3Fs%3D20%26amp%3Bt%3DI7zCQMgauvNsP3y8AZo36w%22%20target%3D%22_blank%22%20rel%3D%22noopener%20nofollow%20noreferrer%22%3EFind%20the%20discussion%20on%20twitter%3C%2FA%3E%3C%2FP%3E%3C%2FLINGO-BODY%3E%3CLINGO-TEASER%20id%3D%22lingo-teaser-3268194%22%20slang%3D%22en-US%22%3E%3CP%3E%3CSPAN%20class%3D%22lia-inline-image-display-wrapper%20lia-image-align-inline%22%20image-alt%3D%22supermario.jpg%22%20style%3D%22width%3A%20999px%3B%22%3E%3CIMG%20src%3D%22https%3A%2F%2Ftechcommunity.microsoft.com%2Ft5%2Fimage%2Fserverpage%2Fimage-id%2F358878i3815F7DD55D106E1%2Fimage-size%2Flarge%3Fv%3Dv2%26amp%3Bpx%3D999%22%20role%3D%22button%22%20title%3D%22supermario.jpg%22%20alt%3D%22supermario.jpg%22%20%2F%3E%3C%2FSPAN%3E%3CBR%20%2F%3EImproving%20open-source%20solutions%20is%20a%20great%20way%20to%20learn%20and%20share%3A%20Making%20good%20things%20even%20better!%3C%2FP%3E%3C%2FLINGO-TEASER%3E
Co-Authors
Version history
Last update:
‎Mar 27 2022 01:41 PM
Updated by: