Partager via


How to: Use Impersonation with WCF

Applies to: Office 2010 | Project 2010 | Project Server 2010 | SharePoint Server 2010

In this article
Developing and Using the WCF-Based Impersonation Application
Setting Permissions for Impersonation
Developing the Application

When an application impersonates a user in Microsoft Project Server 2010, the application acts as though the impersonated user is running it. During impersonation, calls to Project Server Interface (PSI) methods use the global permissions and category permissions of the impersonated user. This article shows how to set permissions for impersonation and how to develop a simple console application for impersonation by using the Windows Communication Foundation (WCF) interface in Project Server. (Sample code in this article is based on a test application by Tiger Wang, Microsoft Corporation.)

This article includes the following sections:

  • Developing and Using the WCF-Based Impersonation Application

  • Setting Permissions for Impersonation

  • Developing the Application

  • Complete Code Example

Security noteSecurity Note

When designing applications that interact with Project Server, you should limit the use of impersonation. Granting impersonation privileges should be limited to a very small number of users because it enables those users to act on behalf of any other user in Project Server. For more information, see the Setting Permissions for Impersonation section.

Project Server event handlers run on the Project Server computer with the SharePoint farm administrator credentials, or with the specified logon account for the Microsoft Project Server Events Service 2010 service. Because event handlers already run on the Project Server back end, not the Project Web App front end, you would not normally use impersonation in an event handler. For more information about event handlers, see How to: Create a Project Server Event Handler and Log an Event.

Developing and Using the WCF-Based Impersonation Application

There are changes in the way Project Server 2010 does impersonation, compared to Microsoft Office Project Server 2007:

  • We recommend that new impersonation applications for Project Server 2010 use the WCF service interface rather than the ASMX interface for Web services. With the WCF interface, it is not necessary to create a derived service class that overrides the GetWebRequest method.

  • Applications for Project Server 2010 must use the Microsoft .NET Framework 3.5.

  • Impersonation applications developed for Office Project Server 2007 use the ASMX interface. For two examples, see Using Impersonation in Project Server. To run on Project Server 2010, impersonation applications should be updated to use the .NET Framework 3.5 and the new constructor overload for the PSContextInfo method.

The Project 2010 SDK download includes the complete sample code for the WCFImpersonationTest application.

Overview of the WCFImpersonationTest application   The WCFImpersonationTest application reads the user account of a person to impersonate. The application gets the GUID of the original user by using the GetCurrentUserUid method and the GUID of the user to impersonate by using the ReadResources method, where the PSI calls do not use impersonation. If the user account is valid, the application then sets the impersonation context and modifies the Web operation header in the outgoing WCF service request, and then uses the GetCurrentUserUid method again. The result shows that the application is impersonating the other user.

The WCFImpersonationTest application includes an option to set the WCF client endpoint either programmatically or by using the app.config file. For an explanation of both ways to set client endpoints, see Walkthrough: Developing PSI Applications Using WCF.

To build the application, you can use the code in this article and add a reference to the PSI proxy assembly or add a WCF service file for the Resource service. The Prerequisites for WCF-Based Code Samples article describes three ways to add a reference for a WCF service.

To add a reference for the Resource WCF service

  • Add a reference to the ProjectServerServices.dll proxy assembly for the PSI. The proxy assembly is in the Project 2010 SDK download.

  • Add the wcf.Resource.cs file from the SDK download. The proxy source file is in the Source.zip file in the Documentation\Intellisense\WCF subdirectory.

  • Add the Resource service reference directly in Visual Studio. The released version of Project Server 2010 requires temporary changes to the web.config file for the Project Server Services application.

We recommend using either of the first two methods, which do not require temporarily changing the web.config file for the Project Server Services application.

Using claims multi-authentication   Authentication of Project Server users, whether by Windows authentication or Forms authentication, is done through claims processing in SharePoint. The WCFImpersonationTest sample is designed for Windows authentication of the application user. Multi-authentication means that the Web application on which Project Web App is provisioned supports both Windows and Forms-based authentication. If that is the case, any call to a WCF or ASMX service that uses Windows authentication will fail with the following error, because claims cannot determine which type of user to authenticate:

The server was unable to process the request due to an internal error. 
For more information about the error, either turn on Include ExceptionDetailInFaults 
(either from ServiceBehaviorAttribute or from the <serviceDebug> configuration behavior) 
on the server in order to send the exception information back to the client, 
or turn on tracing as per the Microsoft .NET Framework 3.0 SDK documentation 
and inspect the server trace logs.

To fix the problem, all calls to WCF methods—whether for impersonation or not—should be within an OperationContextScope that is defined for each PSI service. Do not nest scopes for multiple services; for example, when using calls to the Resource and Project service, each set of calls should be within its own scope.

Before using an impersonation application, a user or group must be granted permissions for impersonation in the SharePoint service application and in Project Web App.

Setting Permissions for Impersonation

The user or group running the application must have permissions set in the Project Server Service application and in the Project Web App site.

Security noteSecurity Note

