Second Life of a Hungarian SharePoint Geek

June 30, 2016

How a Missing Permission for a Single User May Crash a Whole SharePoint Custom Web Application

Filed under: Bugs, Security, SP 2013 — Tags: , , — Peter Holpar @ 08:17

As part of our daily jobs we provide support for several custom-developed SharePoint-based web applications, like purchase order workflows, etc. either. Few of them were developed for MOSS 2007 / SharePoint 2010, and then migrated to SharePoint 2013. The code quality reflects often not the best programming practices as well, to tell the through.

The symptoms

A weird error has arisen in one of that applications from a such coding anti-pattern caused us some headache recently.

Every once in a while the users complained, that the custom pages they use otherwise (for example, a day earlier) throw an exception. Restarting the IIS application pool for the SharePoint web application made the pages to function again, however we considered this as a simple quick-and-dirty workaround until we find out the real reason behind the issue.

In the ULS logs we found the following entries:

Application error when access /_layouts/CustomPages/YourPage.aspx, Error=List ‘Config’ does not exist at site with URL ‘http://YourSharePoint’.   
Microsoft.SharePoint.Client.ResourceNotFoundException: List ‘Config’ does not exist at site with URL ‘
http://YourSharePoint’.
Getting Error Message for Exception System.Web.HttpUnhandledException (0x80004005): Exception of type ‘System.Web.HttpUnhandledException’ was thrown. —> System.TypeInitializationException: The type initializer for ‘Company.Custom.Config’ threw an exception. —> System.ArgumentException: List ‘Config’ does not exist at site with URL ‘
http://YourSharePoint’. —> Microsoft.SharePoint.Client.ResourceNotFoundException: List ‘Config’ does not exist at site with URL ‘http://YourSharePoint’.     — End of inner exception stack trace —     at Microsoft.SharePoint.SPListCollection.GetListByName(String strListName, Boolean bThrowException)     at Company.Custom.Config..cctor()     — End of inner exception stack trace —     at …
…Company.Custom.Config.get_ConfigValue()     at Company.Custom.Pages.Layouts.Company.Custom.YourPage.Page_Load(Object sender, EventArgs e)     at Microsoft.SharePoint.WebControls.UnsecuredLayoutsPageBase.OnLoad(EventArgs e)     at Microsoft.SharePoint.WebControls.LayoutsPageBase.OnLoad(EventArgs e)     at System.Web.UI.Control.LoadRecursive()     at System.Web.UI.Page.ProcessRequestMain(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint)     at System.Web.UI.Page.HandleError(Exception e)     at System.Web.UI.Page.ProcessRequestMain(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint)     at System.Web.UI.Page.ProcessRequest(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint)     at System.Web.UI.Page.ProcessReques…    …t()     at System.Web.UI.Page.ProcessRequest(HttpContext context)     at System.Web.HttpApplication.CallHandlerExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute()     at System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously)

Based on the logs the users have obviously problem with accessing the custom list called “Config”. At first we thought the list was deleted accidentally, or the users do not have permission on it, but after a quick check it turned out that the list is there, and the users have no problem accessing it via the web UI.

This list is used by the web application to persist specific application-wide settings as name-value pairs. A custom static class called “Config” is responsible to read up the configuration values from the SharePoint list into a static Dictionary field, and provide them to the other components of the application as static properties of the class. In the original implementation of the class the configuration values were read up from the list in the static constructor and that is without elevated permission, since (at least theoretically) all users should have at least read permissions to the list.

What’s wrong with this approach? Why can’t users that definitely do have permission to the list access it?

If there is at least a single user without permission to the list, and this user happens to be the first one that tries to access the Config object after the application pool of the web application (re)started or recycled by IIS, the static constructor must throw obviously a TypeInitializationException exception, as there is an unhandled exception in the static constructor. The real exception, that caused the problem is included in the InnerException of the TypeInitializationException exception. But why do the other users (having permissions to the list) become the same error?

The answer can be found on MSDN:

”If a static constructor throws an exception, the runtime will not invoke it a second time, and the type will remain uninitialized for the lifetime of the application domain in which your program is running.”

In our case the application domain means the process of the IIS application pool. Although it is not documented in the article mentioned above, but based on our experience on each further access on any static methods / properties the very same exception would be thrown as the first time.

It is not something SharePoint-specific, one can have the same issue with broken SQL Server connection as well.

To illustrate the behavior I extended the code sample I found here, provided by Jon Skeet, author of one of my favorite books, C# in Depth:

  1. using System;
  2. using System.Threading;
  3.  
  4. public static class Config
  5. {
  6.     static Config()
  7.     {
  8.         DateTime now = DateTime.Now;
  9.  
  10.         Console.WriteLine("Static constructor invoked at {0:s}", now);
  11.         throw new ApplicationException(string.Format("List not found, time stamp: {0:s}", now));
  12.     }
  13.  
  14.     public static string Value
  15.     {
  16.         get { return "a value"; }
  17.     }
  18. }
  19.  
  20. class StaticConfigTest
  21. {
  22.     static void Main()
  23.     {
  24.         for (int i = 0; i < 5; i++)
  25.         {
  26.             DateTime now = DateTime.Now;
  27.             Console.WriteLine("Config value read at {0:s}", now);
  28.  
  29.             try
  30.             {
  31.                 var value = Config.Value;
  32.             }
  33.             catch (Exception e)
  34.             {
  35.                 var text = string.Format("'{0}' ({1})", e.Message, e.GetType().Name);
  36.                 var ie = e.InnerException;
  37.                 if (ie != null)
  38.                 {
  39.                     text += string.Format(" '{0}' ({1})", ie.Message, ie.GetType().Name);
  40.                 }
  41.                 Console.WriteLine(text);
  42.             }
  43.  
  44.             // wait 5 secs
  45.             Thread.Sleep(5000);
  46.         }
  47.     }
  48. }

When you execute this code, it will output something like this:

image

The main points you should notice:

  • There is only a single line of “Static constructor invoked”. It means, the static constructor is only invoked once.
  • The main exception is always a TypeInitializationException, the “real” exception (in our case, it is an ApplicationException) is provided in the InnerException property. Based on the ULS logs above, the InnerException is logged out by SharePoint (that is a good thing), and it caused us a bit of confusion (that is not so good, of course).
  • The exception thrown by accessing the static members is always the same as the first one, compare the time stamps values in the output.

How to solve the problem once we know the real reason of the issue?

As we wanted to remedy the issue as fast as possible, the very first step was to resolve the direct cause of the problem, so we granted permissions on the Config list for the few users who did not have access earlier.

For a long-term solution, I think the most important step was to refactor the Config class. The code of the static constructor was transformed to a private static method (let’s call it InitializeIfNeeded). In this method we lock the Dictionary object used to store the configuration values, to provide a kind of  thread safety and support potential concurrent calls. We check next, if the configuration values were already initialized (via a static boolean field called isConfigInitialized). If they were, we exit from the InitializeIfNeeded method. If they were not, we read up the values from the SharePoint list into the Dictionary object, and set the value of the isConfigInitialized field to true. On accessing each of the static properties of the Config class representing the individual configuration values, we invoke first the InitializeIfNeeded method, to ensure the values are available. Using this simple step we can ensure, that users that do have permission on the SharePoint list can accesd the configuration values. Users without permission may have still the problem that the values cannot be read from the list, if they are the first ones to visit the pages. If they are visiting the pages after the values were already initialized (via a visit of a user with permission), they can of course access the configuration values as well.

