ajryan / TfsProxy

TFS Web API Proxy
0 stars 0 forks source link

Authentication Issues #1

Open ghost opened 11 years ago

ghost commented 11 years ago

Hey there. I'm working on adding stuff to this service for my own uses elsewhere and I'd be happy to contribute back to this.

Currently however, I'm having issues getting this service running with proper access to TFS instance of IIS outside of VS. When I login using valid creds running IIS express on my local machine, it can query TFS fine. As soon as I publish to an IIS server elsewhere, set the app pool to run as a TFS accepted user, login via the same creds, I get errors and/or unauthorized problems.

Your projects controller returns:

{"Message":"An error has occurred.","ExceptionMessage":"Object reference not set to an instance of an object.","ExceptionType":"System.NullReferenceException","StackTrace":"   at TfsProxy.Web.Controllers.ProjectsController.Get() in z:\\Users\\lvanderhoeven\\Code\\Projects\\BFG\\TfsProxy\\TfsProxy.Web\\Controllers\\ProjectsController.cs:line 30\r\n   at lambda_method(Closure , Object , Object[] )\r\n   at System.Web.Http.Controllers.ReflectedHttpActionDescriptor.ActionExecutor.<>c__DisplayClass13.<GetExecutor>b__c(Object instance, Object[] methodParameters)\r\n   at System.Web.Http.Controllers.ReflectedHttpActionDescriptor.ActionExecutor.Execute(Object instance, Object[] arguments)\r\n   at System.Web.Http.Controllers.ReflectedHttpActionDescriptor.<>c__DisplayClass5.<ExecuteAsync>b__4()\r\n   at System.Threading.Tasks.TaskHelpers.RunSynchronously[TResult](Func`1 func, CancellationToken cancellationToken)"}

And my builds controller gets

{"Message":"An error has occurred.","ExceptionMessage":"TF30063: You are not authorized to access http://tfs-dev-01.bfgdev.inside:8080/tfs/rjr.","ExceptionType":"Microsoft.TeamFoundation.TeamFoundationServerUnauthorizedException","StackTrace":"   at Microsoft.TeamFoundation.Client.TfsConnection.ThrowAuthorizationException(Exception e)\r\n   at Microsoft.TeamFoundation.Client.TfsConnection.UseCredentialsProviderOnFailure(Action action, Int32 retries, Boolean throwOnFailure)\r\n   at Microsoft.TeamFoundation.Client.TfsConnection.EnsureAuthenticated()\r\n   at TFSProxy.Web.Controllers.BuildsController.Get(String status, String start, String finish) in z:\\Users\\lvanderhoeven\\Code\\Projects\\BFG\\TfsProxy\\TfsProxy.Web\\Controllers\\BuildsController.cs:line 38\r\n   at lambda_method(Closure , Object , Object[] )\r\n   at System.Web.Http.Controllers.ReflectedHttpActionDescriptor.ActionExecutor.<>c__DisplayClass13.<GetExecutor>b__c(Object instance, Object[] methodParameters)\r\n   at System.Web.Http.Controllers.ReflectedHttpActionDescriptor.ActionExecutor.Execute(Object instance, Object[] arguments)\r\n   at System.Web.Http.Controllers.ReflectedHttpActionDescriptor.<>c__DisplayClass5.<ExecuteAsync>b__4()\r\n   at System.Threading.Tasks.TaskHelpers.RunSynchronously[TResult](Func`1 func, CancellationToken cancellationToken)"}

Is there any magic configuration sauce when deploying to an IIS server outside of the local machine?

ajryan commented 11 years ago

Hi.

First off, the user running the app pool doesn't have anything to do with the TFS authorization: TfsProxy uses the Authorization and TfsUrl headers from the HTTP request to make the connection, so whatever username and password you provide via Basic auth are used to connect. So the IIS hosting is a red herring.

The exception you're getting from the Projects controller indicates that the Team Project Collection is returning null for the workItemStore on this line:

var workItemStore = collection.GetService<WorkItemStore>();

... which is strange, because if you successfully authenticate, you should be able to get an instance of the Work Item Store.

A coulpe of questions to help diagnose the issue:

I may also have a coding error in there - I'm assuming that the user will have permission to access the Work Item Store for each collection, but may not have the right to actually read work items. It's possible that

The purpose of this project is to help people connect to an arbitrary TFS server from clients that can't use the TFS API. That's the reason for taking credentials and TfsUrl in the request headers. Is your user-facing app in C#? If so you might just want to skip TfsProxy and talk to TFS using its native API. Or you might want to rip out my auth attribute in your fork and rely on the appdomain user, as it looks like you are in your Builds controller.

