Friday, April 27, 2018

Who am I and what are my rights (in TFS)?

Some time ago, we've discussed OAuth in TFS. Recently, I've been attacking a related problem - we have a valid OAuth-based connection (a VssConnection object), it can invoke some REST endpoints via OAuth, but which ones? Turns out, the allowed scopes are stored within an OAuth token, it just takes a bit of parsing to retrieve. The following line on an ASPX page returns the scopes for a token:

string Scopes = new System.IdentityModel.Tokens.JwtSecurityToken(Token).Payload["scp"];

There's a sample gist for that.



This came up as I was solving a bigger problem - what exactly is the security context that custom build/release tasks get? When you have a custom Powershell task (or a script in the built-in Powershell task), it gets a global variable called distributedTaskContext. I don't think it's documented anywhere, but you can find references to it in Microsoft's own examples.

Anyway, one can use that variable to call TFS back. You can instantiate .NET client objects and invoke all kinds of API - release, source control, you name it. But the security context is clearly not that of the current service account. Looking at the HTTP traffic, one can see that the HTTP requests back to TFS go with an Authorization: Bearer header with a token. The token, it turns out, can be easily retrieved from the distributedTaskContext:

$Token = $distributedTaskContext.GetEndpoint("SystemVssConnection").Authorization.Parameters.AccessToken

But what is the identity behind the token? Turns out, there's a REST endpoint for that, and invoking it with the token gives you the current user:

$wc = New-Object System.Net.WebClient
$wc.Headers["Authorization"] = "Bearer " + $Token
$wc.DownloadString("http://tfs.example.com:8080/tfs/_api/_common/GetUserProfile?__v=5")

Still, an OAuth client has two parts to its security context: the user, and the scope(s). For the scopes, there's no built-in endpoint that I could find, so I put together my own, see above.

For the record, the user behind the token is an artificial one. The identity record goes:

"IdentityType": "user",
"FriendlyDisplayName": "Project Collection Build Service (TEAM FOUNDATION)",
"DisplayName": "Project Collection Build Service (TEAM FOUNDATION)",
"SubHeader": "Build\\233e4ccc-d129-4ba4-9c5b-ea82c7ae1d15",
"TeamFoundationId": "7a3195ee-870e-4151-ba58-1e522732086c",
"EntityId": "vss.ds.v1.ims.user.7a3195ee870e4151ba581e522732086c",
"Errors": [],
"Warnings": [],
"Domain": "Build",
"AccountName": "233e4ccc-d129-4ba4-9c5b-ea82c7ae1d15",
"IsWindowsUser": false,
"MailAddress": ""

There's one user like this in every team collection. It belongs to a server-level group called "Security Service Group", and also to a collection level group with the same name. The security editing window of TFS, however, has no problem locating this user.

The scope on the token is "app_token". That is a valid scope, and it allows access to all endpoints and all methods in TFS.

Late edit: there is another technique for retrieving the identity behind the token.


This, in turn, came up as I was solving an even bigger problem - how does one make the behavior of a custom Powershell release task conditional upon the identity of the current agent pool (without relying on the pool name)? My answer was - custom capabilities. In order to retrieve the capabilities, I needed to know who should I grant the read access on the pool to, and whether the custom task is allowed to call the pool-related REST endpoints.

No comments:

Post a Comment