As next step, in the InitializeIfNeeded method we embedded the code responsible for reading up the values from the SharePoint list into the Dictionary object into an elevated permission block to ensure all user (even the ones without direct permission to the list) can read the values up.

After testing the new version and deploying it into the production system, we can revoke the direct permissions from the Config list for the standard users, and let only administrators to read and change configuration value via the web UI.

May 16, 2016

Project Server Displays Incorrect Effective Rights for Resources

Filed under: Bugs, PS 2013, Security — Tags: , , — Peter Holpar @ 15:55

We observed the following – in my opinion buggy – behavior in case of Project Server 2013 (patch state: 2016 March CU):

In our project web sites we have a web page that should display the name of the project and the title of the project owner. The name is displayed using client-side technologies, that means JavaScript and the Project Server JavaScript object model. See the code snippets below. Note, that these are parts of an AngularJS applications and cannot be used alone, but only part of the whole application. I show the code only to provide you an overview, about what I’m writing here.

The “business logic” from the controller:

  1. var promise = OurCustomService.getProjInfo($scope);
  2. promise.then(function (pi) {
  3.     var projName = pi.project.get_name();
  4.     // for some users the get_owner() mehtod returns null
  5.     var projManName = pi.project.get_owner().get_title();            
  6. }, function (errorMsg) {
  7.     console.log("Error: " + errorMsg);
  8. });
  9.  
  10.     }, function (errorMsg) {
  11.         console.log("Error: " + errorMsg);
  12.     });
  13. });

…and the service code:

  1. this.getProjInfo = function ($scope) {
  2.     var deferred = $q.defer();
  3.  
  4.     var ctx = new SP.ClientContext.get_current();
  5.  
  6.     var projContext = PS.ProjectContext.get_current();
  7.     projContext.set_isPageUrl(ctx.get_isPageUrl);
  8.     var proj = projContext.get_projects().getById($scope.projectId);
  9.     projContext.load(proj, "Name", "Owner.Title");
  10.  
  11.     projContext.executeQueryAsync(
  12.         function () {
  13.             deferred.resolve(
  14.                 {
  15.                     project: proj
  16.                 });
  17.         },
  18.         function (sender, args) {
  19.             deferred.reject('Request failed. ' + args.get_message() + '\n' + args.get_stackTrace());
  20.         }
  21.     );
  22.  
  23.     return deferred.promise;
  24. };

We found that this solution does not work for a lot of our users. Having a look via Internet Explorer (F12) Developer Tools in the code running with their credentials I found that the object returned by the pi.project.get_owner() expression is null, causing an exception as I want to access the get_title() method of this null object. It was obviously a security issue. To be able to access the title of the project owner (that is a resource as well), the user should have the View Enterprise Resource Data category permission in relation to the project owner resource.

When checking the Owner property of the Project via REST (the Guid in the URL is the ID of the given project):

http://YourProjectServer/PWA/_api/ProjectServer/Projects(‘1EF03FA9-2F7A-E411-80D4-005056B47337&#8217;)/Owner

the users having the problem received null as result, however, other users having more permissions (including the required one) received the full info of the project owner as expected.

Similarly, we have checked the resources available for the user via the REST query:

http://YourProjectServer/PWA/_api/ProjectServer/EnterpriseResources

The result for the “problematic” users did not contained the resource that is the project owner, however for the other users (the ones who had no problem with the AngularJS application mentioned above) the result included this resource as well.

No problem, it sounds OK up to this point.

