Home > Sandboxed Solutions, SharePoint > Update SPPersistedObject in SPProxyOperation

Update SPPersistedObject in SPProxyOperation

Das Konzept der Sandboxed Solutions im SharePoint 2010, bei dem user code in einem sehr restriktivem Container ausgeführt wird, kommt immer dann an seine Grenzen, wenn bestimmte Funktionen benötigt werden, die diesen Sicherheitsbereich überschreiten. An dieser Stelle können full trust proxies eingesetzt werden, die für alle Sandboxed Solutions der gesamten SharePoint Farm eben diese Funktionalitäten bereitstellen.
Bei meiner Arbeit mit Sandboxed Solutions und full trusted proxies hatte ich die Anforderung bestimmte Konfigurationsinformationen auf Farm-Ebene zu persistieren. Als Speichermechanismus sollte der Hierarchical Object Store herhalten, ein in SharePoint 2007 eingeführter, eventuell nicht immer bewußt verwendeter Speicherort für globale Konfigurationswerte.    

Ausgangssituation

Zum Testen der Grundfunktionalität habe ich ein Testprojekt erzeugt, das sich in Auszügen aus folgenden Bestandteilen zusammensetzt.    

  1. Eine einfache Proxy Operation, die von einem sandboxed WebPart aufgerufen wird.
  2.   public class SetValueOperation : SPProxyOperation
        {
      public override object Execute(SPProxyOperationArgs args)
            {
                ...
            }
        }
    

  3. Die Klasse TestPersistedObject, die von SPPersistedObject ableitet und lediglich eine Eigenschaft persistiert.
  4.   public class TestPersistedObject : SPPersistedObject
        {
      [Persisted]
      private string _strTestValue;
    
      public string TestValue
            {
      get { return _strTestValue; }
      set { _strTestValue = value; }
            }
    
      public TestPersistedObject() {}
      public TestPersistedObject(string pName, SPPersistedObject pParent, Guid pID)
      : base(pName, pParent, pID) {}
        }
    

  5. Das Erzeugen beziehungsweise Ändern einer Instanz der Klasse TestPersistedObject und die Speicherung im Hierachical Object Store innerhalb der Execute Methode der Proxy-Operation.
  6.   public class SetValueOperation : SPProxyOperation
        {
      public override object Execute(SPProxyOperationArgs args)
            {
      try
                {
      SetValueArgs setValueArgs = (SetValueArgs)args;
      SPFarm farm = SPFarm.Local;
    
      TestPersistedObject configObject = farm.GetChild(setValueArgs.SiteID);
      if (!(configObject != null))
                    {
      configObject = new TestPersistedObject(setValueArgs.SiteID, farm, Guid.NewGuid());
                    }
    
      configObject.TestValue = setValueArgs.Value;
                    configObject.Update();
    
      return "Item Updated";
                }
      catch (Exception ex)
                {
                    return ex.Message;
                }
            }
        }
    

  7. Die Proxy-Operation habe ich dem User Code Service bekannt gegeben und den WebPart auf einer Site Collection veröffentlicht.

 

Fehlerbeschreibung

Beim Ausführen der ProxyOperation erhielt ich die Fehlermeldung „Access denied“ und aus dem ULS Log konnte ich diese Zeilen fischen.    