Granting widespread impersonation permissions bypasses the security model of Project Server and becomes an administrative problem. Following are examples of how to avoid granting many users the permissions necessary for impersonation:

  • Some scenarios that require impersonation in Microsoft Office Project Server 2007, such as reading and submitting status for another resource, are no longer necessary in Project Server 2010 with the new ReadStatusForResource and SubmitStatusForResource methods. If the application user has the StatusBrokerPermission global permission, the UpdateStatus(String) method can update timephased assignment data by including the ResID attribute in the Assn element for the changeXml parameter.

  • Use an event handler. Event handlers run under the user account for the Project Server Events Service. That user account could have impersonation permissions.

To set permissions in the Project Server Service application

  1. Open the SharePoint 2010 Central Administration page, and then click Manage Service Applications.

  2. Select the row for the Project Server Service application (Figure 1). Instead of clicking the name, select the row to highlight it.

  3. On the Service Applications tab, click Permissions.

    Figure 1. Permissions for the Project Server Service application

    Permissions for the Project Server Service

  4. In the Connection Permissions for Project Server Service Application dialog box (Figure 2), add the user or group that needs permission to run impersonation applications. After you click Add, and the user name shows in the list of claims, select the added user in the list and then select the Full Control check box. Otherwise, the user is not added when you click OK. Full Control is the only option.

    Figure 2. Adding connection permissions for impersonation

    Adding connection permissions for impersonation

  5. To ensure that the user or group is added, reopen the Connection Permissions for Project Server Service Application dialog box and confirm that the new user or group displays in the list of claims.

To set permissions in the Project Web App site

  1. On the Site Actions menu of Project Web App, click Site Permissions.

  2. On the Edit tab of the Permission Tools ribbon, click Grant Permissions.

  3. In the Grant Permissions dialog box (Figure 3), type the name of a user or group in the Users/Groups text box.

  4. Grant the user or group a permission such as Restricted Read, or a higher permission. You can use a SharePoint group, or grant one or more permissions directly.

    Figure 3. Granting permissions for the Project Web App site

    Granting permissions for the Project Web App site

  5. Click OK.

Developing the Application

The following procedures show the essential parts of the WCFImpersonationTest application. For the complete code, see the Complete Code Example section or the SDK download.

Note

You can use either Microsoft Visual Studio 2008 or Microsoft Visual Studio 2010. If you use Visual Studio 2008, you can later open the solution in Visual Studio 2010 to upgrade it.

Procedure 1. To create the console application framework

  1. In Visual Studio, create a Windows console application. In the New Project dialog box, in the drop-down list at the top of the dialog box, select .NET Framework 3.5. For this example, in the Name text box, type WCFImpersonationTest.

  2. In Solution Explorer, add the following references:

    • Microsoft.Office.Project.Server.Library

    • System.Runtime.Serialization

    • System.ServiceModel

    • System.ServiceModel.Web

  3. If you want to use the PSI proxy assembly, as described in Prerequisites for WCF-Based Code Samples, add the ProjectServerServices reference. If you do not use the PSI proxy assembly as a reference, add the wcfResource.cs file that is the source code for the Resource service in the proxy assembly. In either case, the arbitrary namespace name is SvcResource. If you are using the wcfResource.cs file, right-click the file name, and then click View Code to see the namespace name.

    Note

    The wcfResource.cs file is a proxy for the Resource service. It contains several classes, including ResourceClient, ResourceDataSet, UserDelegationDataSet, and others. When you double-click the wcfResource.cs file, Visual Studio tries to open the designer for a WCF service, which results in the following error: The class ResourceDataSet can be designed, but is not the first class in the file. Visual Studio requires that designers use the first class in the file. Move the class code so that it is the first class in the file and try loading the designer again.

    You should not make modifications to the wcfResource.cs file, because it is generated by SvcUtil.exe. Instead, right-click the file, and then click View Code.

  4. Add the following to the using statement at the top of the Program.cs file: using System.ServiceModel;

    The System.ServiceModel namespace includes the CommunicationException type that is returned by WCF service exceptions.

  5. Add the static class variables, the ParseCommandLine method, the Usage method, and the ExitApp method to the Program class, as shown in the Program.cs file content in the Complete Code Example section. The RunTests method and the remainder of the Main method should be added after the ImpersonationTest class is added in the following procedures.

There are many ways to organize the classes and members. In this article, the ImpersonationTest class is implemented in two separate files. The ImpersonationTest.cs file includes the class variables and the NoImpersonation, SimpleImpersonation, and DisposeClients methods. The Utilities.cs file includes methods to set the client endpoints and to set the impersonation context. Utilities.cs also includes the ResourceUtilities class with the GetResourceUid method and the GetResourceName method.

