TFS 2015 u2. Trying to write a TFS extension that would use JavaScript API to manipulate the security on a release definition. Security related APIs fail on me with error 401. The code goes:
VSS.require(["VSS/Service", "VSS/Security/RestClient"],
function (Srv, SecAPI)
{
var SecClient = Srv.getCollectionClient(SecAPI.SecurityHttpClient);
SecClient.queryAccessControlLists("aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee").then(function(a)
{
//...
});
}
That errors out with 401 Unauthorized. As far as I understand, the list of REST API that an extension may use is driven by the scopes parameter in the manifest. What do I place there for this to work? The scopes list lists none of the sort.
Meanwhile, calling the same endpoint from a regular REST client with Windows auth works as expected.
In TFS 2017 u2, finally, there's vso.security_manage.
In TFS 2017 u1, there is a scope vso.base that covers this API endpoint, but only with GET. POST, which is required to change the descriptor, is still not covered by the scope.
In TFS 2015 u2, and presumably below, there's no scope that covers the ACL related endpoints.
I've found a very hackish way to enable those endpoints for OAuth in older versions of TFS. It's only applicable to on-premises TFS. The relationship between OAuth scopes and service endpoint URLs/methods is stored in a global, public, mutable singleton data structure that a piece of user code might just be able to alter. You can see it in your favorite MSIL disassembler (ILDASM, ILSpy, Reflector) if you poke around method CreateDefault in class Microsoft.VisualStudio.Services.DelegatedAuthorization.AuthorizationScopeDefinitions within Microsoft.TeamFoundation.Framework.Server.dll.
The following Global.asax does the trick. You have to copy it to C:\Program Files\Microsoft Team Foundation Server 14.0\Application Tier\Web Services (for TFS 2015).
<%# Application Inherits="Microsoft.TeamFoundation.Server.Core.TeamFoundationApplication" %>
<%# Import namespace="Microsoft.VisualStudio.Services.DelegatedAuthorization" %>
<%# Import namespace="System.Collections.Generic" %>
<%# Import namespace="System.Linq" %>
<script runat="server">
void Session_Start(object o, EventArgs a)
{
AuthorizationScopeDefinition Def = AuthorizationScopeDefinitions.Default.scopes
.FirstOrDefault(d => d.scope == "vso.identity");
if(Array.IndexOf(Def.patterns, "/_apis/SecurityNamespaces#GET") < 0)
{
List<string> l = Def.patterns.ToList();
l.Add("/_apis/SecurityNamespaces#GET");
l.Add("/_apis/AccessControlLists#GET+POST");
l.Add("/DefaultCollection/_apis/SecurityNamespaces#GET");
l.Add("/DefaultCollection/_apis/AccessControlLists#GET+POST");
Def.patterns = l.ToArray();
}
}
</script>
Hooking Application_Start would've made more sense, but the code-behind DLL already hooks it. Another handler in Global.asax doesn't override. I monkey-patch the vso.identity scope, because my extension already claims that, but feel free to use any other one.
Introducing your own, brand new scope probably won't work.
Unfortunately, there isn’t any REST API to change permission of release definition or release environment.
There is a user voice that you can vote. REST API for release defintion or release enviornment TFS PM will kindly review your suggestion.
Related
I'm rather new to Blazor, but I am currently trying to get access to some classes from within a class library that I've created and deployed as a Nuget package. As background, the Nuget package is an Api library, which allows me to talk to a webservice (I don't know if this is relevant or not). However, every time I go to the page where I'm testing, the page never loads and instead I left looking at the browser loading circle until I navigate away or close the application. During my testing here, it seems like it's the #inject call of my interface into the Blazor component which is causing the issue as when I remove it and try to load the page normally, the page does so.
So to demonstrate what I have setup, here is where I've added the Singletons to the DI:
builder.Services.AddSingleton<IApiConfigHelper, ApiConfigHelper>();
builder.Services.AddSingleton<IApiHelper, ApiHelper>();
builder.Services.AddSingleton<ISystemEndpoint, SystemEndpoint>();
Then on the blazor page, I have the following declarations at the top of my page:
#using Library.Endpoints
#using Library.Models
#page "/"
#inject ISystemEndpoint _systemEndpoint
Now I am leaning towards is this something to do with the Nuget package and using it with DI. I have tested the library away from this project (In a console application) and can confirm it's working as it should.
I have also created a local class library as a test to, to see if I could inject a data access class into the page and I can confirm that this works without an issue, which suggests to me that DI is working, just not with my Nuget package.
I did have a look into CORS, given that the Nuget package is accessing an external domain, and setup the following simple CORS policy in the app:
builder.Services.AddCors(policy =>
{
policy.AddPolicy("OpenCorsPolicy", opt =>
opt.AllowAnyOrigin()
.AllowAnyHeader()
.AllowAnyMethod());
});
Which is added to the app after the AddRouting call like so:
app.UseCors("OpenCorsPolicy");
However again, this wasn't the solution so if anyone is able to point me in the right direction with where I may be going wrong with this or offer any advice, I would be most grateful.
EDIT 1 - Provides details #mason queried
Regarding SystemEndpoint, the constructor is being injected with 2 things, as below:
public SystemEndpoint(IApiHelper apiHelper, IOptions<UriConfigModel> uriOptions)
{
_apiHelper = apiHelper;
_uriOptions = uriOptions.Value;
}
My Nuget Library is dependant on the following:
Azure.Identity
Azure.Security.KeyVault.Secrets
Microsoft.AspNet.WebApi.Client
Microsoft.Extensisons.Options.ConfigurationExtensions
EDIT 2 - Doing some further testing with this I have added a simple Endpoint class to my Nuget library, which returns a string with a basic message, as well as returning the values of the 2 UriConfig properties as below. I added this test to 1) sanity check that my DI was working correctly, and 2) check the values that are being assigned from appsettings to my UriConfig Object.
public class TestEndpoint : ITestEndpoint
{
private readonly IOptions<UriConfigModel> _uriConfig;
public TestEndpoint(IOptions<UriConfigModel> uriConfig)
{
_uriConfig = uriConfig;
}
public string TestMethod()
{
return $"You have successfully called the test method\n\n{_uriConfig.Value.Release} / {_uriConfig.Value.Version}";
}
}
However when adding in the dependency of IApiHelper into the Ctor, the method then breaks and fails to load the page. Looking into ApiHeloer, the Ctor has a dependency being injected into it of IApiConfigHelper. Looking at the implementation, the Ctor of ApiConfigHelper is setting up the values and parameters of the HttpClient that should make the REST calls to the external Api.
Now I believe what is breaking the code at this point is a call I'm making to Azure Key Vault, via REST, to pull out the secret values to connect to the Api. The call to KeyVault is being orchestrated via the following method, making use of the Azure.Security.KeyVault.Secrets Nuget Package, however I assume that at the heart of it, it's making a REST call to Azure on my behalf:
private async Task<KeyVaultSecret> GetKeyVaultValue(string secretName = "")
{
try
{
if (_secretClient is not null)
{
var result = await _secretClient.GetSecretAsync(secretName);
return result.Value;
}
}
catch (ArgumentException ae)
{
Console.WriteLine(ae.Message);
}
catch (Azure.RequestFailedException rfe)
{
Console.WriteLine(rfe.Message);
}
return new(secretName, "");
}
So that's where I stand with this at the moment. I still believe it could be down to CORS, as it seems to be falling over when making a call to an external service / domain, but I still can say 100%. As a closing thought, could it be something as simple as when I call call the above method, it's not being awaited????
So after persisting with this it seems like the reason it was failing was down to "awaiting" the call to Azure KeyVault, which was happening indirectly via the constructor of ApiConfigHelper. The resulting method for getting KeyVault value is now:
private KeyVaultSecret GetKeyVaultValue(string secretName = "")
{
try
{
if (_secretClient is not null)
{
var result = _secretClient.GetSecret(secretName);
if (result is not null)
{
return result.Value;
}
}
}
catch (ArgumentException ae)
{
Console.WriteLine(ae.Message);
}
catch (Azure.RequestFailedException rfe)
{
Console.WriteLine(rfe.Message);
}
return new(secretName, "");
}
I am now able to successfully make calls to my library and return values from the Api it interacts with.
I can also confirm that this IS NOT a CORS issue. Once I saw that removing the await was working, I then removed the CORS policy declarations from the service and the app in my Blazor's start-up code and everything continued to work without an issue.
As a final note, I must stress that this is only seems an issue when using the library with Blazor (possibly webApi projects) as I am able to use the library, awaiting the Azure call just fine in a console application.
I am trying to use breeze sharp with Blazor Webassembly.
I have been able to recompile breeze sharp for .Net Standard 2.0 and 2.1 that has shown positive results reading data from the server.
However when trying to load Metadata from script I have noticed that MetadaStore.ImportMetadata method fails to load metadata.
When I followed up I found that there Breeze.Sharp expects metadataVersion property and namingConvention node from the metadata json returned by the breeze server.
What I can see is that First the metadata format has changed and those properties are no longer present in the generated metadata.
But if I FetchMetadata using DataService then the metadata is loaded into the metadatastore.
My Question is ...Is there a plan to update breeze sharp to be aligned with recent developments in the dotnet platform?
Kindly consider it as it aligns very well with Blazor. Actually my experiment went very fine expect for loading metadata stored locally instead of loading it from the server every time.
private void DeserializeFrom(JNode jNode, bool isFromServer) {
MetadataVersion = jNode.Get<String>("metadataVersion");
// may be more than just a name
var ncNode = jNode.GetJNode("namingConvention");
if (ncNode != null) {
var nc = Configuration.Instance.FindOrCreateNamingConvention(ncNode);
if (nc == null) {
OnMetadataMismatch(null, null, MetadataMismatchTypes.MissingCLRNamingConvention, ncNode.ToString());
} else {
// keep any preexisting ClientServerNamespaceMap info
NamingConvention = nc.WithClientServerNamespaceMapping(this.NamingConvention.ClientServerNamespaceMap);
}
}
// localQueryComparisonOptions
jNode.GetJNodeArray("dataServices").Select(jn => new DataService(jn)).ForEach(ds => {
if (GetDataService(ds.ServiceName) == null) {
AddDataService(ds);
}
});
jNode.GetJNodeArray("structuralTypes")
.ForEach(jn => UpdateStructuralTypeFromJNode(jn, isFromServer));
jNode.GetMap<String>("resourceEntityTypeMap").ForEach(kvp => {
var stName = kvp.Value;
if (isFromServer) {
stName = TypeNameInfo.FromStructuralTypeName(stName).ToClient(this).StructuralTypeName;
}
// okIfNotFound because metadata may contain refs to types that were already excluded earlier in
// UpdateStructuralTypeFromJNode
var et = GetEntityType(stName, true);
if (et != null) {
SetResourceName(kvp.Key, et);
}
});
}
I have been following up very closely new developments and this is what I have noticed so far.
Microsoft is reviving the product called Microsoft Restier.
First it was developed on the .Net Framework Platform but now they are rewriting it to run on the .Net Core and is expected to go RTM in the first half 0f the year 2020. A good thing about this Microsoft Restier is that your Full Entity Context is exposed as OData endpoint with your entity lists exposed without the need for creating Controllers on Actions.
This way it reduces the tedious work of writing an action for each entity that you want to expose as an OData resource different from the current case with plain OData and Breeze Server.
There are so many areas for configurations and extensibility.
Microsoft is remaking RIA Data Services in the form of OData Connected Service.
This creates a Proxy in the same way as RIA Data Services.
With the Proxy generated there is no need to create a client data model. The model created on the server will suffice... not as the case for breeze sharp where you need to create a model on the client although there are signs that at DevForce they are exploring using PostSharp to make it possible to use POCO objects sharable between breeze server and clients. We however do not know when will this be available.
OData Connected Service works seamlessly with Blazor Server (On the Client Side there some bugs that are being worked on ) and removes the headache of working with bare bone HttpClient.
As for breeze sharp it currently works well with both Blazor Server and Web Assembly versions (Breeze Sharp Standard). I think they will rework their product and enable it to work with OData the same way Breezejs does. This combined with Microsoft.Restier will make life very easy.
Actually breeze has very nice features especially when it comes to caching.
Lets wait and see.
I have a wcfservice that is coded in vb.net. I want to use it in a c# mvc 4 client application. But when I added this service with right click on references and Add Service References, I can not use this. How can I do this?
Your service codes contain contracts and the implementation. However, The service code should be used in only the service, not in the client programs. You should generate proxy classes to be used by client programs like your Asp.NET applications.
There are 2 typical ways:
1. You run the service, then create a Service Reference to the service instance, so VS IDE will generate proxy classes under folder Service References.
2. Generate proxy classes using svcutil.exe against the service assembly that contain contracts, and build a client API assembly.
The 2nd is the most tidy and efficient way. Please check this article for more details at http://www.codeproject.com/Articles/627240/WCF-for-the-Real-World-Not-Hello-World
This is a pretty basic question but generally speaking you can add a web service reference and endpoint info in the main Web.Config file but I suspect you are having trouble with calling the WCF service URL, if so I posted an example of a generic class/wrapper for calling WCF web services in an MVC application.
Add the Web Reference to Visual Studio 2012 (or similar):
Right click the project in the Solution Explorer
Choose Add–> Service Reference –> Then click the Advanced Button... –>
Then click the "Add Web Reference…" button –> then type the address of your Web Service into the URL box. Then click the green arrow and Visual Studio will discover your Web Services and display them.
You may have known the above already and might just need a generic wrapper class which makes calling the WCF Web Service easy in MVC. I've found that using the generic class works well. I can't take credit for it; found it on the internet and there was no attribution. There is a complete example with downloadable source code at http://www.displacedguy.com/tech/powerbuilder-125-wcf-web-services-asp-net-p3 that calls a WCF Web service that was made using PowerBuilder 12.5.Net, but the process of calling the WCF Web service in MVC is the same no matter if it was created in Visual Studio or PowerBuilder.
Generic wrapper class for calling WCF Web Services in ASP.NET MVC
Of course don't model your error handling after my incomplete example...
using System;
using System.ServiceModel;
namespace LinkDBMvc.Controllers
{
public class WebService<T>
{
public static void Use(Action<T> action)
{
ChannelFactory<T> factory = new ChannelFactory<T>("*");
T client = factory.CreateChannel();
bool success = false;
try
{
action(client);
((IClientChannel)client).Close();
factory.Close();
success = true;
}
catch (EndpointNotFoundException e)
{
LinkDBMvc.AppViewPage.apperror.LogError("WebService", e, "Check that the Web Service is running");
}
catch (CommunicationException e)
{
LinkDBMvc.AppViewPage.apperror.LogError("WebService", e, "Check that the Web Service is running");
}
catch (TimeoutException e)
{
LinkDBMvc.AppViewPage.apperror.LogError("WebService", e, "Check that the Web Service is running");
}
catch (Exception e)
{
LinkDBMvc.AppViewPage.apperror.LogError("WebService", e, "Check that the Web Service is running");
}
finally
{
if (!success)
{
// abort the channel
((IClientChannel)client).Abort();
factory.Abort();
}
}
}
}
}
I have a form:
#using (Html.BeginForm(new { ReturnUrl = ViewBag.ReturnUrl })) {
#Html.AntiForgeryToken()
#Html.ValidationSummary()...
and action:
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public ActionResult Login(LoginModel model, string returnUrl, string City)
{
}
occasionally (once a week), I get the error:
The anti-forgery token could not be decrypted. If this application is
hosted by a Web Farm or cluster, ensure that all machines are running
the same version of ASP.NET Web Pages and that the configuration
specifies explicit encryption and validation keys. AutoGenerate cannot
be used in a cluster.
i try add to webconfig:
<machineKey validationKey="AutoGenerate,IsolateApps"
decryptionKey="AutoGenerate,IsolateApps" />
but the error still appears occasionally
I noticed this error occurs, for example when a person came from one computer and then trying another computer
Or sometimes an auto value set with incorrect data type like bool to integer to the form field by any jQuery code please also check it.
I just received this error as well and, in my case, it was caused by the anti-forgery token being applied twice in the same form. The second instance was coming from a partial view so wasn't immediately obvious.
validationKey="AutoGenerate"
This tells ASP.NET to generate a new encryption key for use in encrypting things like authentication tickets and antiforgery tokens every time the application starts up. If you received a request that used a different key (prior to a restart for instance) to encrypt items of the request (e.g. authenication cookies) that this exception can occur.
If you move away from "AutoGenerate" and specify it (the encryption key) specifically, requests that depend on that key to be decrypted correctly and validation will work from app restart to restart. For example:
<machineKey
validationKey="21F090935F6E49C2C797F69BBAAD8402ABD2EE0B667A8B44EA7DD4374267A75D7
AD972A119482D15A4127461DB1DC347C1A63AE5F1CCFAACFF1B72A7F0A281B"
decryptionKey="ABAA84D7EC4BB56D75D217CECFFB9628809BDB8BF91CFCD64568A145BE59719F"
validation="SHA1"
decryption="AES"
/>
You can read to your heart's content at MSDN page: How To: Configure MachineKey in ASP.NET
Just generate <machineKey .../> tag from a link for your framework version and insert into <system.web><system.web/> in Web.config if it does not exist.
Hope this helps.
If you get here from google for your own developer machine showing this error, try to clear cookies in the browser. Clear Browser cookies worked for me.
in asp.net Core you should set Data Protection system.I test in Asp.Net Core 2.1 or higher.
there are multi way to do this and you can find more information at Configure Data Protection and Replace the ASP.NET machineKey in ASP.NET Core and key storage providers.
first way: Local file (easy implementation)
startup.cs content:
public class Startup
{
public Startup(IConfiguration configuration, IWebHostEnvironment webHostEnvironment)
{
Configuration = configuration;
WebHostEnvironment = webHostEnvironment;
}
public IConfiguration Configuration { get; }
public IWebHostEnvironment WebHostEnvironment { get; }
// This method gets called by the runtime.
// Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
// .... Add your services like :
// services.AddControllersWithViews();
// services.AddRazorPages();
// ----- finally Add this DataProtection -----
var keysFolder = Path.Combine(WebHostEnvironment.ContentRootPath, "temp-keys");
services.AddDataProtection()
.SetApplicationName("Your_Project_Name")
.PersistKeysToFileSystem(new DirectoryInfo(keysFolder))
.SetDefaultKeyLifetime(TimeSpan.FromDays(14));
}
}
second way: save to db
The Microsoft.AspNetCore.DataProtection.EntityFrameworkCore NuGet
package must be added to the project file
Add MyKeysConnection ConnectionString to your projects
ConnectionStrings in appsettings.json > ConnectionStrings >
MyKeysConnection.
Add MyKeysContext class to your project.
MyKeysContext.cs content:
public class MyKeysContext : DbContext, IDataProtectionKeyContext
{
// A recommended constructor overload when using EF Core
// with dependency injection.
public MyKeysContext(DbContextOptions<MyKeysContext> options)
: base(options) { }
// This maps to the table that stores keys.
public DbSet<DataProtectionKey> DataProtectionKeys { get; set; }
}
startup.cs content:
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime.
// Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
// ----- Add this DataProtection -----
// Add a DbContext to store your Database Keys
services.AddDbContext<MyKeysContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("MyKeysConnection")));
// using Microsoft.AspNetCore.DataProtection;
services.AddDataProtection()
.PersistKeysToDbContext<MyKeysContext>();
// .... Add your services like :
// services.AddControllersWithViews();
// services.AddRazorPages();
}
}
If you use Kubernetes and have more than one pod for your app this will most likely cause the request validation to fail because the pod that generates the RequestValidationToken is not necessarily the pod that will validate the token when POSTing back to your application. The fix should be to configure your nginx-controller or whatever ingress resource you are using and tell it to load balance so that each client uses one pod for all communication.
Update: I managed to fix it by adding the following annotations to my ingress:
https://kubernetes.github.io/ingress-nginx/examples/affinity/cookie/
Name Description Values
nginx.ingress.kubernetes.io/affinity Sets the affinity type string (in NGINX only cookie is possible
nginx.ingress.kubernetes.io/session-cookie-name Name of the cookie that will be used string (default to INGRESSCOOKIE)
nginx.ingress.kubernetes.io/session-cookie-hash Type of hash that will be used in cookie value sha1/md5/index
I ran into this issue in an area of code where I had a view calling a partial view, however, instead of returning a partial view, I was returning a view.
I changed:
return View(index);
to
return PartialView(index);
in my control and that fixed my problem.
I got this error on .NET Core 2.1. I fixed it by adding the Data Protection service in Startup:
public void ConfigureServices(IServiceCollection services)
{
services.AddDataProtection();
....
}
you are calling more than one the #Html.AntiForgeryToken() in your view
I get this error when the page is old ('stale'). A refresh of the token via a page reload resolves my problem. There seems to be some timeout period.
I found a very interesting workaround for this problem, at least in my case. My view was dynamically loading partial views with forms in a div using ajax, all within another form. the master form submits no problem, and one of the partials works but the other doesn't. The ONLY difference between the partial views was at the end of the one that was working was an empty script tag
<script type="text/javascript">
</script>
I removed it and sure enough I got the error. I added an empty script tag to the other partial view and dog gone it, it works! I know it's not the cleanest... but as far as speed and overhead goes...
I know I'm a little late to the party, but I wanted to add another possible solution to this issue. I ran into the same problem on an MVC application I had. The code did not change for the better part of a year and all of the sudden we started receiving these kinds of error messages from the application.
We didn't have multiple instances of the anti-forgery token being applied to the view twice.
We had the machine key set at the global level to Autogenerate because of STIG requirements.
It was exasperating until I got part of the answer here: https://stackoverflow.com/a/2207535/195350:
If your MachineKey is set to AutoGenerate, then your verification
tokens, etc won't survive an application restart - ASP.NET will
generate a new key when it starts up, and then won't be able to
decrypt the tokens correctly.
The issue was that the private memory limit of the application pool was being exceeded. This caused a recycle and, therefore, invalidated the keys for the tokens included in the form. Increasing the private memory limit for the application pool appears to have resolved the issue.
My fix for this was to get the cookie and token values like this:
AntiForgery.GetTokens(null, out var cookieToken, out var formToken);
For those getting this error on Google AppEngine or Google Cloud Run, you'll need to configure your ASP.NET Core website's Data Protection.
The documentation from the Google team is easy to follow and works.
https://cloud.google.com/appengine/docs/flexible/dotnet/application-security#aspnet_core_data_protection_provider
A general overview from the Microsoft docs can be found here:
https://cloud.google.com/appengine/docs/flexible/dotnet/application-security#aspnet_core_data_protection_provider
Note that you may also find you're having to login over and over, and other quirky stuff going on. This is all because Google Cloud doesn't do sticky sessions like Azure does and you're actually hitting different instances with each request.
Other errors logged, include:
Identity.Application was not authenticated. Failure message: Unprotect ticket failed
I have some code to automate the creation of build definitions in TFS.
Now I'd like to have this code invoked whenever a branch is created.
Looking at the API, I see that there is a BranchObjectCreatedEvent in Microsoft.TeamFoundation.VersionControl.Client.VersionControlServer.
So I've added some code to a console app to handle the event.
private static void MonitorBranchCreated()
{
try
{
TfsTeamProjectCollection tfs = InitialiseTfs();
var vcs = tfs.GetService<VersionControlServer>();
var projects = vcs.GetAllTeamProjects(true);
foreach (var project in projects)
{
project.VersionControlServer.BranchObjectCreated += BranchObjectCreated;
}
Console.WriteLine("Subscribed to TFS BranchObjectCreated Event - Awaiting Notification...");
Console.ReadLine();
}
catch (Exception exception)
{
DisplayError(exception);
}
}
private static void BranchObjectCreated(object sender, BranchObjectCreatedEventArgs e)
{
// Create the Build
}
The trouble is that the event never fires when I create a branch from Source Control Explorer in Visual Studio.
The MSDN documentation is limited and I can't find any other examples of usage so I'm hoping somebody might be able to tell me if this is the correct approach.
If so, why might the event not be firing? If not, is there another way I can hook into TFS so that I can handle events related to creation of branches?
When you hook up events to the client API, you only get events that were created by that client. If you were to hook up a BranchObjectCreated listener, then call VersionControlServer.CreateBranch(), then your branch object created listener would be called.
If you want to listen to events on the server (such as when somebody else creates a branch, or you create a branch from a different client), then you need to tie into the server's project alert system.
You can install the Alerts Explorer in the Team Foundation Server Power Tools that will allow you to configure fine-grained alerts on projects that will send you email or call a web method. At this point, you can create a new build that references this new branch.