Updating SPPersistedObject TestPersistedObject-452e-4eee-9615-f17d51f2c41f. Version: -1 Ensure: False, HashCode: 38226137, Id: 8ee88993-de35-4455-9509-012db9ccb7c1, Stack: at Microsoft.SharePoint.Administration.SPPersistedObject.BaseUpdate() at PersistFarmConfigValue.SetValueOperation.Execute(SPProxyOperationArgs args) at Microsoft.SharePoint.Utilities.SPUtility_SubsetProxy.ExecuteProxyOperation_Core(String assemblyName, String typeName, SPProxyOperationArgs args) at Microsoft.SharePoint.Utilities.SPUtility_SubsetProxy.ExecuteProxyOperation_Inner(String assemblyName, String typeName, SPProxyOperationArgs args) at System.Runtime.Remoting.Messaging.StackBuilderSink._PrivateProcessMessage(IntPtr md, Object[] args, Object server, Int32 methodPtr, Boolean fExecuteInContext, Object[]& outArgs) at System.Runtime.Remoting.Messaging.StackBuilderSink.SyncProcessMessage(IMessage msg, Int32 methodPtr, Boolean fExecuteInContext) at System.Runtime.Remoting.Messaging.ServerObjectTerminatorSink.SyncProcessMessage(IMessage reqMsg) at System.Runtime.Remoting.Lifetime.LeaseSink.SyncProcessMessage(IMessage msg) at System.Runtime.Remoting.Messaging.ServerContextTerminatorSink.SyncProcessMessage(IMessage reqMsg) at System.Runtime.Remoting.Channels.CrossContextChannel.SyncProcessMessageCallback(Object[] args) at System.Runtime.Remoting.Channels.ChannelServices.DispatchMessage(IServerChannelSinkStack sinkStack, IMessage msg, IMessage& replyMsg) at System.Runtime.Remoting.Channels.BinaryServerFormatterSink.ProcessMessage(IServerChannelSinkStack sinkStack, IMessage requestMsg, ITransportHeaders requestHeaders, Stream requestStream, IMessage& responseMsg, ITransportHeaders& responseHeaders, Stream& responseStream) at System.Runtime.Remoting.Channels.Ipc.IpcServerTransportSink.ServiceRequest(Object state) at System.Runtime.Remoting.Channels.SocketHandler.ProcessRequestNow() at System.Runtime.Remoting.Channels.SocketHandler.BeginReadMessageCallback(IAsyncResult ar) at System.Runtime.Remoting.Channels.Ipc.IpcPort.AsyncFSCallback(UInt32 errorCode, UInt32 numBytes, NativeOverlapped* pOverlapped) at System.Threading._IOCompletionCallback.PerformIOCompletionCallback(UInt32 errorCode, UInt32 numBytes, NativeOverlapped* pOVERLAP) The SPPersistedObject, TestPersistedObject-452e-4eee-9615-f17d51f2c41f, could not be updated because the current user is not a Farm Administrator.

 

Fehlersuche

Auf den ersten Blick erschien mir das Problem sehr simpel. Das Speichern von Objekten im Hierachical Object Store setzt bestimmte Berechtigungen auf die Sharepoint-Konfigurationsdatenbank (SharePoint_Config) voraus. Ergo, sollte der Account mit dem der SPUCWorkerProcessProxy ausgeführt wird, in der Gruppe der Farmadministratoren sein und die benötigte Berechtigung dadurch auf der Datenbank erlangen. So kann man es ja auch aus der Fehlermeldung herauslesen. In meiner Umgebung läuft sowohl die Zentraladministration als auch der User Code Service mit dem gleichen Service-Account. Folglich sollte das Berechtigungsproblem nicht vorhanden sein.  

Ok, dann halt mal mit dem SQL Profiler nachschauen, ob überhaupt etwas in Richtung Konfigurationsdatenbank abgefeuert wird. Siehe da, die Leitung ist, zumindest was das Speichern meines Objekts angeht, tot.  

Das mein Code beim Aufrufen der Methode configObject.Update() den Fehler verursacht, findet man recht schnell beim Debuggen heraus. Das sollte aber eigentlich nicht sein, da die Anwendung augenscheinlich mit den richtigen Rechten ausgestattet ist. Es bleibt nicht aus, ich muß der Sache mit dem Reflektor auf den Grund gehen.Der Einstiegspunkt ist die Methode Update der Klasse SPPersistedObject. Diese ruft lediglich die Methode BaseUpdate() auf. Moment, da war doch was im StackTrace der Exception:    

at Microsoft.SharePoint.Administration.SPPersistedObject.BaseUpdate()

Jap und da ist auch schon die Fehlermeldung, wunderbar hard coded in ihrer vollen Pracht.  

internal void BaseUpdate()
{
    ...
  if ((!this.m_SkipPersistedStoreWriteCheck
  !this.PersistedStoreProvider.DoesCurrentUserHaveWritePermission(this))
  !this.HasAdditionalUpdateAccess())
    {
  ULS.SendTraceTag(0x38647975,
  ULSCat.msoulscat_WSS_Topology, ULSTraceLevel.High,
  "The SPPersistedObject, {0}, could not be updated because the current user is not a Farm Administrator.",
  new object[] { this.ToString() });
  this.PersistedStoreProvider.InvalidateCache(this);
  throw new SecurityException(SPResource.GetString("AccessDenied", new object[0]));
    }
    ...
}

So, dann rennt der Code also in diesen If-Block und bricht die weitere Ausführung der Methode ab. Mein Gefühl sagt mir, das dies etwas mit dem Ergebnis der Methode this.PersistedStoreProvider.DoesCurrentUserHaveWritePermission(this) zu tun haben könnte. Dann weiter, wer ist in diesem Fall der PersistedStoreProvider und wie hat dieser die Methode DoesCurrentUserHaveWritePermission() implementiert?  

bool ISPPersistedStoreProvider.DoesCurrentUserHaveWritePermission(SPPersistedObject persistedObject)
{
  if (!SPSecurity.AdministrationAllowedInCurrentProcess)
    {
  return false;
    }
  SPFarm farm = (persistedObject is SPFarm) ? (persistedObject as SPFarm) : this.Farm;
  if (null != farm)
    {
  return farm.CurrentUserIsAdministrator();
    }
  return SPFarm.CurrentUserIsBoxAdministrator();
}