Procedure 2. To add the ImpersonationTest class

  1. In Solution Explorer, right-click the WCFImpersonationTest project, and then add a class named ImpersonationTest.

  2. Change the class declaration to partial class. Add the using statements, class variables, and class constructor. When the client endpoints are set, the resourceClient class variable is used to access methods in the Resource service through the front-end ProjectServer.svc router in Project Web App.

    The contextString class variable is set for impersonation in the SetImpersonationContext method (step 5 in Procedure 3).

    using System;
    using System.ServiceModel;
    using System.Globalization;
    using System.ServiceModel.Web;
    using PSLibrary = Microsoft.Office.Project.Server.Library;
    using SvcResource;
    
    namespace WCFImpersonationTest
    {
        public partial class ImpersonationTest
        {
            private static ResourceClient resourceClient;   // Client of the SvcResource reference.
            private static string appUserAccount;           // Name of the application user.
            private static Guid appUserUid;                 // GUID of the application user.
            private static string impersonatedUserAccount;  // User to impersonate.
            private static Guid impersonatedUserUid;        // GUID of user to impersonate.
            private static String contextString = String.Empty; // Impersonation context.
    
            // Variables for setting the impersonation context.
            bool isWindowsUser = true;          // Use a Windows account in this test application.
            Guid trackingGuid = Guid.Empty;     // The tracking GUID for Queue operations is not used.
            Guid siteId = Guid.Empty;           // The Project Web App site ID is not used.
            CultureInfo languageCulture = null; // The language culture is not used.
            CultureInfo localeCulture = null;   // The locale culture is not used.
    
            // ImpersonationTest constructor. Set the user account and the client endpoints. 
            public ImpersonationTest(Uri uri, string user, bool useConfig)
            {
                impersonatedUserAccount = user;
    
                if (useConfig)
                    ConfigClientEndpoints();
                else
                    SetClientEndpoints(uri);
            }
            // Add the NoImpersonation, SimpleImpersonation, and DisposeClients 
            // methods here.
            }
        }
    }
    

You can add the NoImpersonation, SimpleImpersonation, and DisposeClients methods after completing Procedure 3.

The ImpersonationTest class needs additional methods to set client endpoints programmatically or by using app.config. The class also needs methods to set the impersonation context. Procedure 3 shows how to add the additional methods in the Utilities.cs file.

For more information about setting endpoints and using app.config, see Walkthrough: Developing PSI Applications Using WCF.

Procedure 3. To add utilities for the ImpersonationTest class

  1. In Solution Explorer, right-click the WCFImpersonationTest project, and then add a class named Utilities.

  2. In the Utilities.cs file, change the class declaration to public partial class ImpersonationTest and add using statements, as follows:

    using System;
    using System.Net;
    using System.Net.Security;
    using System.Security.Cryptography.X509Certificates;
    using System.Security.Principal;
    using System.ServiceModel;
    using System.Globalization;
    using PSLibrary = Microsoft.Office.Project.Server.Library;
    using SvcResource;
    
    namespace WCFImpersonationTest
    {
        // Contains methods to set the client endpoints and set the impersonation context.
        public partial class ImpersonationTest
        {
        . . .
        }
    }
    
  3. Add the ConfigClientEndpoints method, which creates an instance of a resourceClient object with endpoints that are defined in the app.config file. In this example, app.config does not include an endpoint for the HTTPS protocol. The resourceClient class variable is declared in the ImpersonationTest.cs file.

    // Use the endpoints defined in app.config to configure the client.
    public static void ConfigClientEndpoints()
    {
        resourceClient = new ResourceClient("basicHttp_Resource");
    }
    
  4. Add the SetClientEndpoints method, which sets bindings and endpoints programmatically, and then creates an instance of a resourceClient object with those settings.

    CustomCertificateValidation is a callback method that validates a server certificate for HTTPS. In this example, it always returns true.

    // Add validation code for callback if using X509 certificate and HTTPS protocol. 
    private static bool CustomCertificateValidation(
        object sender, X509Certificate cert, X509Chain chain, SslPolicyErrors error)
    {
        return true;
    }
    
    // Programmatically set the endpoint of the Resource service client.
    public static void SetClientEndpoints(Uri pwaUri)
    {
        // Set the address of the front-end WCF router.
        const string svcRouter = "_vti_bin/PSI/ProjectServer.svc";
    
        string pwaUrl = pwaUri.Scheme + Uri.SchemeDelimiter + pwaUri.Host + ":"
            + pwaUri.Port + pwaUri.AbsolutePath;
    
        BasicHttpBinding binding = null;
    
        if (pwaUri.Scheme.Equals(Uri.UriSchemeHttps))
        {
            ServicePointManager.ServerCertificateValidationCallback +=
                new RemoteCertificateValidationCallback( 
                    CustomCertificateValidation);
    
            binding = new BasicHttpBinding(BasicHttpSecurityMode.Transport);
        }
        else
        {
            binding = new BasicHttpBinding(
                BasicHttpSecurityMode.TransportCredentialOnly);
        }
    
        binding.Name = "basicHttpConf";
        binding.SendTimeout = TimeSpan.MaxValue;
        ((BasicHttpBinding)binding).MaxReceivedMessageSize = 500000000;
        ((BasicHttpBinding)binding).ReaderQuotas.MaxNameTableCharCount = 500000000;
        ((BasicHttpBinding)binding).MessageEncoding = WSMessageEncoding.Text;
        ((BasicHttpBinding)binding).Security.Transport.ClientCredentialType =
            HttpClientCredentialType.Ntlm;
    
        EndpointAddress address = new EndpointAddress(pwaUrl + svcRouter);
    
        resourceClient = new ResourceClient(binding, address);
        resourceClient.ChannelFactory.Credentials.Windows.AllowedImpersonationLevel =
            TokenImpersonationLevel.Impersonation;
        resourceClient.ChannelFactory.Credentials.Windows.AllowNtlm = true;
    }
    
  5. Add the SetImpersonationContext method and the GetImpersonationContext method to the TestImpersonation class. They are similar to the methods used for impersonation applications in Office Project Server 2007, except they use the languageCulture and localeCulture parameters for the new PSContextInfo constructor overload. In this example, those parameters are set to null in the ImpersonationTest.cs file.

    The contextString class variable is declared in the ImpersonationTest.cs file and used when setting the Web operation context to make PSI calls that use impersonation.

    // Set the impersonation context for calls to the PSI on behalf of 
    // the impersonated user.
    internal static void SetImpersonationContext(
                            bool isWindowsUser, String userNTAccount,
        Guid userGuid, Guid trackingGuid, Guid siteId,
        CultureInfo languageCulture, CultureInfo localeCulture)
    {
        contextString = GetImpersonationContext(
                            isWindowsUser, userNTAccount, userGuid,
            trackingGuid, siteId, languageCulture, localeCulture);
    }
    
    // Get the impersonation context.
    private static String GetImpersonationContext(
        bool isWindowsUser, String userNTAccount,
        Guid userGuid, Guid trackingGuid, Guid siteId,
        CultureInfo languageCulture, CultureInfo localeCulture)
    {
        PSLibrary.PSContextInfo contextInfo = new PSLibrary.PSContextInfo(
            isWindowsUser, userNTAccount, userGuid, trackingGuid,
            siteId, languageCulture, localeCulture);
        String contextInfoString = PSLibrary.PSContextInfo.SerializeToString( 
            contextInfo);
    
        return contextInfoString;
    }
    
  6. Add a method to get a resource GUID from an account name and a method to get a resource name from a GUID. In this example, the GetResourceUid and GetResourceName methods are in a class named ResourceUtilities. The class is added to the Utilities.cs file; you could also add the class in a separate file. For the code, see the Utilities.cs section in the Complete Code Example.

    The GetResourceUid method uses a filter to reduce the amount of data in the ResourceDataSet returned by the call to the ReadResources method. For more information about using filters in PSI calls, see How to: Use a Filter Parameter with PSI Methods.