However, when we selected any of  “problematic” these users on the Manage Users page in PWA Settings, clicked Check Effective Rights, change the Permission Type to Category Permission – Resource, and selected the selected the project owner (the one, the user has in practice no permission at all) in the Security Object – Resource list, the report shows, that the user has View Enterprise Resource Data permission via a group (let’s say All Users) and a category (let’s say My Project Team). Then we clicked other resources in the Security Object – Resource list as well, and found, that based on the report, the user should have View Enterprise Resource Data permission to almost all of these resources either, although based on the REST query above (http://YourProjectServer/PWA/_api/ProjectServer/EnterpriseResources) he has permission only a very few of them.

That is pretty strange. The users are really member of the All Users group, and the My Project Team category is really assigned to the All Users group.

The resources affected by the My Project Team category are selected by the “They are members of a Project Team on a project owned by the User” rule:

image

Members of the All Users group have View Enterprise Resource Data permission on resources included in the My Project Team category:

image

The resources displayed by the Effective Rights page as ones the “problematic” users have permission to are however no team members of the users at all!

How is it possible? In this post I don’t want to bore you with very deep technical details (I plan to post these details in a follow-up post later), the most important facts are, that the objects and stored procedures used to check the permissions when you want to access a resource differ from the ones used to display the effective rights.

For example, when checking the “They are members of a Project Team on a project owned by the User” rule, the pub.MSP_WEB_FN_SEC_ResourcesInCategoryRule3 table-valued function is used, where @res_uid parameter is the resource ID of the current user. It should return the ID of all of the resources that are affected by this category rule:

SELECT RES_UID AS OBJ_UID
FROM pub.MSP_ASSIGNMENTS
WHERE WRES_UID_MANAGER = @res_uid
  AND WASSN_DELETED_IN_PROJ = 0

UNION

SELECT PR.RES_UID AS OBJ_UID
FROM pub.MSP_PROJECTS P
INNER JOIN pub.MSP_PROJECT_RESOURCES PR ON PR.PROJ_UID = P.PROJ_UID
WHERE P.WRES_UID = @res_uid

That means, resources returned by the query if the resource that belongs to the current user (the one that wants to access another resource) is either an assignment owner of  a non-deleted project task assignment where the target resource (the one the current user want to access) is the assignment resource (first part of the UNION query), or there is a project that has the current user as project manager and the target resource as project resource (second part of the UNION query). That sounds logically.

On the contrary, when displaying the effective rights, the pub.MSP_WEB_FN_SEC_GetEffectiveCategories_NONCLAIMSCOMPLIANT tabled-value function is called by the pub.MSP_WEB_SP_SEC_ReadUserEffectiveRightsWithCategoryPermissions_NONCLAIMSCOMPLIANT stored procedure. This function uses the following condition to check the “They are members of a Project Team on a project owned by the User” rule, where @res_uid parameter is the resource ID of the current user, and the @wsec_obj_uid parameter is the ID of the target resource. It should insert the value 3 into the temporary @rule_table is the target resource is affected by the category rule:

IF EXISTS (SELECT TOP 1 RES_UID FROM MSP_ASSIGNMENTS WHERE WRES_UID_MANAGER = @res_uid AND RES_UID = @wsec_obj_uid)
    OR EXISTS (SELECT TOP 1 RES_UID FROM MSP_PROJECT_RESOURCES WHERE RES_UID = @wsec_obj_uid)
    OR EXISTS (SELECT TOP 1 WRES_UID as RES_UID FROM MSP_PROJECTS WHERE WRES_UID = @res_uid)
BEGIN
    INSERT INTO @rule_table(WSEC_OBJ_RULE_TYPE) VALUES (3)
END

As far as I see, this condition is wrong. It says that there should be an assignment having the current user as an assignment owner and the target resource as assignment resource (see first part of the UNION in the first SQL query above, differs in checking the WASSN_DELETED_IN_PROJ flag), or there is any project, where the target resource is a resource, or there is any project where the current user is the project manager (compare with the second part of the UNION query above, condition this time is total wrong). It means we may receive a false positive on the Effective Rights page for each resources, that are resources on any project, and for all resources if the current user (the one we check the effective rights for) is a project manager of any project. In fact, we should receive a positive value in all of these cases (as long as there is no explicit deny), it is a false positive, when there is no other, valid positive value via other categories.

I think one should re-arrange the condition like this:

IF EXISTS (SELECT TOP 1 RES_UID FROM MSP_ASSIGNMENTS WHERE WRES_UID_MANAGER = @res_uid AND RES_UID = @wsec_obj_uid AND WASSN_DELETED_IN_PROJ = 0)
    OR EXISTS (SELECT TOP 1 PR.RES_UID FROM MSP_PROJECTS P INNER JOIN MSP_PROJECT_RESOURCES PR ON PR.PROJ_UID = P.PROJ_UID
    WHERE PR.RES_UID = @wsec_obj_uid AND P.WRES_UID = @res_uid)
BEGIN
    INSERT INTO @rule_table(WSEC_OBJ_RULE_TYPE) VALUES (3)
END

or even better, one could simply re-use the logic implemented in the pub.MSP_WEB_FN_SEC_ResourcesInCategoryRule3 table-valued function:

IF EXISTS (SELECT TOP 1 OBJ_UID FROM pub.MSP_WEB_FN_SEC_ResourcesInCategoryRule3(@res_uid) WHERE OBJ_UID = @wsec_obj_uid)
BEGIN
    INSERT INTO @rule_table(WSEC_OBJ_RULE_TYPE) VALUES (3)
END

March 3, 2016

Access Denied when Setting Group Properties as Group Owner

Filed under: Bugs, Security, SP 2013 — Tags: , , — Peter Holpar @ 22:10

We have a SharePoint group whose members should administer group membership of other, business-related SharePoint groups. We assigned this group as a Group Owner to the other groups, thus they have the required permissions to group administration.

This week one of the group administrators was to change the description of one of the groups. Actually, it is not part they mission, however I don’t see any problem with that. SharePoint itself defines the rule of the group owner on the Change Group Settings page:

The owner can change anything about the group such as adding and removing members or deleting the group.

Yes, they can even delete the group, I have tested it.

However in this case the member of the group owner group received an Access Denied error, when he submitted the changes to the server. To be able to understand the reason, let’s first see the corresponding ULS logs:

03.01.2016 17:27    w3wp.exe (0x339C)                           0x439C    SharePoint Foundation             General                           8e2s    Medium      Unknown SPRequest error occurred. More information: 0x80070005    e552649d-8539-f075-b98b-accc2d7bd4f5
03.01.2016 17:27    w3wp.exe (0x339C)                           0x439C    SharePoint Foundation             General                           aix9j    High        SPRequest.AddOrUpdateItem: UserPrincipalName=i:0).w|s-1-5-21-1613396233-3282607421-4023646941-2481, AppPrincipalName= ,bstrUrl=
http://YourSharePoint ,bstrListName={F0FF0E7F-61FE-4BBF-993A-2F396E44E133} ,bAdd=False ,bSystemUpdate=False ,bPreserveItemVersion=False ,bPreserveItemUIVersion=False ,bUpdateNoVersion=False ,pbstrNewDocId=00000000-0000-0000-0000-000000000000 ,bHasNewDocId=False ,bstrVersion=23 ,bCheckOut=False ,bCheckin=False ,bUnRestrictedUpdateInProgress=False ,bMigration=False ,bPublish=False ,bstrFileName=<null>    e552649d-8539-f075-b98b-accc2d7bd4f5
03.01.2016 17:27    w3wp.exe (0x339C)                           0x439C    SharePoint Foundation             General                           ai1wu    Medium      System.UnauthorizedAccessException: Access is denied. (Exception from HRESULT: 0x80070005 (E_ACCESSDENIED)), StackTrace:    at Microsoft.SharePoint.SPListItem.AddOrUpdateItem(Boolean bAdd, Boolean bSystem, Boolean bPreserveItemVersion, Boolean bNoVersion, Boolean bMigration, Boolean bPublish, Boolean bCheckOut, Boolean bCheckin, Guid newGuidOnAdd, Int32& ulID, Object& objAttachmentNames, Object& objAttachmentContents, Boolean suppressAfterEvents, String filename, Boolean bPreserveItemUIVersion)     at Microsoft.SharePoint.SPListItem.UpdateInternal(Boolean bSystem, Boolean bPreserveItemVersion, Guid newGuidOnAdd, Boolean bMigration, Boolean bPublish, Boolean bNoVersion, Boolean bCheckOut, Boolean bCheckin, Boolean suppressAfterEvents, String filename, Boolean bPreserveItemUIVersion)     at …    e552649d-8539-f075-b98b-accc2d7bd4f5
03/01/2016 17:27:28.61*    w3wp.exe (0x339C)                           0x439C    SharePoint Foundation             General                           ai1wu    Medium      …Microsoft.SharePoint.SPListItem.Update()     at Microsoft.SharePoint.ApplicationPages.CBaseNewGroup.UpdateAdditionalProperties(Int32 groupId)     at Microsoft.SharePoint.ApplicationPages.EditGroup.DoOperation()     at Microsoft.SharePoint.ApplicationPages.CBaseNewGroup.BtnOK_Click(Object sender, EventArgs e)     at System.Web.UI.WebControls.Button.RaisePostBackEvent(String eventArgument)     at System.Web.UI.Page.ProcessRequestMain(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint)     at System.Web.UI.Page.ProcessRequest(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint)     at System.Web.UI.Page.ProcessRequest()     at System.Web.UI.Page.ProcessRequest(HttpContext context)     at System.Web.HttpApplication.CallHandlerExecutionStep.Syste…    e552649d-8539-f075-b98b-accc2d7bd4f5
03/01/2016 17:27:28.61*    w3wp.exe (0x339C)                           0x439C    SharePoint Foundation             General                           ai1wu    Medium      …m.Web.HttpApplication.IExecutionStep.Execute()     at System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously)     at System.Web.HttpApplication.PipelineStepManager.ResumeSteps(Exception error)     at System.Web.HttpApplication.BeginProcessRequestNotification(HttpContext context, AsyncCallback cb)     at System.Web.HttpRuntime.ProcessRequestNotificationPrivate(IIS7WorkerRequest wr, HttpContext context)     at System.Web.Hosting.PipelineRuntime.ProcessRequestNotificationHelper(IntPtr rootedObjectsPointer, IntPtr nativeRequestContext, IntPtr moduleData, Int32 flags)     at System.Web.Hosting.PipelineRuntime.ProcessRequestNotification(IntPtr rootedObjectsPointer, IntPtr nativeRequestContext, IntPtr moduleData, Int32 flags)     at System.Web.Hosting.UnsafeII…    e552649d-8539-f075-b98b-accc2d7bd4f5
03/01/2016 17:27:28.61*    w3wp.exe (0x339C)                           0x439C    SharePoint Foundation             General                           ai1wu    Medium      …SMethods.MgdIndicateCompletion(IntPtr pHandler, RequestNotificationStatus& notificationStatus)     at System.Web.Hosting.UnsafeIISMethods.MgdIndicateCompletion(IntPtr pHandler, RequestNotificationStatus& notificationStatus)     at System.Web.Hosting.PipelineRuntime.ProcessRequestNotificationHelper(IntPtr rootedObjectsPointer, IntPtr nativeRequestContext, IntPtr moduleData, Int32 flags)     at System.Web.Hosting.PipelineRuntime.ProcessRequestNotification(IntPtr rootedObjectsPointer, IntPtr nativeRequestContext, IntPtr moduleData, Int32 flags)      e552649d-8539-f075-b98b-accc2d7bd4f5
03.01.2016 17:27    w3wp.exe (0x339C)                           0x439C    SharePoint Foundation             General                           ftd0    Medium      Access Denied. Exception: Access is denied. (Exception from HRESULT: 0x80070005 (E_ACCESSDENIED)), StackTrace:   at Microsoft.SharePoint.Library.SPRequestInternalClass.AddOrUpdateItem(String bstrUrl, String bstrListName, Boolean bAdd, Boolean bSystemUpdate, Boolean bPreserveItemVersion, Boolean bPreserveItemUIVersion, Boolean bUpdateNoVersion, Int32& plID, String& pbstrGuid, Guid pbstrNewDocId, Boolean bHasNewDocId, String bstrVersion, Object& pvarAttachmentNames, Object& pvarAttachmentContents, Object& pvarProperties, Boolean bCheckOut, Boolean bCheckin, Boolean bUnRestrictedUpdateInProgress, Boolean bMigration, Boolean bPublish, String bstrFileName, ISP2DSafeArrayWriter pListDataValidationCallback, ISP2DSafeArrayWriter pRestrictInsertCallback, ISP2DSafeArrayWriter pUniqueFieldCallback)  …    e552649d-8539-f075-b98b-accc2d7bd4f5
03/01/2016 17:27:28.61*    w3wp.exe (0x339C)                           0x439C    SharePoint Foundation             General                           ftd0    Medium      …   at Microsoft.SharePoint.Library.SPRequest.AddOrUpdateItem(String bstrUrl, String bstrListName, Boolean bAdd, Boolean bSystemUpdate, Boolean bPreserveItemVersion, Boolean bPreserveItemUIVersion, Boolean bUpdateNoVersion, Int32& plID, String& pbstrGuid, Guid pbstrNewDocId, Boolean bHasNewDocId, String bstrVersion, Object& pvarAttachmentNames, Object& pvarAttachmentContents, Object& pvarProperties, Boolean bCheckOut, Boolean bCheckin, Boolean bUnRestrictedUpdateInProgress, Boolean bMigration, Boolean bPublish, String bstrFileName, ISP2DSafeArrayWriter pListDataValidationCallback, ISP2DSafeArrayWriter pRestrictInsertCallback, ISP2DSafeArrayWriter pUniqueFieldCallback).    e552649d-8539-f075-b98b-accc2d7bd4f5
03.01.2016 17:27    w3wp.exe (0x339C)                           0x439C    SharePoint Foundation             Micro Trace                       uls4    Medium      Micro Trace Tags: 0 nasq,6 agb9s,18 ak8dj,12 b4ly,0 b4ly,41 aix9j,0 ai1wu,0 ftd0    e552649d-8539-f075-b98b-accc2d7bd4f5
03.01.2016 17:27    w3wp.exe (0x339C)                           0x439C    SharePoint Foundation             Monitoring                        b4ly    Medium      Leaving Monitored Scope (Request (POST:
http://YourSharePoint/_layouts/15/editgrp.aspx?Group=YourGroup&Source=http%3A%2F%2FYourSharePoint%2F%5Flayouts%2F15%2Fpeople%2Easpx%3FMembershipGroupId%3D6650)). Execution Time=81,9053366451775    e552649d-8539-f075-b98b-accc2d7bd4f5
03.01.2016 17:27    w3wp.exe (0x339C)                           0x439C    SharePoint Foundation             Claims Authentication             amge7    Medium      SPFederationAuthenticationModule.IsRedirectToLogOnPage: Detected a redirection but the redirect is not to a known signin page:
http://YourSharePoint/_layouts/15/AccessDenied.aspx    e552649d-8539-f075-b98b-accc2d7bd4f5
03.01.2016 17:27    w3wp.exe (0x339C)                           0x49EC    SharePoint Foundation             Monitoring                        nasq    Medium      Entering monitored scope (Request (GET:
http://YourSharePoint/_layouts/15/AccessDenied.aspx)). Parent No    

As you can see, on the page editgrp.aspx we have an UnauthorizedAccessException: Access is denied. However, at first it seems to be not group-related, as it is thrown in the SPListItem.AddOrUpdateItem method. What’s that?

As I am aware of that the rich text description of a group is stored in the Notes field corresponding list item in the hidden user information list,and not in the SPGroup object itself, at this point I had already an idea, what might be the reason of the the issue. But let’s prove that scientifically!

Later in the stack trace we found the UpdateAdditionalProperties method of the Microsoft.SharePoint.ApplicationPages.CBaseNewGroup class, called by the DoOperation method of the Microsoft.SharePoint.ApplicationPages.EditGroup class.

The DoOperation method updates group properties stored in the SPGroup object itself, but the UpdateAdditionalProperties method updates the information stored in the list item fields in the user information list, like the description of the group, and other fields requested in optional query string parameters:

  1. protected void UpdateAdditionalProperties(int groupId)
  2. {
  3.     SPListItem itemById = base.Web.SiteUserInfoList.GetItemById(groupId);
  4.     string text = this.txtGrpDescription.Text;
  5.     itemById["Notes"] = text;
  6.     int num = 1;
  7.     while (true)
  8.     {
  9.         string str2 = base.Request.QueryString["FieldName" + num.ToString(CultureInfo.InvariantCulture)];
  10.         if (string.IsNullOrEmpty(str2))
  11.         {
  12.             break;
  13.         }
  14.         string str3 = base.Request.QueryString["FieldValue" + num.ToString(CultureInfo.InvariantCulture)];
  15.         itemById[str2] = str3;
  16.         num++;
  17.     }
  18.     itemById.Update();
  19. }

A minor detour about the user information list. As you know, it is a hidden list on the SharePoint site collection root web site, and typically accessed via the URL http://YourSharePoint/_catalogs/users/simple.aspx. However, it is a common misunderstanding, even in case of well-known SharePoint experts, that “This list is only visible to and accessible by administrators”. Based on my experience, it is true only for the webpage (_catalogs/users/simple.aspx), but not for the list itself. The list inherits its permissions from the root site, meaning anyone having read permission on the root site, can access the list.

For example, simply by using REST:

http://YourSharePoint/_api/Web/SiteUserInfoList

or (assuming the Guid is the ID of the list):

http://YourSharePoint/_api/Lists/GetById(‘6749e2d2-ca87-445f-8fc1-b7f7a4e410ad&#8217;)

However

http://YourSharePoint/_api/Lists/GetByTitle(‘User%20Information%20List&#8217;)

does not work, because it is a hidden list.

One can even list all site users via the URL:

http://YourSharePoint/_api/Web/SiteUserInfoList/Items?$select=Title,Name

To be sincerely, I’m not sure if it is a feature or simply a security vulnerability. I’ve also tried to update the list items of the user list from the client side using the managed client object model by the credentials of a user having write permission on the site, but up to know I have not “succeeded”, received access denied. But I don’t give up. Winking smile

After the detour, let’s back to our issue. Since in our case the user had no write permissions on the root site (and thus no write permissions on the list and the list items), the UpdateAdditionalProperties method was not able to set the group description (the Notes field of the list item) and resulted in the Access Denied error. Other members of the same owner group, having the write permission on the root site level, have no such problems.

It is important to point out, that even if the user has no write permission and get the Access Denied error, other changes in the group configuration (like the Name or “Who can edit the membership of the group“) are updated for the group, as they are already saved by the DoOperation method before the exception was thrown by the UpdateAdditionalProperties method. You can prove that by navigating back to the Change Group Settings page, and reload it in browser via F5. The same is true, when one sets the group properties by code, like the managed or JavaScript client object model or by REST request. The owner can change group properties stored in the SPGroup object without error, but not the list item in the user list.

I consider this behavior to be a bug, and I think the UpdateAdditionalProperties method should include an elevated privileges code block to enable group owners to change the properties stored in the list item fields as well.

That’s all I wanted to share about the error itself, but if you have time, you can read on for another story.

There is an other misunderstanding I read in other SharePoint blogs while researching the issue. One may think, that setting a group as its own group owner (Solution 1) and selecting the “Group Members” option for “Who can edit the membership of the group“ (Solution 2) are interchangeable (see Scott Baitz stating his Solution 2 “will provide the same functionality as the solution above”, Solution 1). That is definitely false. Let’s see the difference, starting with Solution 2, then comparing the extra permissions one get via Solution 1.

Solution 2

If you allow group members to edit group membership by implementing Solution 2, the group members can add other users to the group or remove users, even themselves from the group. (Note: If they removed themself  from the group, that action would be of course not reversible. They can not add themself back to the group, only if they have that permission via the group owner group or they have other administrative permission, like site collection administrator, or full control permission on the site.)

The group members have access to the page People and Groups : YourGroup (http://YourSharePoint/_layouts/15/people.aspx?MembershipGroupId=4600, assuming 4600 is the ID of your group). where they can list and change the group membership.

On this page they have the following options:

New menu:
Add Users

Actions menu:
E-Mail Users
Call/Message Selected Users
Remove Users from Group

Settings menu:
View Group Permissions

The group members can not edit group properties. The “Group Settings” is not displayed in the Settings menu, and even if they try to access the page by its URL (like http://YourSharePoint//_layouts/15/editgrp.aspx?Group=YourGroup), they get an error message:

Only owners of the group "YourGroup" can change its settings

(Actually, the permissions are checked in the InitPage method of the Microsoft.SharePoint.ApplicationPages.EditGroup class, by invoking the CanCurrentUserManageGroup method of the Microsoft.SharePoint.SPGroup class.)

Solution 1

As a group owner, you have the same options as in the case of Solution 1, and additionally you have the “Group Settings” in the Settings menu to access the People and Groups : Change Group Settings page and edit group properties. (Of course, if you remove yourself or your group from the Group Owner field, and you have no other extra permission on the site, you can not undo this action. It’s the same as above in Solution 2 with group membership.). If the group owner has no write permission of the root site, an Access Denied is thrown when setting group properties via the page, see the original issue in this post.

Finally, when a user browses the groups in a site, he can found that the “View Group Permissions” is displayed in the  Settings menu for one group, but it is not available for the other group. This option (as you can expect) is again permission dependent. The permissions are checked in the OnLoad method of the Microsoft.SharePoint.ApplicationPages.PeoplePage class (the code behind of the people.aspx page). The “View Group Permissions” option is visible, if the DoesCurrentUserHavePermission method of the Microsoft.SharePoint.WebControls.GroupPermissions class returns true:

internal static bool DoesCurrentUserHavePermission(SPWeb web, SPGroup group)
{
    if (!group.ContainsCurrentUser && !group.CanCurrentUserEditMembership)
    {
        return false;
    }
    return true;
}

(Note that the method has two parameters. The first one is of type SPWeb but that is not used at all in the method.)

That means, the “View Group Permissions” option is displayed only if the user is member of the group or can edit the group membership. On the other side, if the user can access the people.aspx page for the group, and see the group membership only because the group is configured to allow “Everyone” as “Who can view the membership of the group“, this option is not available. With other words, a user that is not in the group itself, not the owner of the group, and have no admin rights are not allowed to see the group permissions.

February 15, 2016

How to Set a SharePoint Document Library to Read-Only Mode

Filed under: PowerShell, Security, SP 2013 — Tags: , , — Peter Holpar @ 23:49

Recently we had to archive a few document libraries on our SharePoint farm. The document libraries may have they own permission setting on the library level, and / or at the folder, and / or the document levels either. We do not have any custom permission level defined in our sites.

We defined “archive” as this: all users that have read / write permission to an item, should keep the access on the item, but it has to be restricted to read permission in the future. The users having permission to the documents should be able to download them, and work on them locally (if they wish) but are not allowed to save them back to the library.

I wrote a PowerShell script that processes the permissions, and replaces write permissions with read permissions on demand.

Note: the solution implemented in this post is a one-way street. It’s not a read-only switch you can turn on or off as you can do for example in the case of a site collection (like Set-SPSite http://YourSharePointSite -LockState ReadOnly). You won’t be able to reproduce the original permissions once you run the script.

  1. $url = "http://YourSharePointSite/SubSite&quot;
  2. $docLibName = "Documents"
  3.  
  4. $web = Get-SPWeb $url
  5. $docLib = $web.Lists[$docLibName]
  6.  
  7. $limitedAccess = $web.RoleDefinitions.GetByType([Microsoft.SharePoint.SPRoleType]::Guest)
  8. $readAccess = $web.RoleDefinitions.GetByType([Microsoft.SharePoint.SPRoleType]::Reader)
  9.  
  10. $allowedAccess = $limitedAccess.Name, $readAccess.Name
  11.  
  12. function Replace-Permissions($securable)  
  13. {
  14.   $securable.RoleAssignments | ? { $_.RoleDefinitionBindings | ? { $allowedAccess -notcontains $_.Name }} | % {
  15.     $_.RoleDefinitionBindings.RemoveAll()
  16.     $_.RoleDefinitionBindings.Add($readAccess)
  17.     $_.Update()
  18.   }
  19. }
  20.  
  21. # if the doc. lib. inherits the permissions and if there is any role assignments that contains an access level beyond read only
  22. # we should break the inheritance first, to be able to change the permissions on the library level
  23. If (($doclib.HasUniqueRoleAssignments) -and (($docLib.RoleAssignments | ? { $_.RoleDefinitionBindings | ? { $allowedAccess -notcontains $_.Name }}).Count -gt 0))
  24. {
  25.   $doclib.BreakRoleInheritance($true);
  26. }
  27.  
  28. # set permissions on the doc. lib level
  29. Replace-Permissions($docLib)
  30. # set permissions on all folders having its own role assignment
  31. $doclib.Folders | ? { $_.HasUniqueRoleAssignments } | % { Replace-Permissions $_ }
  32. # set permissions on all documents having its own role assignment
  33. $doclib.Items | ? { $_.HasUniqueRoleAssignments } | % { Replace-Permissions $_ }

In the Replace-Permissions function I replace any permissions  on a securable object other than Read or Limited Access (Guest) permissions with Read permissions.

Note: If you remove a Limited Access permission using the web UI, or the corresponding role assignment from code, you will loose the permissions set explicitly for that user anywhere in the hierarchy below that level. As described here, you can call the RemoveAll method on the RoleDefinitionBindings without such side effects. Then we can add the read permissions in place of the removed permissions.

I invoke the Replace-Permissions function once for the document library, then once for each folder and document having its own unique role assignments.

November 24, 2015

Recovering Passwords for SharePoint Managed Accounts

Filed under: PowerShell, Reflection, Security, SP 2013 — Tags: , , , — Peter Holpar @ 23:47

We have a SharePoint 2013 farm on a Windows 2008 R2 server. Recently we found this error in the Windows event logs in relation with the web application pool account:

Event ID 1511 – Windows cannot find the local profile and is logging you on with a temporary profile. Changes you make to this profile will be lost when you log off.

We tried to solve the issue based on the information we found in this post, but at the step below we faced the problem, that the password stored for the web application pool account (in this case we assumed domain\wa_pool_account) in our password repository does not work any more.

runas /u:domain\wa_pool_account /profile cmd

The web application pool account is registered as a managed account in SharePoint, at the original password has been already automatically changed by the system.

We could reset the password for the managed account as described in this article, but before changing the password I wanted to be sure there is no way to recover the current password from the system. I found a blog post and the related PowerShell code in TechNet Gallery, but I found the method described there (creating a new web application, and using an external tool, appcmd.exe) a bit overkill.

Instead of this I came up with an alternative solution that query the password directly from the SPManagedAccount object, via its private m_Password field (of type SPEncryptedString) that we can access by using Reflection. The public SecureStringValue property of the SPEncryptedString class returns an instance of the System.Security.SecureString class, and as illustrated here, we can “decode” its value to a simple string via Marshaling.

Using this approach, recovering the managed account password is so simple:

$ma = Get-SPManagedAccount domain\wa_pool_account
$maType = $ma.GetType()

$bindingFlags = [Reflection.BindingFlags]::NonPublic -bor [Reflection.BindingFlags]::Instance

$m_Password = $maType.GetField("m_Password", $bindingFlags)
$pwdEnc = $m_Password.GetValue($ma)

$ssv = $pwdEnc.SecureStringValue
$ptr = [System.Runtime.InteropServices.Marshal]::SecureStringToGlobalAllocUnicode($ssv)
[System.Runtime.InteropServices.Marshal]::PtrToStringUni($ptr)

July 9, 2015

Creating custom SharePoint permission levels via PowerShell

Filed under: Permissions, PowerShell, Security, SP 2010 — Tags: , , , — Peter Holpar @ 15:36

Today I had to create some custom permissions via code, however the best post I found in this theme included only C# code, and I was to use PowerShell. Although it is not very difficult to translate the code, it is not trivial as well. So I thought I share my “translation”, and hope somebody find it useful.

Copy a permission level:
$contributorRole = $web.RoleDefinitions.GetByType([Microsoft.SharePoint.SPRoleType]::Contributor)
$customRole = New-Object Microsoft.SharePoint.SPRoleDefinition($contributorRole)

Add a permission:
$customRole.BasePermissions = $customRole.BasePermissions -bor [Microsoft.SharePoint.SPBasePermissions]::CreateGroups

Add multiple permissions:
$customRole.BasePermissions = $customRole.BasePermissions -bor ([Microsoft.SharePoint.SPBasePermissions]::ApplyStyleSheets -bor [Microsoft.SharePoint.SPBasePermissions]::ApproveItems)

Add all permissions:
$customRole.BasePermissions = $customRole.BasePermissions -bor [Microsoft.SharePoint.SPBasePermissions]::FullMask

Remove a permission:
$customRole.BasePermissions = $customRole.BasePermissions -band -bnot [Microsoft.SharePoint.SPBasePermissions]::DeleteListItems

Remove multiple permissions:
$customRole.BasePermissions = $customRole.BasePermissions -band -bnot ([Microsoft.SharePoint.SPBasePermissions]::DeleteVersions -bor [Microsoft.SharePoint.SPBasePermissions]::EditListItems)

Remove all permissions:
$customRole.BasePermissions = $customRole.BasePermissions -band [Microsoft.SharePoint.SPBasePermissions]::EmptyMask

Test for a permission:
$permissionTest = ($customRole.BasePermissions -band [Microsoft.SharePoint.SPBasePermissions]::DeleteListItems) -eq [Microsoft.SharePoint.SPBasePermissions]::DeleteListItems

Save a permission level:
$customRole.Name = "Your custom permission"
$customRole.Description = "Description of your custom permission level"
$web.RoleDefinitions.Add($customRole)
$web.Update()

April 6, 2015

Make the Status Bar Visible on Project Detail Pages for Users with Read-Only Permissions on PWA with Publishing Feature

Filed under: JavaScript, Project Server, Security — Tags: , , — Peter Holpar @ 22:03

Assume the following situation: You have a Project Web Access site (PWA) on your Project Server that has the SharePoint Server Publishing feature activated both on the site collection and on the site level. This detail might seem to be irrelevant now, but gets an importance pretty soon. The permissions on the PWA are customized and rather restricted, even project managers have only Read permissions on the root site level. The PMs complain often, that they can not open the project from the Project Detail Pages (PDP) for editing, the corresponding buttons are inactive on the ribbon.

image

The administrators check the project and see, that it is checked out to someone else.

Problem: the administrators see the status information on the PDPs, however other user do not. For example, if an administrator opens the project for editing, the following screen is displayed:

image

The users with standard rights see however only this, without the status bar:

image

I found, that the content of the status bar is contained in a DIV with id="pageStatusBar" on the web page. There are several JavaScript methods, that manipulate this content, like addStatus, appendStatus, removeAllStatus, removeStatus, setStatusPriColor (all of these in init.debug.js). After setting breakpoints in these methods, and reloading the site, I found, that the status bar is displayed temporally in each cases, but it is hidden then for standard users, via a JavaScript method that is included in the PDP itself (like Schedule.aspx):

document.onreadystatechange=fnRemoveAllStatus; function fnRemoveAllStatus(){removeAllStatus(true)};

For the administrators, this script was not included in the page, and so the status bar remained visible.

After a search using Reflector, I found the very same script block being inserted to the page by the HideStatusBar method of the PublishingRibbon class (namespace: Microsoft.SharePoint.Publishing.Internal.WebControls, assembly: Microsoft.SharePoint.Publishing, just as any other classes below). This method was called by the HideRibbonAndStatusBarAndSiteActionsMenu method, which is called by the OnLoad method of the same PublishingRibbon class, if the CanShowSiteActionsMenuItems method of the ConsoleVisibleUtilities class returns false:

public static bool CanShowSiteActionsMenuItems
{
  get
  {
    SPBasePermissions permissions = SPBasePermissions.BrowseUserInfo | SPBasePermissions.CreateSSCSite | SPBasePermissions.CreateAlerts | SPBasePermissions.UseRemoteAPIs | SPBasePermissions.UseClientIntegration |  SPBasePermissions.ViewVersions | SPBasePermissions.OpenItems | SPBasePermissions.ViewListItems | SPBasePermissions.ViewPages | SPBasePermissions.Open | SPBasePermissions.ViewFormPages;
    CombinedBasePermissions permissions2 = new CombinedBasePermissions();
    If ((((permissions | permissions2.ListItemPermissions) == permissions) && ((permissions | permissions2.ListPermissions) == permissions)) && ((permissions | permissions2.RootSitePermissions) == permissions))
    {
      return ((permissions | permissions2.SitePermissions) != permissions);
    }
    return true;
  }
}

The CombinedBasePermissions is a helper class that aggregates the site and list permissions of the current user from the current SharePoint context.

The value of the permissions variable is a combination of several list- and site-level base permissions. If you compare it with the standard Read permission level (see the next two screenshots below), you can see, that it is exactly the same combination of permissions:

List permissions for the Read permission level

image

Site permissions for the Read permission level

image

We compare the site and list permission returned by CombinedBasePermissions with these predefined set of base permissions. In the case of the PDPs we have no list context, that means, only the site permissions will be compared. If the current user has no permission beyond the ones included in the standard Read permission level, the remove script will be injected into the page, and the status bar won’t be displayed for the user.

The following screenshot illustrates the site permissions for the Team members (Microsoft Project Web App) permission level:

image

It includes the Browse Directories and the Edit Personal User Information base permissions. If the user had the Team members (Microsoft Project Web App) permission level either with or without the Read permission level, they would like to see the status bar. However, granting extra permissions to the users might be not desired on one hand, on the other hand, its effect would not be limited to the PDPs, the relevant status bar would be displayed on all other pages as well.

You can consider, I you really need the SharePoint Server Publishing feature. If not, deactivating it on the site level might solve the issue as well.

If you are OK with a quick and dirty solution, include the following script in you PDPs, for example, via a Script Editor Web Part:

<script type="text/ecmascript">
  function removeAllStatus(b) {
  }
</script>

The script overrides the default implementation of the removeAllStatus method in the scope of the page it is included into, and makes it impossible to hide the status bar.

As the result of the page customization, that status bar left displayed for standard users as well:

image

February 7, 2015

Changing Site Collection Administrators via PowerShell without Elevated Permissions

Filed under: PowerShell, Security, SP 2010 — Tags: , , — Peter Holpar @ 22:20

In my recent post I’ve already illustrated how to read and set the primary and secondary site collection administrators (the Owner and SecondaryContact properties of the corresponding SPSite object) via PowerShell. In that samples I’ve used elevated privileges to achieve my goals.

Let’s see if it is there a way without elevating the privileges.

Before PowerShell, before SharePoint 2010, the standard way to display / change the site owner and the secondary owner in command line was the stsadm command, that is still available to us.

For example, we can display this information for all site of a web application using the enumsites operation:

stsadm -o enumsites -url http://mysiteroot

To set the site owner, we can use the siteowner operation with the ownerlogin parameter:

stsadm -o siteowner -url http://mysiteroot/users/user1 -ownerlogin "domain\user1"

For the secondary admin, use the secondarylogin parameter instead of ownerlogin.

Note: You should have local administrator rights on the server where you run this commands, otherwise you receive an “Access denied.” error message. The reason, that for most of the stsadm operation, the following security check is performed in the entry method (public static int Main) of the Microsoft.SharePoint.StsAdmin.SPStsAdmin class:

if (!SPAdministrationServiceUtilities.IsCurrentUserMachineAdmin())
{
  Console.WriteLine(SPResource.GetString("AccessDenied", new object[0]));
  Console.WriteLine();
  return -2147024891;
}

I decided to check, what kind of method stsadm uses to read and change the values. The implementation of the siteowner operation can be found in the Microsoft.SharePoint.StsAdmin.SPSiteOwner class. To access the the Owner and SecondaryContact properties of the SPSite object, the OwnerLoginName and SecondaryContactLoginName properties of the SPSiteAdministration class are used. In the constructor of this class there is a security check that verify if the calling user is a farm administrator:

internal SPSiteAdministration(SPSite site)
{
    if (site == null)
    {
        throw new ArgumentNullException("site");
    }
    this.m_Site = site;
    if (this.m_Site.WebApplication.Farm.CurrentUserIsAdministrator())
    {
        this.m_Site.AdministratorOperationMode = true;
    }

To display the owner and the secondary contact of the site collection, we can use the following PowerShell script:

$url = "http://mysiteroot/users/user1&quot;
$siteAdmin = New-Object Microsoft.SharePoint.Administration.SPSiteAdministration($url)
$siteAdmin.OwnerLoginName
$siteAdmin.SecondaryContactLoginName

Changing these values is so simple as:

$url = "http://mysiteroot/users/user1&quot;
$siteAdmin = New-Object Microsoft.SharePoint.Administration.SPSiteAdministration($url)
$siteAdmin.OwnerLoginName = "domain\user1"
$siteAdmin.SecondaryContactLoginName = "domain\user2"

Note, that we are using the login name as string, and not an SPUser when assigning the values, and there is no need for elevated privileges. The caller must be a farm administrator for the current SharePoint farm, however, as we call this code directly, and not from stsadm, where this is checked, the user should not be a local admin.

I found it a bit inconsistent and disturbing, that we can access (read / set) the same properties via various objects and methods of the base SharePoint library, that perform various permission checks, and so one can avoid the security checks implemented in one of the objects when accessing the very same information via another class.

Changing Site Collection Administrators via PowerShell Using Elevated Permissions

Filed under: PowerShell, Security, SP 2010 — Tags: , , — Peter Holpar @ 05:06

Recently we migrated the users of a SharePoint farm into another domain. As part of the migration the MySites of the users should have been migrated as well. For the user migration we used the Move-SPUser Cmdlet, however it seems to have no effect on the site primary and secondary administrators (that means the Owner and SecondaryContact properties of the corresponding SPSite object). As you might know, each MySite is a site collection in SharePoint, the user the MySite belongs to is by default the primary site collection administrator and there is no secondary admin specified. It is possible to change (“migrate”) the admins via the Central Administration web UI, however having hundreds of users, it was not a viable option to us. But no problem, we can surely change these values via PowerShell as well, couldn’t we? Let’s test it first, and use PowerShell to read the values. We have all of the MySites in a separate web application, so we tried to iterate through all its site collections and dump out the information we need.

These samples assume that the URL of the web application (the MySite root) is http://mysiteroot, and the MySites are under the managed path /users, for example, the URL of the MySite of user1 is http://mysiteroot/users/user1.

Our first try was:

$waUrl = "http://mysiteroot&quot;

$wa = Get-SPWebApplication $waUrl
Get-SPSite -WebApplication $wa -Limit ALL | % {
  Write-Host "Url:" $_.Url
  Write-Host "Primary Administrator:" $_.Owner.LoginName
  Write-Host "Secondary Administrator:" $_.SecondaryContact.LoginName
  Write-Host "—————————–"
}

Surprise! The URLs are dumped out, however the Owner only for the root site and the own MySite of the user who executed the script, the SecondaryContact only for the root (in the MySite of the executing user had no SecondaryContact defined). That means one could access these properties only for the site where one is defined as site collection administrator (either primary or secondary). There is no error message (like Access denied) for the other sites, simply no information about the admins displayed.

However, if we run the code in an elevated privileges block, the Owner and SecondaryContact properties are dumped out as well:

$waUrl = "http://mysiteroot&quot;

[Microsoft.SharePoint.SPSecurity]::RunWithElevatedPrivileges (
  {
    $wa = Get-SPWebApplication $waUrl
    Get-SPSite -WebApplication $wa -Limit ALL | % {
      Write-Host "Url:" $_.Url
      Write-Host "Primary Administrator:" $_.Owner.LoginName
      Write-Host "Secondary Administrator:" $_.SecondaryContact.LoginName
      Write-Host "—————————–"
    }
  }
)

Note 1: If you would like to dump out the info without Write-Host in a RunWithElevatedPrivileges code block, you would not see the result in the console, even the Url properties would disappear.

The following sample display no result:

$waUrl = "http://mysiteroot&quot;

[Microsoft.SharePoint.SPSecurity]::RunWithElevatedPrivileges (
  {
    $wa = Get-SPWebApplication $waUrl
    Get-SPSite -WebApplication $wa -Limit ALL | % {
      $_.Url
      $_.Owner.LoginName
      $_.SecondaryContact.LoginName
    }
  }
)

Note 2: You don’t need to place all of your code in the elevated block. It is enough to place the code that gets the reference to the web application (site, web, etc.) in this block.

The following code works just as well as the original one with elevated privileges:

$waUrl = "http://mysiteroot&quot;

[Microsoft.SharePoint.SPSecurity]::RunWithElevatedPrivileges (
  {
    $wa = Get-SPWebApplication $waUrl
  }
)

Get-SPSite -WebApplication $wa -Limit ALL | % {
  Write-Host "Url:" $_.Url
  Write-Host "Primary Administrator:" $_.Owner.LoginName
  Write-Host "Secondary Administrator:" $_.SecondaryContact.LoginName
  Write-Host "—————————–"
}

After we displayed the information about the site admins, let’s see, how can we change the configuration. First I tried without elevation, although I was already sure, that it won’t perform the requested operation. In the following samples I try to set the Owner property, but in the case of SecondaryContact it would have the same outcome.

$url = "http://mysiteroot/users/user1&quot;
$siteOwnerLogin = "domain\user1" 
$web = Get-SPWeb $url
$user = $web.EnsureUser($siteOwnerLogin)
$site = $web.Site
$site.Owner = $user

The code above does not work. We receive an error message (Attempted to perform an unauthorized operation.), and the site owner is not changed.

However, using the elevated code block will solve the problem again:

$url = "http://mysiteroot/users/user1&quot;
$siteOwnerLogin = "domain\user1"

[Microsoft.SharePoint.SPSecurity]::RunWithElevatedPrivileges(
     {
         $web = Get-SPWeb $url
         $user = $web.EnsureUser($siteOwnerLogin)
         $site = $web.Site
         $site.Owner = $user
     }
)

You can find opinions on the web, like this one, that states, that running PowerShell code in elevated block has no effect at all. The samples above demonstrate however, that it really DOES have effect.

But what effect of code elevation is it, that makes it possible to get and set the Owner and SecondaryContact properties in the former samples?

If you have a look at the source code of the Owner (or SecondaryContact) property, for example, using Reflector, you will see, that there is a double permission check, both of them can throw an UnauthorizedAccessException. We can ignore the first one (that is !this.AdministratorOperationMode), while this condition is only checked if there is no CurrentUser for the RootWeb of the site. The second permission check is the important one, that is checked if the CurrentUser is not null, and it is:

(!this.RootWeb.CurrentUser.IsSiteAdmin)

If this condition is true, then an UnauthorizedAccessException is thrown. The setter method of the Owner (or SecondaryContact) property calls first the getter, so the same condition is checked in this case as well. Although the call to the getter is in a try-catch block, the catch is valid only for the type SPException, so it has no effect on the UnauthorizedAccessException thrown by the getter.

Remark: I have to admit, that it is not yet clear to me, why the exception thrown by the getter is not displayed when calling the getter from PowerShell.

Let’s see a further example of code elevation from PowerShell to clear the reason, why the former samples with elevation solved the issue of getting / setting the Owner and SecondaryContact properties:

$url = "http://mysiteroot/users/user1&quot;
$web = Get-SPWeb $url

$userName = $web.CurrentUser.LoginName
$userIsAdmin = $web.CurrentUser.IsSiteAdmin

# elevated privilages
[Microsoft.SharePoint.SPSecurity]::RunWithElevatedPrivileges(
     {
         $web = Get-SPWeb $url
         $userNameElevated = $web.CurrentUser
         $userIsAdminElevated = $web.CurrentUser.IsSiteAdmin
     }
)

Write-Host "Before elevation"
Write-Host "User name: " $userName
Write-Host "User is site admin: " $userIsAdmin
Write-Host "After elevation"
Write-Host "User name: " $userNameElevated
Write-Host "User is site admin: " $userIsAdminElevated

Assuming the executing user has neither owner nor secondary contact for the site collection, the code returns the same user name before and after elevation, however the value of the IsSiteAdmin property is false before the elevation, but true after the elevation. As we have seen from the previous reflectoring, this change is just enough for the getter / setter  methods of the Owner and SecondaryContact properties to work.

In my next post I show you an alternative method that make it possible to read / change these values from PowerShell without any kind of elevation.

May 30, 2014

Older Posts »

Blog at WordPress.com.