Cascade Skyline - with Microsoft Logo and Project Support header - author Brian Smith

  • Brian Smith's Microsoft Project Support Blog

    Which master pages can you edit in Project Server 2007?

    • 15 Comments

    A recent support incident showed that some of our documentation didn’t make it clear just what you can and can’t do with master pages.  There are two different types of sites in Project – PWA (the Project Web Access sites) and PWS (the project workspace sites).  For the PWA sites you are limited to making use of the default.master we already supply to give any extra pages the same look and feel, but you are not able to edit these default.master pages.  We also block SharePoint Designer from opening these sites – so you cannot use this tool against a PWA site.  You can however use SharePoint Designer against the PWA workspace sites – and edit the default.master.

    The reason for this difference is that it would be easy to break our application by editing our pages – and this would not make life good for a support engineer (and indeed for the customers who manage to break things!).  Also remember that even though the workspaces can be modified you shouldn’t add extra instances lists of the specific project type lists of issues, risks, dependencies and documents.  Details from these default lists feed through to the reporting database – if there are duplicates then this will break the workspace reporting feature.

    This topic is covered at http://msdn.microsoft.com/en-us/library/ms504195.aspx and this highlights another great way you can add to the sum of human knowledge on MSDN.  BruceVB added some community content based on his experience – and our own Jim Corbin has added a further comment.  Although I wouldn’t necessarily agree that our original document is incorrect – it is certainly not complete in telling you what you can and cannot do.  But even being able to use our default.master on new pages does make it easier to build custom Project Web Access solutions – even if you cannot edit the default.master.

    If you do try and update the PWA default.master through Site Actions, Site Settings then you may see the error message "The enablesessionstate attribute on the page directive is not allowed in this page." 

    You would need to reset to the site definition to get the site working again. 

    Please do use the community content features on MSDN – it certainly does help us to understand how we can improve our content – and thanks again BruceVB for bringing this particular problem to our attention.

  • Brian Smith's Microsoft Project Support Blog

    100 Posts Today!

    • 1 Comments

    According to my blog dashboard this is my 100th post!  So to celebrate I thought I would be totally self-indulgent (so nothing new there) and ignore Project and talk about something else.  It has been fun keeping the blog going and also great to have communication with so many of you – either through the blog (600+ comments, and rising) or on support cases or at the Project conference.  Keep the comments coming – and the answers too, as I will sometimes post questions I don’t have the answers for.

    If you didn’t already know I am from the UK, but moved to the Seattle area about 5 1/2 years ago and live on the Snoqualmie Valley.  My journey to and from the office gives me the chance to see some of the great beauty that the Pacific North West has to offer – and even this time of year when it has been known to rain we still get great mountain views at some point on most days.  One of my other interests is photography – and in fact my first career was in the research labs formulating developers and fixers when photography was still “wet”.  I knew that wouldn’t last – so on to computers and digital!  I was recently very fortunate to visit Hawaii for the first time – so took lots of photos – but it is testament to the beauty of the local countryside that for my desktop wallpaper I have chosen recent local pictures I have taken over the Hawaiian paradise.

    Mountains_24

    This first one is looking East from my house just before sunset.  The second one was last Friday evening – when a tree that will regularly have Bald Eagles perched in it had 7!  So back out with my camera to get some pictures – including this one of an immature bald eagle in flight.

    Eagles_11Crop

    I know we have other keen and very capable photographers in the Project community and Jack Dahlgren has posted some spectacular ones too http://zo-d.com/blog/archives/architecture/bay-bridge-at-sunrise.html.

    So posting 101 will be back on topic, but I might just post the occasional photo too…

  • Brian Smith's Microsoft Project Support Blog

    Impersonation Within a Project Server 2007 Server-Side Event Handler

    • 34 Comments

    One topic that our customers and partners seem to be having a hard time with is impersonation.  One reason is that the term we are using – impersonation – may already mean something to developers, and so the specific steps required to get impersonation working for Project Server 2007 and the PSI aren’t taken into consideration.  The SDK covers this in a couple of topics, but to show a couple more examples I’ve prepared a couple of event handlers that show what happens when you use and don’t use impersonation.

    What does impersonation achieve with event handlers?  It allows a an event handler to take action as a particular user – and this is useful for a few different reasons.

    1.  Whatever transaction that is carried out will be running as a chosen user – so it will look as though this user made any changes – and this will be reflected in any user accounts associated with the transaction.

    2.  The account running the event service (the SSP administrator) does not need to be a user in PWA and/or have any specific permissions.

    3.  For certain web services, particularly statusing, the call returns data based on the user making the request – so you need to pretend to be the user in question – rather than the SSP Admin.

    If you don’t impersonate and the SSP Admin is not a user in PWA you will get errors like The request failed with HTTP status 401: Unauthorized.  If this exception is not handled it will also mean the event handler crashes and if it is a “pre” event it will be canceled. Depending on where the event comes from the user may or may not get any notification that the cancellation occurred – so you need to code for this .  If they are a user, then they will need the right permissions for whichever web service they are calling – and if they call a statusing web service such as ReadStatus then the result will be the tasks for the SSP Admin – and not for the user who triggered the request.

    If you do impersonate then you don’t need to be a user in PWA, and a request to ReadStatus gets the tasks for the user who triggered the request.

    My code sample below shows the same code both with and without impersonation and catches the timesheet OnCreated event, and also the statusing  OnStatusUpdate.  The event handlers don’t do anything useful – just write to the event log both some data from the event payload and also make a request to a web service and write some details of the response – just to illustrate the differences seen when impersonating.

    As usual, my samples don’t include the level of exception handling you should include – and also include some hard coded values that you may wish to either use in application settings or to resolve at run-time using reflection.  Remember that with event handlers, and particularly the “pre” events, you want your code to be very light weight.  Also remember that in “pre” events that although you have access to more of the payload than post events you cannot modify it.

    For my sample responding the the timesheet OnCreated  event that does not use impersonation then if the SSP Admin is not a user we get the expected 401 error – whoever creates a timesheet, because the call to the ReadTimesheet fails.  If the SSP Admin is a user but is just a team member then the call to ReadTimesheet will work ONLY when the SSP Admin creates a timesheet – but will fail with a GeneralSecurityAccessDenied error from the event handler – as the SSP Admin cannot read anyone else’s timesheet.  If they are an administrator (or explicitly have the View Resource Timesheet global permission)  then they can successfully call the ReadTimesheet and the event handler will work.  If the same is done using impersonation then it will work regardless of the SSP Admin permissions – and they do not even need to be a user in Project Server.

    For the sample responding to the statusing OnStatusUpdate event that does not use impersonation we see slightly different behavior.  We still see failure if the SSP is not a user (401), but if they are a user then the call to ReadStatus will work – but does not return the number of tasks for the user who triggered the event – but always the number of tasks for the SSP Admin (so only correct if the user triggering the event was SSP Admin) and we also see that the name of the SSP Admin is the one returned from this call.

    Basically the steps needed for impersonation are that the code is running as the SSP Admin (which the event service always does – so no problem here) and that the call to web services goes to the SSP location of the web service and not the PWA location (see the code below for examples).  This also needs context setting for the resource and site uid – both of which are available from the contextInfo of the event itself.  The final point is that you need to create web services derived from the Project Server web services that has an override of the WebRequest to add a couple of items to the header.  I used classes already included in the ProjTool sample (in the Utils directory) with very minor changes. 

    Here is the code - and the only other thing you need to know is that you will need to add web references to the LoginWindows, TimeSheet and Statusing web services - and references to the Microsoft.Office.Project.Schema, Microsoft.Office.Project.Server.Library and Microsoft.Office.Project.Server.Events.Receivers.

    I’ve also attached a zip of the two .cs files as I noticed there is some trimming of the longer lines.

    First without impersonation…

    using System;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.Linq;
    using System.Net;
    using System.Text;
    using Microsoft.Office.Project.Server.Events;
    using PSLibrary = Microsoft.Office.Project.Server.Library;
    
    namespace EventHandlerWithNoImpersonation
    {
        public class MyEventHandler:TimesheetEventReceiver 
        {
            const string LOGINWINDOWS = "_vti_bin/PSI/LoginWindows.asmx";
            const string TIMESHEET = "_vti_bin/PSI/TimeSheet.asmx";
            private static WebSvcLoginWindows.LoginWindows loginWindows =
                new WebSvcLoginWindows.LoginWindows();
            private static WebSvcTimeSheet.TimeSheet timeSheet =
                new EventHandlerWithNoImpersonation.WebSvcTimeSheet.TimeSheet();
            private string baseUrl = "http://servername/cal/"; 
    
            public override void OnCreated(PSLibrary.PSContextInfo contextInfo, 
                TimesheetPostEventArgs e)
            {
                base.OnCreated(contextInfo, e);
    
                loginWindows.Url = baseUrl + LOGINWINDOWS;
                loginWindows.Credentials = CredentialCache.DefaultCredentials;
                timeSheet.Url = baseUrl + TIMESHEET;
                timeSheet.Credentials = CredentialCache.DefaultCredentials;
    
                WebSvcTimeSheet.TimesheetDataSet dsTimeSheet =
                    new EventHandlerWithNoImpersonation.WebSvcTimeSheet.TimesheetDataSet();
                //  As this call is using a specific UID it doesn't require 
                dsTimeSheet = timeSheet.ReadTimesheet(e.TsUID);
                WebSvcTimeSheet.TimesheetDataSet.HeadersRow headersRow = 
                    (WebSvcTimeSheet.TimesheetDataSet.HeadersRow)dsTimeSheet.Headers.Rows[0];
    
                string tsName = headersRow.TS_NAME;
    
                // Create an EventLog instance and assign its source.
                EventLog myLog = new EventLog();
                myLog.Source = "Timesheet Event Handler";
    
                // Get information from the event arguments, and 
                // write an entry to the Application event log. 
                string userName = contextInfo.UserName.ToString();
                string timesheetGuid = e.TsUID.ToString();
                int eventId = 3937;
                string logEntry;
    
                logEntry = "User: " + userName + "\nTimesheet UID: "
                    + timesheetGuid + "\nThis timesheet is called: " + tsName;
                myLog.WriteEntry(logEntry, EventLogEntryType.Information, eventId);
    
    
            }
            
        }
        public class MyEventHandler2 : StatusingEventReceiver
        {
            const string LOGINWINDOWS = "_vti_bin/PSI/LoginWindows.asmx";
            const string STATUSING = "_vti_bin/PSI/Statusing.asmx";
            private static WebSvcLoginWindows.LoginWindows loginWindows =
                new WebSvcLoginWindows.LoginWindows();
            private static WebSvcStatusing.Statusing statusing =
                new EventHandlerWithNoImpersonation.WebSvcStatusing.Statusing();
            private string baseUrl = "http://servername/PWA/";
    
            public override void OnStatusUpdating(Microsoft.Office.Project.Server.Library.PSContextInfo contextInfo, StatusUpdatePreEventArgs e)
            {
                base.OnStatusUpdating(contextInfo, e);
    
                loginWindows.Url = baseUrl + LOGINWINDOWS;
                loginWindows.Credentials = CredentialCache.DefaultCredentials;
                statusing.Url = baseUrl + STATUSING;
                statusing.Credentials = CredentialCache.DefaultCredentials;
    
                WebSvcStatusing.StatusingDataSet dsStatusing =
                    new EventHandlerWithNoImpersonation.WebSvcStatusing.StatusingDataSet();
                dsStatusing = statusing.ReadStatus(Guid.Empty,DateTime.MinValue,DateTime.MaxValue);
                WebSvcStatusing.StatusingDataSet.ResourcesRow resourceRow =
                    (WebSvcStatusing.StatusingDataSet.ResourcesRow)dsStatusing.Resources.Rows[0];
    
    
                int taskCount = dsStatusing.Tasks.Count;
                string statusOwner = resourceRow.RES_NAME;
    
                // Create an EventLog instance and assign its source.
                EventLog myLog = new EventLog();
                myLog.Source = "Statusing Event Handler";
    
                // Get information from the event arguments, and 
                // write an entry to the Application event log. 
                string userName = contextInfo.UserName.ToString();
                string statusingXml = e.ChangeXml;
                int eventId = 3938;
                string logEntry;
    
                logEntry = "User: " + userName + "\nChangeXML: "
                    + statusingXml + "\nThis statusing dataset has " + taskCount
                    + " tasks, and they belong to " + statusOwner;
                myLog.WriteEntry(logEntry, EventLogEntryType.Information, eventId);
    
    
            }
        }
    }
    and then with impersonation…
    using System;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.Linq;
    using System.Net;
    using System.Text;
    using Microsoft.Office.Project.Server.Events;
    using PSLibrary = Microsoft.Office.Project.Server.Library;
    
    namespace EventHandlerWithImpersonation
    {
        public class MyEventHandlerImp :TimesheetEventReceiver 
        {
            const string LOGINWINDOWS = "LoginWindows.asmx";
            const string TIMESHEET = "TimeSheet.asmx";
           
            
            private string baseUrl = "http://servername:56737/SharedServices1/PSI/";
    
            private static LoginWindowsDerived loginWindows = new LoginWindowsDerived();
            private static TimesheetDerived timeSheet = new TimesheetDerived();
           
            public override void OnCreated(PSLibrary.PSContextInfo contextInfo, TimesheetPostEventArgs e)
            {
                base.OnCreated(contextInfo, e);
                bool isWindowsAccount = contextInfo.IsWindowsUser;
                Guid trackingGuid = Guid.NewGuid();
                string lcid = "1033";
                string userNTAccount = contextInfo.UserName;
                Guid resourceGuid = new Guid(contextInfo.UserGuid.ToByteArray());
                Guid siteId = new Guid(contextInfo.SiteGuid.ToByteArray());
    
                LoginWindowsDerived.SetImpersonationContext(isWindowsAccount, userNTAccount, resourceGuid, trackingGuid, siteId, lcid);
                TimesheetDerived.SetImpersonationContext(isWindowsAccount, userNTAccount, resourceGuid, trackingGuid, siteId, lcid);
    
                
                loginWindows.Url = baseUrl + LOGINWINDOWS;
                loginWindows.Credentials = CredentialCache.DefaultCredentials;
                timeSheet.Url = baseUrl + TIMESHEET;
                timeSheet.Credentials = CredentialCache.DefaultCredentials;
    
                WebSvcTimeSheet.TimesheetDataSet dsTimeSheet =
                    new EventHandlerWithImpersonation.WebSvcTimeSheet.TimesheetDataSet();
                dsTimeSheet = timeSheet.ReadTimesheet(e.TsUID);
                WebSvcTimeSheet.TimesheetDataSet.HeadersRow headersRow = 
                    (WebSvcTimeSheet.TimesheetDataSet.HeadersRow)dsTimeSheet.Headers.Rows[0];
    
                string tsName = headersRow.TS_NAME;
    
                // Create an EventLog instance and assign its source.
                EventLog myLog = new EventLog();
                myLog.Source = "Impersonating Timesheet Event Handler";
    
                // Get information from the event arguments, and 
                // write an entry to the Application event log. 
                string userName = contextInfo.UserName.ToString();
                string timesheetGuid = e.TsUID.ToString();
                int eventId = 3939;
                string logEntry;
    
                logEntry = "User: " + userName + "\nTimesheet UID: "
                    + timesheetGuid + "\nThis timesheet is called: " + tsName;
                myLog.WriteEntry(logEntry, EventLogEntryType.Information, eventId);
    
    
            }
            
    
            
        }
        public class MyEventHandlerImp2 :StatusingEventReceiver
        {
            const string LOGINWINDOWS = "LoginWindows.asmx";
            const string STATUSING = "Statusing.asmx";
    
            private string baseUrl = "http://servername:56737/SharedServices1/PSI/";
    
            private static LoginWindowsDerived loginWindows = new LoginWindowsDerived();
            private static StatusingDerived statusing = new StatusingDerived();
    
            public override void OnStatusUpdating(PSLibrary.PSContextInfo contextInfo, StatusUpdatePreEventArgs e)
            {
                base.OnStatusUpdating(contextInfo, e);
    
                bool isWindowsAccount = contextInfo.IsWindowsUser;
                Guid trackingGuid = Guid.NewGuid();
                string lcid = "1033";
                string userNTAccount = contextInfo.UserName;
                Guid resourceGuid = new Guid(contextInfo.UserGuid.ToByteArray());
                Guid siteId = new Guid(contextInfo.SiteGuid.ToByteArray());
    
                LoginWindowsDerived.SetImpersonationContext(isWindowsAccount, userNTAccount, resourceGuid, trackingGuid, siteId, lcid);
                StatusingDerived.SetImpersonationContext(isWindowsAccount, userNTAccount, resourceGuid, trackingGuid, siteId, lcid);
    
    
                loginWindows.Url = baseUrl + LOGINWINDOWS;
                loginWindows.Credentials = CredentialCache.DefaultCredentials;
                statusing.Url = baseUrl + STATUSING;
                statusing.Credentials = CredentialCache.DefaultCredentials;
    
                WebSvcStatusing.StatusingDataSet dsStatusing =
                    new EventHandlerWithImpersonation.WebSvcStatusing.StatusingDataSet();
                dsStatusing = statusing.ReadStatus(Guid.Empty, DateTime.MinValue, DateTime.MaxValue);
                WebSvcStatusing.StatusingDataSet.ResourcesRow resourceRow = 
                    (WebSvcStatusing.StatusingDataSet.ResourcesRow)dsStatusing.Resources.Rows[0];
    
    
                int taskCount = dsStatusing.Tasks.Count;
                string statusOwner = resourceRow.RES_NAME;
    
                // Create an EventLog instance and assign its source.
                EventLog myLog = new EventLog();
                myLog.Source = "Impersonating Statusing Event Handler";
    
                // Get information from the event arguments, and 
                // write an entry to the Application event log. 
                string userName = contextInfo.UserName.ToString();
                string statusingXml = e.ChangeXml;
                int eventId = 3940;
                string logEntry;
    
                logEntry = "User: " + userName + "\nChangeXML: "
                    + statusingXml + "\nThis statusing dataset has " + taskCount 
                    + " tasks, and they belong to " + statusOwner;
                myLog.WriteEntry(logEntry, EventLogEntryType.Information, eventId);
    
            }
        }
        class LoginWindowsDerived : WebSvcLoginWindows.LoginWindows
        {
            private static String ContextString = String.Empty;
    
            protected override WebRequest GetWebRequest(Uri uri)
            {
                //here we are overriding the GetWebRequest method and adding the 2 web request headers
                WebRequest webRequest = base.GetWebRequest(uri);
                if (ContextString != String.Empty)
                {
                    webRequest.UseDefaultCredentials = true;
    
                    bool isImpersonating = (System.Security.Principal.WindowsIdentity.GetCurrent(true) != null);
                    webRequest.Credentials = CredentialCache.DefaultNetworkCredentials;
    
                    webRequest.Headers.Add("PjAuth", ContextString);
                    webRequest.Headers.Add("ForwardFrom", "/_vti_bin/psi/LoginWindows.asmx");
    
                    webRequest.PreAuthenticate = true;
                }
                return webRequest;
            }
    
            public static void SetImpersonationContext(bool isWindowsUser, String userNTAccount, Guid userGuid, Guid trackingGuid, Guid siteId, String lcid)
            {
                ContextString = GetImpersonationContext(isWindowsUser, userNTAccount, userGuid, trackingGuid, siteId, lcid);
            }
    
            private static String GetImpersonationContext(bool isWindowsUser, String userNTAccount, Guid userGuid, Guid trackingGuid, Guid siteId, String lcid)
            {
                PSLibrary.PSContextInfo contextInfo = new PSLibrary.PSContextInfo(isWindowsUser, userNTAccount, userGuid, trackingGuid, siteId, lcid);
                String contextString = PSLibrary.PSContextInfo.SerializeToString(contextInfo);
                return contextString;
            }
        }
        class TimesheetDerived : WebSvcTimeSheet.TimeSheet 
        {
            private static String ContextString = String.Empty;
    
            protected override WebRequest GetWebRequest(Uri uri)
            {
                //here we are overriding the GetWebRequest method and adding the 2 web request headers
                WebRequest webRequest = base.GetWebRequest(uri);
                if (ContextString != String.Empty)
                {
                    webRequest.UseDefaultCredentials = true;
    
                    bool isImpersonating = (System.Security.Principal.WindowsIdentity.GetCurrent(true) != null);
                    webRequest.Credentials = CredentialCache.DefaultNetworkCredentials;
    
                    webRequest.Headers.Add("PjAuth", ContextString);
                    webRequest.Headers.Add("ForwardFrom", "/_vti_bin/psi/timesheet.asmx");
    
                    webRequest.PreAuthenticate = true;
                }
                return webRequest;
            }
    
            public static void SetImpersonationContext(bool isWindowsUser, String userNTAccount, Guid userGuid, Guid trackingGuid, Guid siteId, String lcid)
            {
                ContextString = GetImpersonationContext(isWindowsUser, userNTAccount, userGuid, trackingGuid, siteId, lcid);
            }
    
            private static String GetImpersonationContext(bool isWindowsUser, String userNTAccount, Guid userGuid, Guid trackingGuid, Guid siteId, String lcid)
            {
                PSLibrary.PSContextInfo contextInfo = new PSLibrary.PSContextInfo(isWindowsUser, userNTAccount, userGuid, trackingGuid, siteId, lcid);
                String contextString = PSLibrary.PSContextInfo.SerializeToString(contextInfo);
                return contextString;
            }
        }
        class StatusingDerived : WebSvcStatusing.Statusing
        {
            private static String ContextString = String.Empty;
    
            protected override WebRequest GetWebRequest(Uri uri)
            {
                //here we are overriding the GetWebRequest method and adding the 2 web request headers
                WebRequest webRequest = base.GetWebRequest(uri);
                if (ContextString != String.Empty)
                {
                    webRequest.UseDefaultCredentials = true;
    
                    bool isImpersonating = (System.Security.Principal.WindowsIdentity.GetCurrent(true) != null);
                    webRequest.Credentials = CredentialCache.DefaultNetworkCredentials;
    
                    webRequest.Headers.Add("PjAuth", ContextString);
                    webRequest.Headers.Add("ForwardFrom", "/_vti_bin/psi/statusing.asmx");
    
                    webRequest.PreAuthenticate = true;
                }
                return webRequest;
            }
    
            public static void SetImpersonationContext(bool isWindowsUser, String userNTAccount, Guid userGuid, Guid trackingGuid, Guid siteId, String lcid)
            {
                ContextString = GetImpersonationContext(isWindowsUser, userNTAccount, userGuid, trackingGuid, siteId, lcid);
            }
    
            private static String GetImpersonationContext(bool isWindowsUser, String userNTAccount, Guid userGuid, Guid trackingGuid, Guid siteId, String lcid)
            {
                PSLibrary.PSContextInfo contextInfo = new PSLibrary.PSContextInfo(isWindowsUser, userNTAccount, userGuid, trackingGuid, siteId, lcid);
                String contextString = PSLibrary.PSContextInfo.SerializeToString(contextInfo);
                return contextString;
            }
        }
        
    }
    Enjoy!
  • Brian Smith's Microsoft Project Support Blog

    Project Server 2007 server-side event handling

    • 0 Comments

    Christophe has just posted on the Codeplex solution that allows easy handling of event handlers http://blogs.msdn.com/chrisfie/archive/2008/04/15/deploying-a-custom-event-handler-has-never-been-easier.aspx and I had my first chance to play with this yesterday.  It made life so much easier!  Please do take a look if you are working with server -side event handlers. 

    On a similar topic I have been working on event handlers that use impersonation to ensure that the event can get at the Project Server data in the right context - or use a specific user account to have the right permissions.  I'll be posting that in the next few days.  But for now - take a look at the Codeplex stuff.

    Technorati Tags: ,
  • Brian Smith's Microsoft Project Support Blog

    And here is the PowerPoint for the WebCast...

    • 2 Comments
    Seemed to hit some limit with everything in a zip - so here is the PowerPoint.
Page 76 of 96 (476 items) «7475767778»