The ImpersonationTest.cs file needs the methods that perform the tests for impersonation. In this example, the NoImpersonation method calls GetCurrentUserUid() and displays the GUID for the application user. The NoImpersonation method also initializes a ResourceUtilities object, calls the GetResourceUid method with the user account that will be impersonated, and displays that in the Console window.

The SimpleImpersonation method calls GetCurrentUserUid again, but uses the impersonation context.

Procedure 4. To add the tests for impersonation

  1. Add the DisableFormsAuth method that sets the Web operation context to enable Windows authentication in a multi-authentication installation of Project Server.

    internal void DisableFormsAuth(bool isWindowsAuth)
    {
        WebOperationContext.Current.OutgoingRequest.Headers.Remove(
            "X-FORMS_BASED_AUTH_ACCEPTED");
    
        if (isWindowsAuth)
        {
            // Disable Forms authentication, to enable Windows authentication.
            WebOperationContext.Current.OutgoingRequest.Headers.Add(
                "X-FORMS_BASED_AUTH_ACCEPTED", "f");
        }
    }
    
  2. Add the NoImpersonation method to the ImpersonationTest class in the ImpersonationTest.cs file. The method gets the GUID of the application user and the GUID of the user to impersonate. The PSI calls are made by using the normal context of the application user.

    If the user account does not exist in Project Server, GetResourceUid returns an empty GUID (00000000-0000-0000-0000-000000000000), and the NoImpersonation method returns false. If the account is not valid, the application shows an error message and does not use the SimpleImpersonation method.

    internal bool NoImpersonation()
    {
        ResourceUtilities resUtilities = new ResourceUtilities(resourceClient);
        bool isValidUser = true;
    
        // Limit the scope of WCF calls to the client channel. 
        using (OperationContextScope scope = new OperationContextScope(
            resourceClient.InnerChannel))
        {
            // Add a Web request header to enable Windows authentication in 
            // multi-authentication installations.
            DisableFormsAuth(isWindowsUser);
    
            // Get the GUID of the application user.
            appUserUid = resourceClient.GetCurrentUserUid();
            appUserAccount = resUtilities.GetResourceName(appUserUid);
    
            impersonatedUserUid = resUtilities.GetResourceUid(impersonatedUserAccount);
        }
    
        Console.WriteLine("GUID of {0}\n\tfrom GetCurrentUserUid:\t{1}", 
            appUserAccount, appUserUid);
        Console.WriteLine("\nGUID of user to impersonate,\n\tfrom ReadResources:\t{0}", 
            impersonatedUserUid.ToString());
    
        if (impersonatedUserUid == Guid.Empty)
        {
            Console.ForegroundColor = ConsoleColor.Red;
            Console.WriteLine("Invalid account name for user to impersonate: {0}",
                impersonatedUserAccount);
            Console.ResetColor();
            isValidUser = false;
        }
        return isValidUser;
    }
    
  3. Add the SimpleImpersonation method to the ImpersonationTest class. The method sets the impersonation context, modifies the header for the outgoing Web request to use the impersonation context, and then gets the GUID of the impersonated user by using the GetCurrentUserUid method. The application limits the scope of impersonation with an OperationContextScope object. The InnerChannel property of the WCF resourceClient object is the Web request that contains the "PjAuth" header with the impersonation context.

    Note

    Although the OperationContextScope Class description on MSDN shows an example that uses the OperationContext class to modify message headers, you must use WebOperationContext for the Project Server services.

    internal void SimpleImpersonation()
    {
        SetImpersonationContext(isWindowsUser, impersonatedUserAccount, impersonatedUserUid, 
            trackingGuid, siteId, languageCulture, localeCulture);
        bool pass = false;
    
        // Limit the scope of impersonation to the WCF client channel. 
        using (OperationContextScope scope = new OperationContextScope(
            resourceClient.InnerChannel))
        {
            // Use WebOperationContext in the HTTP channel, not the OperationContext class.
            WebOperationContext.Current.OutgoingRequest.Headers.Remove("PjAuth");
            WebOperationContext.Current.OutgoingRequest.Headers.Add("PjAuth", contextString);
    
            // Add a Web request header to enable Windows authentication in 
            // multi-authentication installations.
            DisableFormsAuth(isWindowsUser);
    
            // The impersonated user is now making the PSI calls.
            Guid getCurrentUserUidResult = resourceClient.GetCurrentUserUid();
    
            Console.WriteLine("GUID from GetCurrentUserUid:\t{0}", getCurrentUserUidResult);
            pass = getCurrentUserUidResult.Equals(impersonatedUserUid);
        }
    
        if (pass)
        {
            Console.ForegroundColor = ConsoleColor.Green;
            Console.WriteLine("\nThe expected GUID and actual GUID match.");
            Console.ResetColor();
        }
        else
        {
            Console.ForegroundColor = ConsoleColor.Red;
            Console.WriteLine("\nFAIL! The expected and actual GUIDs do not match.");
            Console.ResetColor();
        }
    }
    

    Note

    If you try to use the System.Security.Principal.WindowsIdentity.GetCurrent().Name property before or after the SetImpersonationContext method, the value in both cases is the name of the calling user of the process, not the impersonated user. For PSI calls in a WCF-based application for Project Server, impersonation is set in the WebOperationContext header, not by a call to the WindowsIdentity.Impersonate method.

  4. Add the DisposeClients method to the ImpersonationTest class, to close the WCF client object.

    internal void DisposeClients()
    {
        resourceClient.Close();
    }
    