ajryan commented 11 years ago

Also, it looks like this may be your issue: http://osnabrugge.wordpress.com/2011/11/04/tfs-api-workitemstore-is-null-and-throws-nullreferenceexception/

Try changing to using the WorkItemStore constructor on the line I mentioned above and see if you get the same exception mentioned in the blog post.

Did you add any references to the project? Specifically, did you add any from the ...\Common7\IDE\ReferenceAssemblies\v4.0 folder?

ghost commented 11 years ago

Thanks for the feedback. I'm not well versed in .NET authentication/authorization paradigm, so this is very helpful. My background is much more in Ruby than C#, but I need a good TFS api and this seems like a better start than what I was working on. The consumer will probably end up being either a JS or Ruby client of some kind. I am as of yet undecided.

Answers:

  1. I am running against TFS 2012. To reiterate, both the BuildController and your ProjectController worked when running locally.
  2. I've created a mini ruby script to set the headers and pass over the required headers/basic auth. The script is pasted below.
  3. I solved this one. I was not using the actual credentials provided by the Current.User UserDataPrinciple. I added it to the tfs connection and it's working now. Also helped to run a tfs.EnsureAuthenticated() on the new connection. Seemed to be recommended by a number of MS resources.
require 'httpi'

request = HTTPI::Request.new 'http://api-dev-01.../api/projects'
request.headers["TfsUrl"] = "http://tfs-dev-01:8080/tfs"
request.open_timeout = 20
request.auth.basic('domain\username', 'password')

response = HTTPI.get(request)

puts response.code
puts response.body
ajryan commented 11 years ago

OK, sounds like this project could be useful to you for a Ruby or JavaScript client.

In your builds controller you were relying on the current App Pool user, but now you've switched to HttpContext.Current.User as UserDataPrincipal, which is the credential provided by the TfsBasicAuthenticationAttribute. You'll probably get better performance if you get the build service from the HttpContext.Current.Items["TFS_CONFIG_SERVER"] as TfsConfigurationServer -- it's already authenticated, but you're probably re-authenticating if you're constructing a new TfsConnection instance.

In any case, if your end-users aren't going to enter their own specific TFS credentials, you may be better off ripping out TfsBasicAuthenticationAttribute and just using the current App Pool user. In your non-working BuildController (before using the UserDataPrincipal), TfsBasicAuthenticationAttribute may have been interfering with authenticating as the app pool user when on full IIS, because it changes the HttpContext.Current.User.

As for the exception in ProjectsController, check out the link I posted above -- changing to the WorkItemStore constructor may expose a more specific error.

ghost commented 11 years ago

Ah okay. I wasn't sure I could get the IBuildServer service from the HttpContext.Current.Items["TFS_CONFIG_SERVER"] but I think I was doing it wrong. Appears to work now. Thanks for that. I will check the issue on WorkItemStore momentarily and get back to you on that.

Out of curiosity, and mostly because I don't understand all the code is doing, does each request need to do basic auth, or are the creds cached in a cookie? And if so, is there a way to do an initial "auth" request, capture the authentication and then have all subsequent calls use that instead of requiring basic auth for every call?

ajryan commented 11 years ago

An auth cookie is returned (named .ASPXAUTH I believe), which you can set on further requests (and omit the basic auth header). I have not actually tested this scenario, because my client lib automatically caches the Basic auth stuff. It ought to work.

Are you going to have users enter their individual TFS creds? If not, you can rip out the TfsBasicAuthenticationAttribute all together, not set any basic auth on the request, and just rely on the app pool user's identity to auth against TFS.

ajryan commented 11 years ago

By the way, someone at Microsoft has published a more fully-featured TFS proxy that exposes TFS resources over an OData API. The reason I created my proxy is that the Microsoft proxy gets configured server-side to talk to one specific TFS server, but I wanted clients to be able to specify an arbitrary server. The OData proxy also uses Basic auth, but doesn't take a header for the TFS URL. Here is is: http://www.microsoft.com/en-us/download/details.aspx?id=36230

There is a Ruby OData client lib: https://github.com/visoft/ruby_odata

... might save you some work

plukevdh commented 11 years ago

I had seem some buzz around odata but wasn't sure if it was ready for prime time yet. I'll investigate. Thanks for expanding.

plukevdh commented 11 years ago

I did add my build controller to my fork. Any feedback about how it's being done would be awesome.

As for the other issue regarding the project controller, I made the suggested changes, but in both cases, I am now only getting back an error with no backtrace. I'll dig further. And update.