{"id":143,"date":"2019-06-24T03:03:00","date_gmt":"2019-06-24T03:03:00","guid":{"rendered":"https:\/\/blog.devopsabcs.com\/?p=143"},"modified":"2023-09-23T03:11:24","modified_gmt":"2023-09-23T03:11:24","slug":"one-project-to-rule-them-all-3","status":"publish","type":"post","link":"https:\/\/blog-dev-001-bcdr.devopsabcs.com\/index.php\/2019\/06\/24\/one-project-to-rule-them-all-3\/","title":{"rendered":"One Project To Rule Them All"},"content":{"rendered":"\n<h3 class=\"wp-block-heading\">Part 3: Security<\/h3>\n\n\n\n<p>In <a href=\"https:\/\/blog.devopsabcs.com\/index.php\/2019\/06\/12\/one-project-to-rule-them-all\/\">Part 1 of the series<\/a> (first showcased on <a href=\"https:\/\/devblogs.microsoft.com\/premier-developer\/one-project-to-rule-them-all\/\">Microsoft&#8217;s Premier Developer Blog<\/a>), we discussed the business value of taking a single organization, single project approach in your Azure DevOps journey. In <a href=\"https:\/\/blog.devopsabcs.com\/index.php\/2019\/06\/20\/one-project-to-rule-them-all-2\/\">Part 2<\/a>, we covered the fundamental techniques on which the <strong>Migration Tool<\/strong> relies. In this post, we will talk about security considerations and techniques.<\/p>\n\n\n\n<p>Security Requirements can vary widely from one enterprise to another. Under certain circumstances, it may even be a reason to deviate from the &#8220;One Project To Rule Them All&#8221; strategy and <a href=\"https:\/\/docs.microsoft.com\/en-us\/azure\/devops\/organizations\/projects\/about-projects?view=azure-devops#when-to-add-another-project\">add another project<\/a>.<\/p>\n\n\n\n<p>That being said, Azure DevOps offers a <a href=\"https:\/\/docs.microsoft.com\/en-us\/azure\/devops\/organizations\/security\/about-security-identity?view=azure-devops\">high level of granularity and control<\/a> from the organization level all the way down to object-level permissions. For many enterprises, this is sufficient to adopt the &#8220;One Project To Rule Them All&#8221; approach.<\/p>\n\n\n\n<p>Security is the 4th stage of the <b>Migration Tool<\/b>. In this stage, we take a simplified approach: give the migrated project&#8217;s default team the <a href=\"https:\/\/docs.microsoft.com\/en-us\/azure\/devops\/organizations\/security\/permissions-access?view=azure-devops\">default Contributor permissions and access<\/a> to all of the project&#8217;s migrated items. Though simplified, the approach is complex enough to illustrate the main techniques in dealing with security in Azure DevOps. An enterprise can easily adopt these techniques to implement different, more complex, security configurations. Furthermore, a tailored structured approach is an opportunity to consolidate and standardize your security strategy in Azure DevOps.<\/p>\n\n\n\n<p>The heart of setting access control entries using the REST API is through the following <a href=\"https:\/\/docs.microsoft.com\/en-us\/rest\/api\/azure\/devops\/security\/access%20control%20entries\/set%20access%20control%20entries?view=azure-devops-rest-5.0\">HTTP POST call<\/a>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>POST https:\/\/dev.azure.com\/{organization}\/_apis\/accesscontrolentries\/{securityNamespaceId}?api-version=5.0<\/code><\/pre>\n\n\n\n<figure class=\"wp-block-image\"><a href=\"https:\/\/blog.devopsabcs.com\/wp-content\/uploads\/2019\/06\/Set-Access-Control-Entries.png\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"657\" src=\"https:\/\/blog.devopsabcs.com\/wp-content\/uploads\/2019\/06\/Set-Access-Control-Entries-1024x657.png\" alt=\"\" class=\"wp-image-153\" srcset=\"https:\/\/blog-dev-001-bcdr.devopsabcs.com\/wp-content\/uploads\/2019\/06\/Set-Access-Control-Entries-1024x657.png 1024w, https:\/\/blog-dev-001-bcdr.devopsabcs.com\/wp-content\/uploads\/2019\/06\/Set-Access-Control-Entries-300x193.png 300w, https:\/\/blog-dev-001-bcdr.devopsabcs.com\/wp-content\/uploads\/2019\/06\/Set-Access-Control-Entries-768x493.png 768w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/a><figcaption>Set Access Control Entries<\/figcaption><\/figure>\n\n\n\n<p>As with many POST calls, setting access control entries requires a <em>request body<\/em>. The Microsoft Documentation provides this example:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{\n  \"token\": \"newToken\",\n  \"merge\": true,\n  \"accessControlEntries\": [\n    {\n      \"descriptor\": \"Microsoft.TeamFoundation.Identity;S-1-9-1551374245-1204400969-2402986413-2179408616-0-0-0-0-2\",\n      \"allow\": 8,\n      \"deny\": 0,\n      \"extendedinfo\": {}\n    }\n  ]\n}<\/code><\/pre>\n\n\n\n<p>We will go over each detail, as each deserves special attention:<\/p>\n\n\n\n<ul class=\"wp-block-list\"><li>How does one obatin the <code>{securityNamespaceId}<\/code> of the POST call?<\/li><li>How to get the <code>token<\/code> in the request body?<\/li><li>How to get the <code>descriptor<\/code> in the request body? This question was asked in the <a href=\"https:\/\/developercommunity.visualstudio.com\/content\/problem\/229624\/identity-descriptor-security-api.html\">Visual Studio Developer Community<\/a>.<\/li><li>How to calculate the <code>allow<\/code> and <code>deny<\/code> integer values?<\/li><\/ul>\n\n\n\n<h4 class=\"wp-block-heading\">Security Namespaces<\/h4>\n\n\n\n<p>As the <a href=\"https:\/\/docs.microsoft.com\/en-us\/rest\/api\/azure\/devops\/security\/security%20namespaces?view=azure-devops-rest-5.0\">Microsoft Documentation<\/a> states:<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\"><p>Security namespaces are used to store&nbsp;<a href=\"https:\/\/docs.microsoft.com\/en-us\/rest\/api\/vsts\/security\/access%20control%20lists\/query?#accesscontrollist\">access control lists<\/a>&nbsp;(ACLs) on tokens. <\/p><\/blockquote>\n\n\n\n<p>One can obtain a <a href=\"https:\/\/docs.microsoft.com\/en-us\/rest\/api\/azure\/devops\/security\/security%20namespaces\/query?view=azure-devops-rest-5.0\">list of security namespaces<\/a> with the following HTTP GET call:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>https:\/\/dev.azure.com\/{organization}\/_apis\/securitynamespaces?api-version=5.0<\/code><\/pre>\n\n\n\n<p>Most likely, you will obatin something similar to this JSON element for the <em>Git Repositories<\/em> Security Namespace:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>        {\n            \"namespaceId\": \"2e9eb7ed-3c0a-47d4-87c1-0ffdd275fd87\",\n            \"name\": \"Git Repositories\",\n            \"displayName\": \"Git Repositories\",\n            \"separatorValue\": \"\/\",\n            \"elementLength\": -1,\n            \"writePermission\": 8192,\n            \"readPermission\": 2,\n            \"dataspaceCategory\": \"Git\",\n            \"actions\": [\n                {\n                    \"bit\": 1,\n                    \"name\": \"Administer\",\n                    \"displayName\": \"Administer\",\n                    \"namespaceId\": \"2e9eb7ed-3c0a-47d4-87c1-0ffdd275fd87\"\n                },\n                {\n                    \"bit\": 2,\n                    \"name\": \"GenericRead\",\n                    \"displayName\": \"Read\",\n                    \"namespaceId\": \"2e9eb7ed-3c0a-47d4-87c1-0ffdd275fd87\"\n                },\n                {\n                    \"bit\": 4,\n                    \"name\": \"GenericContribute\",\n                    \"displayName\": \"Contribute\",\n                    \"namespaceId\": \"2e9eb7ed-3c0a-47d4-87c1-0ffdd275fd87\"\n                },\n                {\n                    \"bit\": 8,\n                    \"name\": \"ForcePush\",\n                    \"displayName\": \"Force push (rewrite history, delete branches and tags)\",\n                    \"namespaceId\": \"2e9eb7ed-3c0a-47d4-87c1-0ffdd275fd87\"\n                },\n                {\n                    \"bit\": 16,\n                    \"name\": \"CreateBranch\",\n                    \"displayName\": \"Create branch\",\n                    \"namespaceId\": \"2e9eb7ed-3c0a-47d4-87c1-0ffdd275fd87\"\n                },\n                {\n                    \"bit\": 32,\n                    \"name\": \"CreateTag\",\n                    \"displayName\": \"Create tag\",\n                    \"namespaceId\": \"2e9eb7ed-3c0a-47d4-87c1-0ffdd275fd87\"\n                },\n                {\n                    \"bit\": 64,\n                    \"name\": \"ManageNote\",\n                    \"displayName\": \"Manage notes\",\n                    \"namespaceId\": \"2e9eb7ed-3c0a-47d4-87c1-0ffdd275fd87\"\n                },\n                {\n                    \"bit\": 128,\n                    \"name\": \"PolicyExempt\",\n                    \"displayName\": \"Bypass policies when pushing\",\n                    \"namespaceId\": \"2e9eb7ed-3c0a-47d4-87c1-0ffdd275fd87\"\n                },\n                {\n                    \"bit\": 256,\n                    \"name\": \"CreateRepository\",\n                    \"displayName\": \"Create repository\",\n                    \"namespaceId\": \"2e9eb7ed-3c0a-47d4-87c1-0ffdd275fd87\"\n                },\n                {\n                    \"bit\": 512,\n                    \"name\": \"DeleteRepository\",\n                    \"displayName\": \"Delete repository\",\n                    \"namespaceId\": \"2e9eb7ed-3c0a-47d4-87c1-0ffdd275fd87\"\n                },\n                {\n                    \"bit\": 1024,\n                    \"name\": \"RenameRepository\",\n                    \"displayName\": \"Rename repository\",\n                    \"namespaceId\": \"2e9eb7ed-3c0a-47d4-87c1-0ffdd275fd87\"\n                },\n                {\n                    \"bit\": 2048,\n                    \"name\": \"EditPolicies\",\n                    \"displayName\": \"Edit policies\",\n                    \"namespaceId\": \"2e9eb7ed-3c0a-47d4-87c1-0ffdd275fd87\"\n                },\n                {\n                    \"bit\": 4096,\n                    \"name\": \"RemoveOthersLocks\",\n                    \"displayName\": \"Remove others' locks\",\n                    \"namespaceId\": \"2e9eb7ed-3c0a-47d4-87c1-0ffdd275fd87\"\n                },\n                {\n                    \"bit\": 8192,\n                    \"name\": \"ManagePermissions\",\n                    \"displayName\": \"Manage permissions\",\n                    \"namespaceId\": \"2e9eb7ed-3c0a-47d4-87c1-0ffdd275fd87\"\n                },\n                {\n                    \"bit\": 16384,\n                    \"name\": \"PullRequestContribute\",\n                    \"displayName\": \"Contribute to pull requests\",\n                    \"namespaceId\": \"2e9eb7ed-3c0a-47d4-87c1-0ffdd275fd87\"\n                },\n                {\n                    \"bit\": 32768,\n                    \"name\": \"PullRequestBypassPolicy\",\n                    \"displayName\": \"Bypass policies when completing pull requests\",\n                    \"namespaceId\": \"2e9eb7ed-3c0a-47d4-87c1-0ffdd275fd87\"\n                }\n            ],\n            \"structureValue\": 1,\n            \"extensionType\": \"Microsoft.TeamFoundation.Git.Server.Plugins.GitSecurityNamespaceExtension\",\n            \"isRemotable\": true,\n            \"useTokenTranslator\": true,\n            \"systemBitMask\": 0\n        }<\/code><\/pre>\n\n\n\n<p>Observe that there are 16 bits required to fully set all possible combinations of permissions on actions. Indeed, 15 of the 16 actions can be seen in the following (only the <code>Administer<\/code> action is missing):<\/p>\n\n\n\n<figure class=\"wp-block-image\"><a href=\"https:\/\/blog.devopsabcs.com\/wp-content\/uploads\/2019\/06\/Git-Repositories-actions.png\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"657\" src=\"https:\/\/blog.devopsabcs.com\/wp-content\/uploads\/2019\/06\/Git-Repositories-actions-1024x657.png\" alt=\"\" class=\"wp-image-157\" srcset=\"https:\/\/blog-dev-001-bcdr.devopsabcs.com\/wp-content\/uploads\/2019\/06\/Git-Repositories-actions-1024x657.png 1024w, https:\/\/blog-dev-001-bcdr.devopsabcs.com\/wp-content\/uploads\/2019\/06\/Git-Repositories-actions-300x193.png 300w, https:\/\/blog-dev-001-bcdr.devopsabcs.com\/wp-content\/uploads\/2019\/06\/Git-Repositories-actions-768x493.png 768w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/a><figcaption>Git Repositories securable actions<\/figcaption><\/figure>\n\n\n\n<p>One small note, you may not see the <code>ReleaseManagement<\/code> Security Namespaces (no, there is no typo, you may see more than one!) in a newly created Azure DevOps organization. In order to see them, you may need to wake them up by actually creating a release definition or release.<\/p>\n\n\n\n<p>In order to obtain the integer values for <code>allow<\/code> or <code>deny<\/code>, we simply use the <a href=\"https:\/\/docs.microsoft.com\/en-us\/dotnet\/csharp\/programming-guide\/enumeration-types#enumeration-types-as-bit-flags\">enumeration types as bit flags<\/a>. From the above response, we can easily generate the following enumeration:<\/p>\n\n\n\n<p><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>    [System.Flags]\n    public enum GitRepositoriesFlags\n    {\n        Administer = 1,\n        GenericRead = 2,\n        GenericContribute = 4,\n        ForcePush = 8,\n        CreateBranch = 16,\n        CreateTag = 32,\n        ManageNote = 64,\n        PolicyExempt = 128,\n        CreateRepository = 256,\n        DeleteRepository = 512,\n        RenameRepository = 1024,\n        EditPolicies = 2048,\n        RemoveOthersLocks = 4096,\n        ManagePermissions = 8192,\n        PullRequestContribute = 16384,\n        PullRequestBypassPolicy = 32768,\n    }<\/code><\/pre>\n\n\n\n<p>So if you&#8217;d like to secure an individual repository with Contributor permissions as in this screenshot:<\/p>\n\n\n\n<figure class=\"wp-block-image\"><a href=\"https:\/\/blog.devopsabcs.com\/wp-content\/uploads\/2019\/06\/Calculating-Allow-value.png\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"657\" src=\"https:\/\/blog.devopsabcs.com\/wp-content\/uploads\/2019\/06\/Calculating-Allow-value-1024x657.png\" alt=\"\" class=\"wp-image-161\" srcset=\"https:\/\/blog-dev-001-bcdr.devopsabcs.com\/wp-content\/uploads\/2019\/06\/Calculating-Allow-value-1024x657.png 1024w, https:\/\/blog-dev-001-bcdr.devopsabcs.com\/wp-content\/uploads\/2019\/06\/Calculating-Allow-value-300x193.png 300w, https:\/\/blog-dev-001-bcdr.devopsabcs.com\/wp-content\/uploads\/2019\/06\/Calculating-Allow-value-768x493.png 768w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/a><figcaption>Figure 1<\/figcaption><\/figure>\n\n\n\n<p>In code, this translates to:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>            int expected = 16502;\n            var actualValue =\n                GitRepositoriesFlags.GenericContribute |\n                GitRepositoriesFlags.PullRequestContribute |\n                GitRepositoriesFlags.CreateBranch |\n                GitRepositoriesFlags.CreateTag |\n                GitRepositoriesFlags.ManageNote |\n                GitRepositoriesFlags.GenericRead;\n\n            int actual = (int)actualValue;\n            Assert.AreEqual(expected, actual);<\/code><\/pre>\n\n\n\n<p>The magic number for the <code>allow<\/code> value is <code>16502<\/code>. If all we were looking for is the Contributor permissions, then we could have just as easily obtained the value by the following <a href=\"https:\/\/docs.microsoft.com\/en-us\/rest\/api\/azure\/devops\/security\/access%20control%20lists\/query?view=azure-devops-rest-5.0\">GET call<\/a>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>https:\/\/dev.azure.com\/agileatscale\/_apis\/accesscontrollists\/2e9eb7ed-3c0a-47d4-87c1-0ffdd275fd87?api-version=5.0<\/code><\/pre>\n\n\n\n<p>Unfortunately, this gives me 1289 results, as I have this many access control lists in my <code>agileatscale<\/code> organization! I need to filter the above query with optional parameters:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>GET https:\/\/dev.azure.com\/{organization}\/_apis\/accesscontrollists\/{securityNamespaceId}?token={token}&amp;descriptors={descriptors}&amp;api-version=5.0<\/code><\/pre>\n\n\n\n<p>In order to do so, we must answer two more questions we had set out to solve:<\/p>\n\n\n\n<ul class=\"wp-block-list\"><li>How to get the <code>token<\/code> in the request body?<\/li><li>How to get the <code>descriptor<\/code> in the request body?<\/li><\/ul>\n\n\n\n<p>Getting the token is highly dependent on the resource you are trying to secure. For Git Repository Tokens, I found this <a href=\"https:\/\/devblogs.microsoft.com\/devops\/git-repo-tokens-for-the-security-service\/\">post<\/a> useful by <a href=\"https:\/\/devblogs.microsoft.com\/devops\/author\/macoopemicrosoft-com\/\">Matt Cooper<\/a>. Note that we have the repositoryId <code>0f22acb2-4c10-4e79-84d3-69dd0d798412<\/code> from the URL of Figure 1. The following GET call:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>https:\/\/dev.azure.com\/agileatscale\/oneproject07\/_apis\/git\/repositories\/0f22acb2-4c10-4e79-84d3-69dd0d798412?api-version=5.0<\/code><\/pre>\n\n\n\n<p>yields this JSON response:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{\n    \"id\": \"0f22acb2-4c10-4e79-84d3-69dd0d798412\",\n    \"name\": \"AS_PS.MAD.D4AS\",\n    \"url\": \"https:\/\/dev.azure.com\/agileatscale\/fe374bc1-e0ad-4ed9-a35e-d8d1564e554e\/_apis\/git\/repositories\/0f22acb2-4c10-4e79-84d3-69dd0d798412\",\n    \"project\": {\n        \"id\": \"fe374bc1-e0ad-4ed9-a35e-d8d1564e554e\",\n        \"name\": \"OneProject07\",\n        \"description\": \"full regression cmd line\",\n        \"url\": \"https:\/\/dev.azure.com\/agileatscale\/_apis\/projects\/fe374bc1-e0ad-4ed9-a35e-d8d1564e554e\",\n        \"state\": \"wellFormed\",\n        \"revision\": 2493,\n        \"visibility\": \"private\",\n        \"lastUpdateTime\": \"2019-06-09T22:44:22.407Z\"\n    },\n    \"defaultBranch\": \"refs\/heads\/master\",\n    \"size\": 101250,\n    \"remoteUrl\": \"https:\/\/agileatscale@dev.azure.com\/agileatscale\/OneProject07\/_git\/AS_PS.MAD.D4AS\",\n    \"sshUrl\": \"git@ssh.dev.azure.com:v3\/agileatscale\/OneProject07\/AS_PS.MAD.D4AS\",\n    \"webUrl\": \"https:\/\/dev.azure.com\/agileatscale\/OneProject07\/_git\/AS_PS.MAD.D4AS\",\n    \"_links\": {\n        \"self\": {\n            \"href\": \"https:\/\/dev.azure.com\/agileatscale\/fe374bc1-e0ad-4ed9-a35e-d8d1564e554e\/_apis\/git\/repositories\/0f22acb2-4c10-4e79-84d3-69dd0d798412\"\n        },\n        \"project\": {\n            \"href\": \"vstfs:\/\/\/Classification\/TeamProject\/fe374bc1-e0ad-4ed9-a35e-d8d1564e554e\"\n        },\n        \"web\": {\n            \"href\": \"https:\/\/dev.azure.com\/agileatscale\/OneProject07\/_git\/AS_PS.MAD.D4AS\"\n        },\n        \"ssh\": {\n            \"href\": \"git@ssh.dev.azure.com:v3\/agileatscale\/OneProject07\/AS_PS.MAD.D4AS\"\n        },\n        \"commits\": {\n            \"href\": \"https:\/\/dev.azure.com\/agileatscale\/fe374bc1-e0ad-4ed9-a35e-d8d1564e554e\/_apis\/git\/repositories\/0f22acb2-4c10-4e79-84d3-69dd0d798412\/commits\"\n        },\n        \"refs\": {\n            \"href\": \"https:\/\/dev.azure.com\/agileatscale\/fe374bc1-e0ad-4ed9-a35e-d8d1564e554e\/_apis\/git\/repositories\/0f22acb2-4c10-4e79-84d3-69dd0d798412\/refs\"\n        },\n        \"pullRequests\": {\n            \"href\": \"https:\/\/dev.azure.com\/agileatscale\/fe374bc1-e0ad-4ed9-a35e-d8d1564e554e\/_apis\/git\/repositories\/0f22acb2-4c10-4e79-84d3-69dd0d798412\/pullRequests\"\n        },\n        \"items\": {\n            \"href\": \"https:\/\/dev.azure.com\/agileatscale\/fe374bc1-e0ad-4ed9-a35e-d8d1564e554e\/_apis\/git\/repositories\/0f22acb2-4c10-4e79-84d3-69dd0d798412\/items\"\n        },\n        \"pushes\": {\n            \"href\": \"https:\/\/dev.azure.com\/agileatscale\/fe374bc1-e0ad-4ed9-a35e-d8d1564e554e\/_apis\/git\/repositories\/0f22acb2-4c10-4e79-84d3-69dd0d798412\/pushes\"\n        }\n    }\n}<\/code><\/pre>\n\n\n\n<p>So the token we are looking for is:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>repoV2\/fe374bc1-e0ad-4ed9-a35e-d8d1564e554e\/0f22acb2-4c10-4e79-84d3-69dd0d798412<\/code><\/pre>\n\n\n\n<p>So if we run the following query with the above token:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>https:\/\/dev.azure.com\/agileatscale\/_apis\/accesscontrollists\/2e9eb7ed-3c0a-47d4-87c1-0ffdd275fd87?token=repoV2\/fe374bc1-e0ad-4ed9-a35e-d8d1564e554e\/0f22acb2-4c10-4e79-84d3-69dd0d798412&amp;api-version=5.0<\/code><\/pre>\n\n\n\n<p>we get the JSON response:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{\n    \"count\": 1,\n    \"value\": [\n        {\n            \"inheritPermissions\": true,\n            \"token\": \"repoV2\/fe374bc1-e0ad-4ed9-a35e-d8d1564e554e\/0f22acb2-4c10-4e79-84d3-69dd0d798412\",\n            \"acesDictionary\": {\n                \"Microsoft.IdentityModel.Claims.ClaimsIdentity;a34c69c7-8959-474a-9690-e98bfb0b55c6\\\\emmanuel@devopsabcs.com\": {\n                    \"descriptor\": \"Microsoft.IdentityModel.Claims.ClaimsIdentity;a34c69c7-8959-474a-9690-e98bfb0b55c6\\\\emmanuel@devopsabcs.com\",\n                    \"allow\": 32382,\n                    \"deny\": 0\n                },\n                \"Microsoft.TeamFoundation.Identity;S-1-9-1551374245-3242932222-2917194062-2740902097-1447974222-1-3578883301-4072410959-2197538308-2551652260\": {\n                    \"descriptor\": \"Microsoft.TeamFoundation.Identity;S-1-9-1551374245-3242932222-2917194062-2740902097-1447974222-1-3578883301-4072410959-2197538308-2551652260\",\n                    \"allow\": 16502,\n                    \"deny\": 0\n                }\n            }\n        }\n    ]\n}<\/code><\/pre>\n\n\n\n<p>We do see the value <code>16502<\/code> but how can we be sure of the SID? To get the descriptor of the group we are trying to attribute permissions to, we must obtain its descriptor.<\/p>\n\n\n\n<p>We do this by fetching all groups using the following <a href=\"https:\/\/docs.microsoft.com\/en-us\/rest\/api\/azure\/devops\/graph\/groups\/list?view=azure-devops-rest-5.0\">GET call<\/a>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>GET https:\/\/vssps.dev.azure.com\/{organization}\/_apis\/graph\/groups?api-version=5.0-preview.1<\/code><\/pre>\n\n\n\n<p>If you have many groups, you may only get 500, in which case, you will need to use a continuation token to fetch the next page of results. Look for the continuation token in the response headers:<\/p>\n\n\n\n<figure class=\"wp-block-image\"><a href=\"https:\/\/blog.devopsabcs.com\/wp-content\/uploads\/2019\/06\/continuation-token.png\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"658\" src=\"https:\/\/blog.devopsabcs.com\/wp-content\/uploads\/2019\/06\/continuation-token-1024x658.png\" alt=\"\" class=\"wp-image-166\" srcset=\"https:\/\/blog-dev-001-bcdr.devopsabcs.com\/wp-content\/uploads\/2019\/06\/continuation-token-1024x658.png 1024w, https:\/\/blog-dev-001-bcdr.devopsabcs.com\/wp-content\/uploads\/2019\/06\/continuation-token-300x193.png 300w, https:\/\/blog-dev-001-bcdr.devopsabcs.com\/wp-content\/uploads\/2019\/06\/continuation-token-768x494.png 768w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/a><figcaption>Continuation token while fetching all groups<\/figcaption><\/figure>\n\n\n\n<p>The continuation token I got was:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>eyJTY29wZUlkIjoiNmQxMDQ3NTEtNWZhNi00ZDY5LTlhMTEtNWFiOWMxYjU3OTM2IiwiUGFnZVNpemUiOjUwMCwiSW5jbHVkZUdyb3VwcyI6dHJ1ZSwiSW5jbHVkZU5vbkdyb3VwcyI6ZmFsc2UsIlBhZ2VuYXRpb25Ub2tlbiI6ImNlZWYwZWRiLWJmNzQtNDc3Yi05NzA4LTdlM2MyNDY2ZDQ1OCJ9<\/code><\/pre>\n\n\n\n<p>which when <a href=\"https:\/\/www.base64decode.org\/\">decoded from base64<\/a> yields:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{\n\t\"ScopeId\": \"6d104751-5fa6-4d69-9a11-5ab9c1b57936\",\n\t\"PageSize\": 500,\n\t\"IncludeGroups\": true,\n\t\"IncludeNonGroups\": false,\n\t\"PagenationToken\": \"ceef0edb-bf74-477b-9708-7e3c2466d458\"\n}<\/code><\/pre>\n\n\n\n<p>Observe that the <code>PagenationToken<\/code> is the originId of the last group fetched in the first 500 results.<\/p>\n\n\n\n<p>However, this discussion is about Security, so if you are paranoid like me and think that this generous website may be recording your sensitive strings, then here are two quick methods that will allow you to achieve the same from the comfort of your trusted PC:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>public static string Base64Decode(string base64EncodedData)\n        {\n            var lengthMod4 = base64EncodedData.Length % 4;\n            if (lengthMod4 != 0)\n            {\n                \/\/fix Invalid length for a Base-64 char array or string\n                base64EncodedData += new string('=', 4 - lengthMod4);\n            }\n            var base64EncodedBytes = System.Convert.FromBase64String(base64EncodedData);\n            return System.Text.Encoding.UTF8.GetString(base64EncodedBytes);\n        }\n\npublic static string Base64Encode(string plainText)\n        {\n            var plainTextBytes = System.Text.Encoding.UTF8.GetBytes(plainText);\n            return System.Convert.ToBase64String(plainTextBytes);\n        }<\/code><\/pre>\n\n\n\n<p>I did find the group in question, namely, <em>[OneProject07]\\AS_Autoscaling Team<\/em> in the second page of results:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>        {\n            \"subjectKind\": \"group\",\n            \"description\": \"The default project team.\",\n            \"domain\": \"vstfs:\/\/\/Classification\/TeamProject\/fe374bc1-e0ad-4ed9-a35e-d8d1564e554e\",\n            \"principalName\": \"[OneProject07]\\\\AS_Autoscaling Team\",\n            \"mailAddress\": null,\n            \"origin\": \"vsts\",\n            \"originId\": \"06f50375-1236-43ef-8294-be7f3d739e6c\",\n            \"displayName\": \"AS_Autoscaling Team\",\n            \"_links\": {\n                \"self\": {\n                    \"href\": \"https:\/\/vssps.dev.azure.com\/agileatscale\/_apis\/Graph\/Groups\/vssgp.Uy0xLTktMTU1MTM3NDI0NS0zMjQyOTMyMjIyLTI5MTcxOTQwNjItMjc0MDkwMjA5Ny0xNDQ3OTc0MjIyLTEtMzU3ODg4MzMwMS00MDcyNDEwOTU5LTIxOTc1MzgzMDgtMjU1MTY1MjI2MA\"\n                },\n                \"memberships\": {\n                    \"href\": \"https:\/\/vssps.dev.azure.com\/agileatscale\/_apis\/Graph\/Memberships\/vssgp.Uy0xLTktMTU1MTM3NDI0NS0zMjQyOTMyMjIyLTI5MTcxOTQwNjItMjc0MDkwMjA5Ny0xNDQ3OTc0MjIyLTEtMzU3ODg4MzMwMS00MDcyNDEwOTU5LTIxOTc1MzgzMDgtMjU1MTY1MjI2MA\"\n                },\n                \"membershipState\": {\n                    \"href\": \"https:\/\/vssps.dev.azure.com\/agileatscale\/_apis\/Graph\/MembershipStates\/vssgp.Uy0xLTktMTU1MTM3NDI0NS0zMjQyOTMyMjIyLTI5MTcxOTQwNjItMjc0MDkwMjA5Ny0xNDQ3OTc0MjIyLTEtMzU3ODg4MzMwMS00MDcyNDEwOTU5LTIxOTc1MzgzMDgtMjU1MTY1MjI2MA\"\n                },\n                \"storageKey\": {\n                    \"href\": \"https:\/\/vssps.dev.azure.com\/agileatscale\/_apis\/Graph\/StorageKeys\/vssgp.Uy0xLTktMTU1MTM3NDI0NS0zMjQyOTMyMjIyLTI5MTcxOTQwNjItMjc0MDkwMjA5Ny0xNDQ3OTc0MjIyLTEtMzU3ODg4MzMwMS00MDcyNDEwOTU5LTIxOTc1MzgzMDgtMjU1MTY1MjI2MA\"\n                }\n            },\n            \"url\": \"https:\/\/vssps.dev.azure.com\/agileatscale\/_apis\/Graph\/Groups\/vssgp.Uy0xLTktMTU1MTM3NDI0NS0zMjQyOTMyMjIyLTI5MTcxOTQwNjItMjc0MDkwMjA5Ny0xNDQ3OTc0MjIyLTEtMzU3ODg4MzMwMS00MDcyNDEwOTU5LTIxOTc1MzgzMDgtMjU1MTY1MjI2MA\",\n            \"descriptor\": \"vssgp.Uy0xLTktMTU1MTM3NDI0NS0zMjQyOTMyMjIyLTI5MTcxOTQwNjItMjc0MDkwMjA5Ny0xNDQ3OTc0MjIyLTEtMzU3ODg4MzMwMS00MDcyNDEwOTU5LTIxOTc1MzgzMDgtMjU1MTY1MjI2MA\"\n        }<\/code><\/pre>\n\n\n\n<p>Now if we decode the descriptor using our trusted C# code:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Uy0xLTktMTU1MTM3NDI0NS0zMjQyOTMyMjIyLTI5MTcxOTQwNjItMjc0MDkwMjA5Ny0xNDQ3OTc0MjIyLTEtMzU3ODg4MzMwMS00MDcyNDEwOTU5LTIxOTc1MzgzMDgtMjU1MTY1MjI2MA<\/code><\/pre>\n\n\n\n<p>we get the SID:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>S-1-9-1551374245-3242932222-2917194062-2740902097-1447974222-1-3578883301-4072410959-2197538308-2551652260<\/code><\/pre>\n\n\n\n<p>This is great! It confirms that we had the right SID in the first place.<\/p>\n\n\n\n<p>Finally, we&#8217;d like to mention one more thing. While we have tested the migration tool with Azure DevOps Services migration, with a little bit more effort, the tool can be made to work on-premise e.g. with <a href=\"https:\/\/azure.microsoft.com\/en-us\/services\/devops\/server\/\">Azure DevOps Server 2019<\/a>. This is because the on-premise version also supports the REST API calls. The only caveat is that we must use the <a href=\"https:\/\/docs.microsoft.com\/en-us\/azure\/devops\/server\/command-line\/tfssecurity-cmd?view=azure-devops\">TFSSecurity.exe tool<\/a> instead of the REST API for security related functionality. That being said, many of the lessons learned here can be carried over with the TFSSecurity.exe tool such as obtaining the security tokens.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Additional Exploration<\/h3>\n\n\n\n<ul class=\"wp-block-list\"><li><a href=\"https:\/\/docs.microsoft.com\/en-us\/rest\/api\/azure\/devops\/?view=azure-devops-rest-5.0\">Azure DevOps Services REST API Reference<\/a><\/li><li><a href=\"https:\/\/docs.microsoft.com\/en-us\/azure\/devops\/server\/command-line\/tfssecurity-cmd?view=azure-devops\">Use TFSSecurity to manage groups and permissions for Azure DevOps<\/a><\/li><\/ul>\n","protected":false},"excerpt":{"rendered":"<p>Part 3: Security In Part 1 of the series (first showcased on Microsoft&#8217;s Premier Developer Blog), we discussed the business value of taking a single organization, single project approach in your Azure DevOps journey. In Part 2, we covered the fundamental techniques on which the Migration Tool relies. In this post, we will talk about<\/p>\n<p><a class=\"readmore\" href=\"https:\/\/blog-dev-001-bcdr.devopsabcs.com\/index.php\/2019\/06\/24\/one-project-to-rule-them-all-3\/\"><span class=\"arrow-right icon\"><\/span>Read More<\/a><\/p>\n","protected":false},"author":1,"featured_media":161,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[4,7,13],"tags":[12,10,8,11,9,14],"class_list":["post-143","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-devops","category-project-management","category-security","tag-agile-at-scale","tag-azure-devops","tag-devops","tag-migrator-tool","tag-project-management","tag-security"],"_links":{"self":[{"href":"https:\/\/blog-dev-001-bcdr.devopsabcs.com\/index.php\/wp-json\/wp\/v2\/posts\/143","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/blog-dev-001-bcdr.devopsabcs.com\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/blog-dev-001-bcdr.devopsabcs.com\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/blog-dev-001-bcdr.devopsabcs.com\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/blog-dev-001-bcdr.devopsabcs.com\/index.php\/wp-json\/wp\/v2\/comments?post=143"}],"version-history":[{"count":30,"href":"https:\/\/blog-dev-001-bcdr.devopsabcs.com\/index.php\/wp-json\/wp\/v2\/posts\/143\/revisions"}],"predecessor-version":[{"id":413,"href":"https:\/\/blog-dev-001-bcdr.devopsabcs.com\/index.php\/wp-json\/wp\/v2\/posts\/143\/revisions\/413"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/blog-dev-001-bcdr.devopsabcs.com\/index.php\/wp-json\/wp\/v2\/media\/161"}],"wp:attachment":[{"href":"https:\/\/blog-dev-001-bcdr.devopsabcs.com\/index.php\/wp-json\/wp\/v2\/media?parent=143"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/blog-dev-001-bcdr.devopsabcs.com\/index.php\/wp-json\/wp\/v2\/categories?post=143"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/blog-dev-001-bcdr.devopsabcs.com\/index.php\/wp-json\/wp\/v2\/tags?post=143"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}