The ImpersonationTest class, which is implemented in both the ImpersonationTest.cs file and the Utilities.cs file, is complete.

If you have not done so already, add the RunTests method and the remainder of the Main method to the Program class in the Program.cs file. Add the app.config file, change the endpoint address in the client element to match the ProjectServer.svc address on your computer, and then compile and run the application. Figure 4 shows the Visual Studio Command Prompt window where WCFImpersonationTest is run three times, as follows:

  1. WCFImpersonationTest: shows the Usage message.

  2. WCFImpersonationTest –pwaUrl https://localhost/pwa -config: uses app.config to set the client endpoints. The account for impersonation is a valid Project Server user.

  3. WCFImpersonationTest –pwaUrl https://localhost/pwa: sets the client endpoints programmatically. The account for impersonation does not exist on Project Server.

Figure 4. Output of the WCFImpersonationTest application

Output of the WCFImpersonationTest application

Complete Code Example

Program.cs file   The following code is the complete content for the Program class in the WCFImpersonationTest application.

using System;
using System.Text;
using System.ServiceModel;

namespace WCFImpersonationTest
{
    class Program
    {
        static private Uri pwaUri;          // URI of Project WebApp.
        static private string pwaUrl;       // URL of Project WebApp.
        static private bool useConfig;      // If true, use app.config for client endpoints.
                                            // If false, programmatically configure endpoints.
        static private string argError;
        static private ConsoleColor foreColor;
        static private ImpersonationTest impersonationTest;

        static void Main(string[] args)
        {
            pwaUrl = string.Empty;
            pwaUri = null;
            useConfig = false;
            argError = null;

            foreColor = Console.ForegroundColor;
            Console.ForegroundColor = ConsoleColor.Gray;

            if (args.Length == 0)
            {
                Usage(argError);
            }
            else if (ParseCommandLine(args))
            {
                if (useConfig)
                    Console.WriteLine("Using app.config to set client endpoints.");
                else
                    Console.WriteLine("Programmatically setting client endpoints.");

                Console.WriteLine(@"Enter a valid Project Server user account, such as domain\username.");
                Console.Write("\nAccount for impersonation:\t");
                Console.ForegroundColor = ConsoleColor.Cyan;

                string userAccount = Console.ReadLine();
                Console.WriteLine();
                Console.ResetColor();

                impersonationTest = new ImpersonationTest(pwaUri, userAccount, useConfig);

                RunTests();

                // Close the client.
                impersonationTest. DisposeClients();
            }
            else
            {
                Usage(argError);
            }
            ExitApp();
        }