Die konkrete Implementierung des Interfaces ISPPersistedStoreProvider in diesem Fall ist die Klasse SPConfigurationDatabase. Sprechende Namen für Klassen sind doch immer wieder eine feine Sache. Die Methode läßt wieder drei Pfade zu, auf denen man zu einer Erklärung gelangen kann. Ich kürze die Sache mal ab, es liegt an der Implementierung von SPSecurity.AdministrationAllowedInCurrentProcess().

internal static bool AdministrationAllowedInCurrentProcess
{
get
{
if (!s_AdministrationAllowedInCurrentProcess.HasValue)
{
string processName = ProcessName;
if ((SPUtility.StsCompareStrings(processName, "SPUCHostService") || SPUtility.StsCompareStrings(processName, "SPUCWorkerProcess")) || SPUtility.StsCompareStrings(processName, "SPUCWorkerProcessProxy"))
{
s_AdministrationAllowedInCurrentProcess = false;
}
else
{
s_AdministrationAllowedInCurrentProcess = true;
}
}
return s_AdministrationAllowedInCurrentProcess.Value;
}
}

Die Ursache wäre damit geklärt, aus dem SPUCWorkerProcessProxy Prozess heraus sollten, so meine Lesart, keine Änderungen an der Konfigurationsdatenbank erfolgen. Erst mal verständlich, leider kann ich es nicht mit meiner Anforderung vereinbaren und sehe auch kein Sicherheitsproblem darin, falls ich einen Weg daran vorbei finde. Wir befinden uns ja auf der Seite der SPProxyOperation, also in einem Prozess der nicht direkt aus der Sandbox heraus manipuliert werden kann. Falls ich bei meinen Überlegungen etwas übersehen habe, freue ich mich auf eure Hinweise!    

Lösungsansatz

Bei der Internetrecherche zu dem Thema hab ich zu einem Zeitpunkt, zu dem ich nichts damit anfangen konnte einen Kommentar gelesen, der am Ende dann auch für mein Problem zur Lösung wurde. Leider habe ich diesen erst nicht so richtig wahr genommen.
Nochmal zurück zu dem kleinen Ausflug in den SharePoint 2010 Code. In der Methode BaseUpdate() wird zu einem frühen Zeitpunkt abgefragt, ob der aktuelle User über ausreichende Berechtigungen verfügt. Diese If-Abfrage beinhaltet noch eine Art Bypass, mit dem alle Berechtigungsabfragen ausgehebelt werden.  

if ((!this.m_SkipPersistedStoreWriteCheck && !this.PersistedStoreProvider.DoesCurrentUserHaveWritePermission(this)) && !this.HasAdditionalUpdateAccess())

Im Reflektor betrachtet ist die Methode HasAdditionalUpdateAccess() der Klasse SPPersistedObject erst einmal absolut unspektakulär. Interessant ist aber der Modifier der Methode, dieser schreit förmlich danach, das hier Hand angelegt werden darf!  

protected virtual bool HasAdditionalUpdateAccess()
{
    return false;
}

Jetzt fehlt nur noch ein Schritt bis zur Lösung. Die von SPPersistedObject abgeleitete Klasse TestPesistedObject muss um die Methode HasAdditionalUpdateAccess() erweitert werden, dann funktioniert das Hinzufügen beziehungsweise Aktualisieren von Objektinstanzen im Hierachical Object Store.  

protected override bool HasAdditionalUpdateAccess()
{
  return true;
}

Zum Ende noch eine kleine Einschränkung, dieser Bypass wird nur innerhalb der BaseUpdate() Methode verwendet. Die Methode DoesCurrentUserHaveWritePermission() wird aber zusätzlich noch von den Settern der Eigenschaften ID, Name und Status von SPPersistedObject verwendet, somit sollten diese nicht aus dem Prozess SPUCWorkerProcessProxy heraus aufzurufen sein.  

  1. Es gibt noch keine Kommentare.
  1. Keine Trackbacks bisher.

Kommentar verfassen

Trage deine Daten unten ein oder klicke ein Icon um dich einzuloggen:

WordPress.com-Logo

Du kommentierst mit Deinem WordPress.com-Konto. Log Out / Ändern )

Twitter-Bild

Du kommentierst mit Deinem Twitter-Konto. Log Out / Ändern )

Facebook-Foto

Du kommentierst mit Deinem Facebook-Konto. Log Out / Ändern )

Verbinde mit %s

Follow

Bekomme jeden neuen Artikel in deinen Posteingang.