I, Brian, have been at Microsoft a very long time. How long? When I joined Microsoft straight out of graduate school, how I remember things, it was a time when the Mac division lead the way in revenue, we also had the Office products for the Mac, we wrote Microsoft Mail for Mac, and I used an Unix email system at work which I remember was one of our email products at the time, and I did my debugging over a serial port. Today, I feel like we are the Microsoft I initially joined; we write software and we don’t care where it runs. Software is our forte.
Today, I have had the great fortune of working with someone that was not raised on the Microsoft stack as I have been, and it has been inspiring and invigorating sharing our knowledge of different languages and platforms.
Allow me to introduce Sidi Merzouk, one of our newest members of Premier Developer. See the following link on Forbes to get an introduction and a sense of Sidi’s developer vigor. Sidi comes with strengths in languages and platforms that is not customary to find in a Microsoft stack developer and has supercharged me with his talents; for example, the node.js code project below, Sidi wrote this code with input from me.
You can find the full REST API Reference at https://docs.microsoft.com/en-us/rest/api/azure/devops/?view=azure-devops-rest-5.0 used in the sample solution.
Sidi and I had a challenge of pulling/getting permissions of an Azure DevOps Organization programmatically, but we managed to get something going. Thus, we decided to share our findings with you in this blog post.
The following sample can be download from our repo in GitHub using the following link https://github.com/PremierDeveloper/Azure-DevOps. You will need the code to go along with this post.
You will need npm which is distributed with Node.js. So, when you download Node.js, you automatically get npm installed on your computer. After downloading, check that you have node and npm installed by running this command in your shell: node -v. If you have Visual Studio installed, you will have Node.exe but it may not be on your path.
Once you have the project downloaded or cloned, confirmed that Node is installed by navigating to the project directory and run npm install to install the needed dependencies; in this case we will be installing the request library and azure-devops-node-api library.
To begin, you will need to create a personal token from the Azure DevOps dashboard portal as seen in figures 1 and 2.
Figure 1: Navigate to Security
Figure 2: Create new token
Edit the index.js file in the project directory; you will be inserting the personal token you just created and your Azure DevOps services organization URL and saving your file. The URL should look like the this: https://dev.azure.com/YOURORGNAME as in the following figure.
Figure 3: Azure DevOps Services organization URL.
const request = require('request'); const vsoNodeApi = require('azure-devops-node-api'); //Insert the newly created personal token below const token = "<INSERT TOKEN>"; // Url to your organization const serverUrl = '<INSERT REPO URL>'; let authHandler = vsoNodeApi.getPersonalAccessTokenHandler(token); let AzDO = new vsoNodeApi.WebApi(serverUrl, authHandler, undefined);
Before we can run our script, we will need to do one last thing which is replacing this line with the actual personal token and URL that points to your Azure DevOps Organization.
const url = `https://<INSERT TOKEN>@<INSERT URL>/${projectId}/_api/_identity/Display?__v=5&tfid=${teamId}`
E.g.:
- let’s say your token is the following string “jdfnjdngfjn238fbeifbisdnksknjfdf12”
- Your organization URL is the following dev.azure.com/simerzou0646
The result should look something like this:
const url = `https:// jdfnjdngfjn238fbeifbisdnksknjfdf12@ dev.azure.com/simerzou0646/${projectId}/_api/_identity/Display?__v=5&tfid=${teamId}`
Now we can safely open the terminal navigate to the folder and run node index.js
The result would look something like this:
Permissions: { "identity": { "IdentityType": "team", "FriendlyDisplayName": "RayProject Team", "DisplayName": "[RayProject]\RayProject Team", "SubHeader": "[RayProject]", "TeamFoundationId": "73544b31-8d7d-47c0-a474-f0bd7db7c5b2", "EntityId": "vss.ds.v1.ims.group.73544b318d7d47c0a474f0bd7db7c5b2", "Errors": [], "Warnings": [], "IsWindowsGroup": false, "IsAadGroup": false, "Description": "The default project team.", "Scope": "RayProject", "MemberCountText": "1", "IsTeam": true, "IsProjectLevel": true } } Permissions: { "identity": { "IdentityType": "team", "FriendlyDisplayName": "Bing Team", "DisplayName": "[Bing]\Bing Team", "SubHeader": "[Bing]", "TeamFoundationId": "432f4d89-46aa-4072-84e9-6ccc59c4a3f3", "EntityId": "vss.ds.v1.ims.group.432f4d8946aa407284e96ccc59c4a3f3", "Errors": [], "Warnings": [], "IsWindowsGroup": false, "IsAadGroup": false, "Description": "The default project team.", "Scope": "Bing", "MemberCountText": "2", "IsTeam": true, "IsProjectLevel": true } } Org: { "RayProject": { "RayProject Team": "Sidi Merzouk" }, "Bing": { "Bing Team": "Kevin Kraus, Sidi Merzouk" } }
Et Voila!
For those of you who want to know what’s happening let me give you a quick walkthrough of what’s happening in the index.js file.
There three major components to the code:
- First, JavaScript is async by default and when we look closely at the code in index.js, you’d find that we are making multiple http request using the azure-devops-node-api library. As you might have picked up that could be a challenge because what if our for loop ends before we get the response back from the server (which is more than likely to be the case), the result will be an empty object. Thus, I had to create a custom for loop function (asyncForEach()) that takes two arguments an array and callback, and we are using async await keyword to halt the code and wait to get the response from our server.
async function asyncForEach(array, callback) { for (let index = 0; index < array.length; index++) { await callback(array[index], index, array); } }
- Second, Our run() function simply calls Azure DevOps library and get the teams for our org. then we loop through it construct a temporary object called teamsToUse and we pass that our constructorTeams() function.
async function run() { var constructedTeams = {} try { var coreApi = await AzDO.getCoreApi(); var teams = await coreApi.getAllTeams(); var i; for (i = 0; i < teams.length; i++) { const team = teams[i] const obj = { url: team['url'], projectId: team['projectId'], teamName: team['name'], teamId: team['id'] } if (!constructedTeams[team['projectName']]){ constructedTeams[team['projectName']] = [obj] } else { constructedTeams[team['projectName']].push(obj) } } const teamsToUse = await constructedTeams const finalConstruct = await constructTeams(teamsToUse) return finalConstruct } catch(err) { console.log(`err ${JSON.stringify(err, null, 2)}`) } }
- Lastly, contructorTeams() function is first taking the object that was passed to it (teamsToUse) turning it into an array of keys and looping through it. In the for loop we are using the REST API to get the permission that a given group has. Once we get the response, we will either create a new object or append the permissions to the existing object. Last but not least, when the for loop ends, we then return the final object called obj.
const constructTeams = async (teams) => { const obj = {} var coreApi = await AzDO.getCoreApi(); const ids = Object.keys(teams) await asyncForEach(ids, async (key) => { await asyncForEach(teams[key], async (el) => { const temp = {} const {projectId, teamId} = el const url = `https://<INSERT TOKEN>@<INSERT URL>/${projectId}/_api/_identity/Display?__v=5&tfid=${teamId}` request.get(url , function(error, response, body) { const parsedBody = JSON.parse(body) if (parsedBody && parsedBody['security'] && parsedBody['security']['permissions']) { delete parsedBody['security'] console.log(`Permissions: ${JSON.stringify(parsedBody, null, 2)}`) } else { console.log(`not found`) } } ); const members = await coreApi.getTeamMembersWithExtendedProperties(el.projectId, el.teamId); const NameMembers = await members.map(member => member.identity.displayName).join(', ') temp[el.teamName] = NameMembers if (!obj[key] || Object.keys(obj[key]).length == 0) { obj[key] = temp } else { Object.assign(obj[key], temp) } }) }) console.log(`Org: ${JSON.stringify(obj, null, 2)}`) return obj }
With that we’ve concluded our little tour that we’ve put together for you. We hope that you’ve enjoyed reading it as much as we’ve enjoyed putting it together. If you have any feedback, questions, comments or suggestions please share your thoughts with us.
Happy Hacking!
Is this project still valid after almost a year? lol.
Using our pat token that has api access, the call to getCoreApi fails with:
fetching core api
err {
“statusCode”: 400
}
Great tutorial, excellent resource to get a grasp of the azure devops api. However, there is a problem with you code.
constructTeams() function line is incorrect and will not work:
const url = `https://@/${projectId}/_api/_identity/Display?__v=5&tfid=${teamId}`
is wrong, there is no teamId or projectId context in constructTeams(), you need to replace with:
const url = 'https://@/'+el['projectId']+'/_api/_identity/Display?__v=5&tfid='+el['teamId']
*Edit*
Im not sure why, im running Node 12, but const {projectId, teamId} = el doesn't seem to work in my environment, and I...
Really great tutorial, i’m learning nodeJs and this is a great example to get me going with web requests and apis. Was getting 401 auth error but gave myself full api access and now all works great!