        static private void RunTests()
        {
            bool isValidUser;

            try
            {
                // Test 1 - Use Resource service without impersonation.
                Console.ForegroundColor = ConsoleColor.Yellow;
                Console.WriteLine("Test 1: No impersonation");
                Console.ResetColor();

                isValidUser = impersonationTest. NoImpersonation();

                // Test 2 - Use simple impersonation.
                if (isValidUser)
                {
                    Console.ForegroundColor = ConsoleColor.Yellow;
                    Console.WriteLine("\nTest 2: Simple impersonation");
                    Console.ResetColor();

                    impersonationTest. SimpleImpersonation();
                }
            }
            catch (EndpointNotFoundException e)
            {
                Console.ForegroundColor = ConsoleColor.Red;
                Console.WriteLine("\n***EndpointNotFoundException:");
                Console.WriteLine("   Check the app.config file or the programmatic"
                    + "\n   configuration for the correct endpoint address,"
                    + "\n   and the binding, endpoint, and contract names.\n");
                Console.WriteLine(e.ToString());
                Console.ResetColor();
            }
            catch (CommunicationException e)
            {
                Console.ForegroundColor = ConsoleColor.Red;
                Console.WriteLine("\n***CommunicationException:\n");
                Console.WriteLine(e.ToString());
                Console.ResetColor();
            }
        }

        static private void ExitApp()
        {
            Console.Write("\nPress any key to exit: ");
            Console.ReadKey(true);
            Console.ForegroundColor = foreColor;
        }

        static private bool ParseCommandLine(string[] args)
        {
            bool error = false;
            bool argErr = false;

            int argsLength = args.Length;

            for (int i = 0; i < args.Length; ++i)
            {
                if (error) break;

                switch (args[i].ToLower())
                {
                    case "/pwaurl":
                    case "-pwaurl":
                        i++;
                        if (i >= argsLength) return false;
                        pwaUrl = args[i];
                        pwaUrl = pwaUrl.ToLower();

                        if (pwaUrl.StartsWith("http") || pwaUrl.StartsWith("https"))
                        {
                            // Add a trailing slash, if it is not present.
                            if (pwaUrl.LastIndexOf("/") != pwaUrl.Length - 1)
                                pwaUrl += "/";

                            // Convert to a URI, for easy access to properties.
                            pwaUri = new Uri(pwaUrl);
                        }
                        else
                            argErr = true;
                        break;
                    case "/config":
                    case "-config":
                        useConfig = true;
                        break;

                    case "/?":
                    case "-?":
                        error = true;
                        break;

                    default:
                        argError = args[i];
                        error = true;
                        break;
                }
            }
            if (pwaUrl == string.Empty) error = true;
            if (argErr) error = true;

            return !error;
        }

        static private void Usage(String errorInfo)
        {
            if (errorInfo != null)
            {
                // A command-line argument error occurred, report it to the user.
                Console.WriteLine("Error: {0} is not a valid argument.\n", errorInfo);
            }

            Console.WriteLine(string.Format(
                "Usage:  [-?] -{0} {1} [-{2}]",
                "pwaUrl", @"<URL>", "config"));

            Console.WriteLine(
                "\n\tpwaUrl:\t\t\tExample: https://ServerName/pwa"
                + "\n\tConfig (optional):\tUse app.config to set endpoints");
        }
    }
}

ImpersonationTest.cs file   The following code is partial content for the ImpersonationTest class.

using System;
using System.ServiceModel;
using System.Globalization;
using System.ServiceModel.Web;
using PSLibrary = Microsoft.Office.Project.Server.Library;
using SvcResource;

namespace WCFImpersonationTest
{
    public partial class ImpersonationTest
    {
        private static ResourceClient resourceClient;   // Client of the SvcResource reference.
        private static string appUserAccount;           // Name of the application user.
        private static Guid appUserUid;                 // GUID of the application user.
        private static string impersonatedUserAccount;  // User to impersonate.
        private static Guid impersonatedUserUid;        // GUID of user to impersonate.
        private static String contextString = String.Empty; // Impersonation context.

        // Variables for setting the impersonation context.
        bool isWindowsUser = true;          // Use a Windows account in this test application.
        Guid trackingGuid = Guid.Empty;     // The tracking GUID for Queue operations is not used.
        Guid siteId = Guid.Empty;           // The Project Web Appsite ID is not used.
        CultureInfo languageCulture = null; // The language culture is not used.
        CultureInfo localeCulture = null;   // The locale culture is not used.

        // ImpersonationTest constructor. Set the user account and the client endpoints. 
        public ImpersonationTest(Uri uri, string user, bool useConfig)
        {
            impersonatedUserAccount = user;

            if (useConfig)
                ConfigClientEndpoints();
            else
                SetClientEndpoints(uri);
        }

        // Close the WCF client object.
         internal void DisposeClients()
        {
            resourceClient.Close();
        }

        // Get the GUIDs of the application user and the user to impersonate,  
        // without using impersonation.
        internal bool NoImpersonation()
        {
            ResourceUtilities resUtilities = new ResourceUtilities(resourceClient);
            bool isValidUser = true;

            // Limit the scope of WCF calls to the client channel. 
            using (OperationContextScope scope = new OperationContextScope(
                resourceClient.InnerChannel))
            {
                // Add a Web request header to enable Windows authentication
                // in multi-authentication installations.
                DisableFormsAuth(isWindowsUser);

                // Get the GUID of the application user.
                appUserUid = resourceClient.GetCurrentUserUid();
                appUserAccount = resUtilities.GetResourceName(appUserUid);

                impersonatedUserUid = resUtilities.GetResourceUid(impersonatedUserAccount);
            }

            Console.WriteLine("GUID of {0}\n\tfrom GetCurrentUserUid:\t{1}", 
                appUserAccount, appUserUid);
            Console.WriteLine("\nGUID of user to impersonate,\n\tfrom ReadResources:\t{0}", 
                impersonatedUserUid.ToString());

            if (impersonatedUserUid == Guid.Empty)
            {
                Console.ForegroundColor = ConsoleColor.Red;
                Console.WriteLine("Invalid account name for user to impersonate: {0}",
                    impersonatedUserAccount);
                Console.ResetColor();
                isValidUser = false;
            }
            return isValidUser;
        }

        // Get the GUID of the impersonated user, by using the GetCurrentUserUid method.
        internal void SimpleImpersonation()
        {
            SetImpersonationContext(isWindowsUser, impersonatedUserAccount, impersonatedUserUid, 
                trackingGuid, siteId, languageCulture, localeCulture);
            bool pass = false;

            // Limit the scope of impersonation to the WCF client channel. 
            using (OperationContextScope scope = new OperationContextScope(
                resourceClient.InnerChannel))
            {
                // Use WebOperationContext in the HTTP channel, not the OperationContext class.
                WebOperationContext.Current.OutgoingRequest.Headers.Remove("PjAuth");
                WebOperationContext.Current.OutgoingRequest.Headers.Add("PjAuth", contextString);
                
                // Add a Web request header to enable Windows authentication
                // in multi-authentication installations.
                DisableFormsAuth(isWindowsUser);
                
                // The impersonated user is now making the PSI calls.
                Guid getCurrentUserUidResult = resourceClient.GetCurrentUserUid();

                Console.WriteLine("GUID from GetCurrentUserUid:\t{0}", getCurrentUserUidResult);
                pass = getCurrentUserUidResult.Equals(impersonatedUserUid);
            }

            if (pass)
            {
                Console.ForegroundColor = ConsoleColor.Green;
                Console.WriteLine("\nThe expected GUID and actual GUID match.");
                Console.ResetColor();
            }
            else
            {
                Console.ForegroundColor = ConsoleColor.Red;
                Console.WriteLine("\nFAIL! The expected and actual GUIDs do not match.");
                Console.ResetColor();
            }
        }

        // For Windows authentication in a multi-authentication installation, 
        // the Web request must disable Forms authentication. 
        // Call DisableFormsAuth within an OperationContextScope.
        internal void DisableFormsAuth(bool isWindowsAuth)
        {
            WebOperationContext.Current.OutgoingRequest.Headers.Remove(
                "X-FORMS_BASED_AUTH_ACCEPTED");

            if (isWindowsAuth)
            {
                // Disable Forms authentication, to enable Windows authentication.
                WebOperationContext.Current.OutgoingRequest.Headers.Add(
                    "X-FORMS_BASED_AUTH_ACCEPTED", "f");
            }
        }

    }
}

Utilities.cs file   The following code includes partial content for the ImpersonationTest class and the complete ResourceUtilities class.

using System;
using System.Net;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
using System.Security.Principal;
using System.ServiceModel;
using System.Globalization;
using PSLibrary = Microsoft.Office.Project.Server.Library;
using SvcResource;

namespace WCFImpersonationTest
{
    // Contains methods to set the client endpoints and set the impersonation context.
    public partial class ImpersonationTest
    {
        // Add validation code for callback if using X509 certificate and HTTPS protocol. 
        private static bool CustomCertificateValidation(
            object sender, X509Certificate cert, X509Chain chain, SslPolicyErrors error)
        {
            return true;
        }

        // Use the endpoints defined in app.config to configure the client.
        public static void ConfigClientEndpoints()
        {
            resourceClient = new ResourceClient(
                "basicHttp_Resource");
        }

        // Programmatically set the endpoint of the Resource service client.
        public static void SetClientEndpoints(Uri pwaUri)
        {
            // Set the address of the front-end WCF router.
            const string svcRouter = "_vti_bin/PSI/ProjectServer.svc";

            string pwaUrl = pwaUri.Scheme + Uri.SchemeDelimiter + pwaUri.Host + ":"
                + pwaUri.Port + pwaUri.AbsolutePath;

            BasicHttpBinding binding = null;

            if (pwaUri.Scheme.Equals(Uri.UriSchemeHttps))
            {
                ServicePointManager.ServerCertificateValidationCallback +=
                    new RemoteCertificateValidationCallback( 
                        CustomCertificateValidation);

                binding = new BasicHttpBinding(BasicHttpSecurityMode.Transport);
            }
            else
            {
                binding = new BasicHttpBinding(
                    BasicHttpSecurityMode.TransportCredentialOnly);
            }

            binding.Name = "basicHttpConf";
            binding.SendTimeout = TimeSpan.MaxValue;
            ((BasicHttpBinding)binding).MaxReceivedMessageSize = 500000000;
            ((BasicHttpBinding)binding).ReaderQuotas.MaxNameTableCharCount = 500000000;
            ((BasicHttpBinding)binding).MessageEncoding = WSMessageEncoding.Text;
            ((BasicHttpBinding)binding).Security.Transport.ClientCredentialType =
                HttpClientCredentialType.Ntlm;

            EndpointAddress address = new EndpointAddress(pwaUrl + svcRouter);

            resourceClient = new ResourceClient(binding, address);
            resourceClient.ChannelFactory.Credentials.Windows.AllowedImpersonationLevel =
                TokenImpersonationLevel.Impersonation;
            resourceClient.ChannelFactory.Credentials.Windows.AllowNtlm = true;
        }

        // Set the impersonation context for calls to the PSI on behalf of 
        // the impersonated user.
        internal static void SetImpersonationContext(
                                bool isWindowsUser, String userNTAccount,
            Guid userGuid, Guid trackingGuid, Guid siteId,
            CultureInfo languageCulture, CultureInfo localeCulture)
        {
            contextString = GetImpersonationContext(
                                isWindowsUser, userNTAccount, userGuid,
                trackingGuid, siteId, languageCulture, localeCulture);
        }

        // Get the impersonation context.
        private static String GetImpersonationContext(
            bool isWindowsUser, String userNTAccount,
            Guid userGuid, Guid trackingGuid, Guid siteId,
            CultureInfo languageCulture, CultureInfo localeCulture)
        {
            PSLibrary.PSContextInfo contextInfo = new PSLibrary.PSContextInfo(
                isWindowsUser, userNTAccount, userGuid, trackingGuid,
                siteId, languageCulture, localeCulture);
            String contextInfoString = PSLibrary.PSContextInfo.SerializeToString( 
                contextInfo);

            return contextInfoString;
        }
    }

    // Utilities for using the Resource service.
    public class ResourceUtilities
    {
        private ResourceClient resourceClient;

        public ResourceUtilities(ResourceClient resClient)
        {
            resourceClient = resClient;
        }

        // Get the GUID for a Project Server account name. 
        public Guid GetResourceUid(String accountName)
        {
            Guid resourceUid = Guid.Empty;
            ResourceDataSet resourceDs = new ResourceDataSet();

            // Filter for the account name, which can be a 
            // Windows account or Project Server account.
            PSLibrary.Filter filter = new PSLibrary.Filter();
            filter.FilterTableName = resourceDs.Resources.TableName;

            PSLibrary.Filter.Field accountField = new PSLibrary.Filter.Field(
                    resourceDs.Resources.TableName,
                    resourceDs.Resources.WRES_ACCOUNTColumn.ColumnName);
            filter.Fields.Add(accountField);

            PSLibrary.Filter.FieldOperator op = new PSLibrary.Filter.FieldOperator(
                    PSLibrary.Filter.FieldOperationType.Equal,
                    resourceDs.Resources.WRES_ACCOUNTColumn.ColumnName, accountName);
            filter.Criteria = op;

            string filterXml = filter.GetXml();

            resourceDs = resourceClient.ReadResources(filterXml, false);

            // Return the account GUID.
            if (resourceDs.Resources.Rows.Count > 0)
                resourceUid = (Guid)resourceDs.Resources.Rows[0]["RES_UID"];

            return resourceUid;
        }

        // Get the account name from the user GUID. 
        public string GetResourceName(Guid accountUid)
        {
            string accountName = "[account unknown]";
            ResourceDataSet resourceDs = new ResourceDataSet();

            resourceDs = resourceClient.ReadResource(accountUid);

            if (resourceDs.Resources.Rows.Count > 0)
                accountName = (string)resourceDs.Resources.Rows[0]["WRES_ACCOUNT"];

            return accountName.ToLower();
        }
    }
}

App.config file   The following code is the configuration file for the application. It is not used when the application programmatically sets WCF bindings and endpoints for the resourceClient object. Change the endpoint address in the client element to match the ProjectServer.svc address on your computer. For information about setting WCF bindings and endpoints in an app.config file, see Walkthrough: Developing PSI Applications Using WCF.

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <system.serviceModel>
        <behaviors>
            <endpointBehaviors>
                <behavior name="basicHttpBehavior">
                    <clientCredentials>
                        <windows allowedImpersonationLevel="Impersonation" />
                    </clientCredentials>
                </behavior>
            </endpointBehaviors>
        </behaviors>
        <bindings>
            <basicHttpBinding>
                <binding name="basicHttpConf" sendTimeout="01:00:00" maxBufferSize="500000000"
                    maxReceivedMessageSize="500000000">
                    <readerQuotas maxDepth="32" maxStringContentLength="8192" maxArrayLength="16384"
                        maxBytesPerRead="4096" maxNameTableCharCount="500000000" />
                    <security mode="TransportCredentialOnly">
                        <transport clientCredentialType="Ntlm" realm="http"/>
                    </security>
                </binding>
            </basicHttpBinding>
        </bindings>
        <client>
            <endpoint address="https://ServerName/ProjectServerName/_vti_bin/PSI/ProjectServer.svc"
                behaviorConfiguration="basicHttpBehavior" binding="basicHttpBinding"
                bindingConfiguration="basicHttpConf" contract="SvcResource.Resource"
                name="basicHttp_Resource" />
        </client>
    </system.serviceModel>
</configuration>

See Also

Tasks

Walkthrough: Developing PSI Applications Using WCF

How to: Create a Project Server Event Handler and Log an Event

Concepts

Prerequisites for WCF-Based Code Samples

Other Resources

Using Impersonation in Project Server

How to: Use a Filter Parameter with PSI Methods

OperationContextScope Class