Asignación CAS de Sharepoint por contexto de usuario

Puesta en situación


En uno de nuestros proyectos hemos tenido que manejar el contexto del usuario en el negocio con el contexto de sharepoint. Consiste en que el cliente dispone de un conjunto de usuarios que están en la central, y otro dispersos por el territorio nacional, asociados en centros o delegaciones.  Una de las funcionalidades que disponía era la de enviar solicitudes desde los centros a la central. Lo que queríamos era que desde una misma página cada usuario solo viera las solicitudes creadas por los usuarios de su centro, y los gestores los de todos los centros.


La solución que pensamos fué la siguiente:


Cada solicitud tendría un CustomField con el centro que crea la solicitud. Este campo se encargaría de determinar el centro al que está asignado el usuario y asignar a la solicitud los permisos de seguridad correspondientes para que lo vieran solo los gestores y los usuarios del centro que lo creo. Para gestionar la seguridad creamos un grupo de seguridad para cada centro, y asignamos los usuarios al centro que le correspondía. Para relacionar Grupo de seguridad con Centro, utilizamos una lista personalizada con los centros disponibles.


Al crear una nueva solicitud, el CustomField detectaba el centro al que pertenecía el usuario comparando la lista de centros con los grupos de seguridad, y a continuación modificaba los permisos CAS del nuevo elemento.


 


Feature


Creamos una Feature de Wss3 para encapsular los componentes y asignar el EventReceiver a las listas al activarse la Feature.


 


CustomField de Wss3


Utilizamos un customField de tipo texto para almacenar el centro que crea la solicitud. Para que el usuario no tuviera que seleccionar su centro, quitamos el campo de los formularios de edición.


 


public class CentroCustomField : SPFieldText
    {
        ……………..


        /// <summary>
        /// Devuelve toda la lista de centros
        /// </summary>
        /// <returns></returns>
        private List<string> LoadAllCentros(){
            List<string> allCentros = new List<string>();
            try{
                SPList centrosList = SPContext.Current.Web.GetList(string.Format(“{0}/Lists/Centros”, SPContext.Current.Web.Name));
                if(centrosList != null){
                    foreach(SPItem centro in centrosList.Items){
                        allCentros.Add((string)centro[“NombreCentro”]);
                    }
                }
            }catch(Exception ex){
            }
            return allCentros;
        }
      }


      ……………..


        protected override void CreateChildControls() {
            try{
              if (this.Field == null || this.ControlMode == SPControlMode.Display)
                return;
              base.CreateChildControls();


              this.CentroSelector = (DropDownList)TemplateContainer.FindControl(“CentroSelector”);

              if (this.CentroSelector == null)
                throw new Exception(“Control CentroFieldControl.ascx corrupto.”);

              if (!this.Page.IsPostBack)
              {
                  if (m_allCentros != null)
                  {
                      this.CentroSelector.Items.Add(” “);
                      this.CentroSelector.Items.Add(“Todos”);
                      foreach (string nombreCentro in m_allCentros)
                      {
                          this.CentroSelector.Items.Add(nombreCentro);
                      }
                  }
              }
            }catch(Exception ex){
            }
          }



……………..


    }


Event Handler


Para que nuestro campo supiera que centro debe asignar a la solicitud creamos un event receiver al agregar un elemento a nuestra lista de solicitudes.


Para registrar el event receiver utilizamos la activación de la feature:


 


public class FeatureReceiver : SPFeatureReceiver
   {

       public override void FeatureActivated(SPFeatureReceiverProperties properties)
       {
           try
           {
               SPWeb site = SPContext.Current.Web;

               string asmName = “FeatureCentros, Version=1.0.0.0, Culture=neutral, PublicKeyToken=ea8da11d5c8d53be”;
               string itemReceiverName = “XXXXXXX.CentroEventReceiver”;

               SPList listaSolicitudes = site.Lists[“Solicitudes”];
               listaSolicitudes.EventReceivers.Add(SPEventReceiverType.ItemAdded,
                                             asmName,
                                             itemReceiverName);

               listaSolicitudes.Update();

           }
           catch { }
       }

}


Para que la feature sepa que debe ejecutar el Event Receiver indicamos en la feature


<Feature Id=”FE642B14-97FF-4a72-85F2-984FBD0A7D14″
   Title=”Herramientas de Centros”
   Description=”XXXXXXXXXXXXX”
   Version=”1.0.0.0″
   Scope=”Web”
   Hidden=”FALSE”
   ImageUrl=”XXXXXXX.jpg”
   ReceiverAssembly=”FeatureCentros, Version=1.0.0.0, Culture=neutral, PublicKeyToken=ea8da11d5c8d53be”
   ReceiverClass=”XXXXXX.FeatureReceiver”
   xmlns=”http://schemas.microsoft.com/sharepoint/”>

  <ElementManifests>


………………………..
  </ElementManifests>


</Feature>

 


Event Receiver


Creamos un EventReceiver para que cuando se añada un elemento a la lista se apliquen los permisos correspondientes.


 


public class CentroEventReceiver : SPItemEventReceiver
    {

        public override void ItemAdded(SPItemEventProperties properties)
        {
            try
            {
                        if ((string)properties.ListItem[“Centro”] != “Todos”)
                        {
                            // Buscamos el centro asociado al usuario
                            SPUser usuario = properties.ListItem.ParentList.ParentWeb.CurrentUser;

                            using (SPWeb webOrigUser = properties.OpenWeb())
                            {
                                SPUserToken token = webOrigUser.AllUsers[“SHAREPOINT\system”].UserToken;
                                using (SPSite site = new SPSite(properties.SiteId, token))
                                {
                                    using (SPWeb currentWeb = site.OpenWeb(properties.RelativeWebUrl))
                                    {

                                        Dictionary<string, SPGroup> gruposAsociados = new Dictionary<string, SPGroup>();
                                        foreach (SPGroup grupo in usuario.Groups)
                                        {
                                            gruposAsociados.Add(grupo.Name, grupo);
                                        }

                                        List<SPGroup> centrosAsociados = new List<SPGroup>();
                                        SPList centrosList = currentWeb.GetList(string.Format(“{0}/Lists/Centros”, currentWeb.Name));
                                        if (centrosList != null)
                                        {
                                            foreach (SPItem centro in centrosList.Items)
                                            {
                                                string nombreCentroLeido = (string)centro[“NombreCentro”];
                                                if (gruposAsociados.ContainsKey(nombreCentroLeido))
                                                {
                                                    centrosAsociados.Add(gruposAsociados[nombreCentroLeido]);
                                                }
                                            }
                                        }

                                        if (centrosAsociados.Count > 0)
                                        {
                                            try
                                            {
                                               DisableEventFiring();

                                                // Asignamos el primer centro al campo en caso de no ir relleno
                                                if ((string)properties.ListItem[“Centro”] == ” “)
                                                {
                                                    properties.ListItem[“Centro”] = centrosAsociados[0].Name;
                                                }

                                                //// Asignamos los CAL que correspondan
                                                SPList listaAModificar = currentWeb.Lists[properties.ListId];
                                                SPListItem itemAModificar = listaAModificar.GetItemById(properties.ListItem.ID);
                                                if (!itemAModificar.HasUniqueRoleAssignments)
                                                    itemAModificar.BreakRoleInheritance(true);



                                                List<SPRoleAssignment> RoleABorrar = new List<SPRoleAssignment>();
                                                SPRoleAssignmentCollection roleAssignmentCollection = itemAModificar.RoleAssignments;
                                                foreach (SPRoleAssignment roleAssignment in roleAssignmentCollection)
                                                {
                                                    if (roleAssignment.Member.Name != “Gestores”                    &&
                                                        roleAssignment.Member.Name != “Consultores”                 &&
                                                        roleAssignment.Member.Name != “Propietarios eLogosPiloto”   &&
                                                        roleAssignment.Member.Name != “Cuenta del sistema”          &&
                                                        roleAssignment.Member.Name != centrosAsociados[0].Name      &&
                                                        roleAssignment.Member.Name != (string)properties.ListItem[“Centro”])
                                                    {
                                                        RoleABorrar.Add(roleAssignment);
                                                    }
                                                }
                                                foreach (SPRoleAssignment roleAssignment in RoleABorrar)
                                                    roleAssignmentCollection.Remove(roleAssignment.Member);


                                                itemAModificar.Update();

                                            }
                                            catch (Exception ex)
                                            {

                                            }
                                            finally
                                            {
                                                EnableEventFiring();
                                            }
                                        }
                                    }
                                }
                            }
                        }
               
            }
            catch (Exception ex)
            {
            }
        }

    }

Fijaros en algunos detalles:


SPUserToken token = webOrigUser.AllUsers[“SHAREPOINT\system”].UserToken;
using (SPSite site = new SPSite(properties.SiteId, token))
{
    using (SPWeb currentWeb = site.OpenWeb(properties.RelativeWebUrl))
    {


Este trozo de código hace que el código se ejecute con los permisos asociados al usuario “SHAREPOINTsystem” para que podamos realizar determinadas tareas de administración. Si no ejecutáramos nuestro código con privilegios al hacer un BreakRoleInheritance nos lanzaría una excepción del tipo  UnauthorizedAccessException.


El objetivo es que se ejecutara con permisos de administración, para ello podríamos haber utilizado un código similar al siguiente:


SPSecurity.RunWithElevatedPrivileges(delegate()

{

  SPUserToken token = webOrigUser.AllUsers[WindowsIdentity.GetCurrent().Name].UserToken;
  using (SPSite site = new SPSite(properties.SiteId, token))
  {
    using (SPWeb currentWeb = site.OpenWeb(properties.RelativeWebUrl))
     {


Esto funcionará siempre que tengamos activado en el fichero Web.config: <Identity Impersonate=”true”>


Fijaros en el código que estamos obteniendo una nueva referencia de SPSite, SPWeb y SPItem en lugar de utilizar las propiedades del parámetros del EventReceiver “SPItemEventProperties properties”. Esto es debido a que necesitamos obtener las referencias al elemento que queremos modificar con las credenciales del usuario administrador.


 


DisableEventFiring: Permite que cualquier cambio que hagamos no provoque la ejecución de un workflow.

GetItemById: Hemos utilizado esta función en lugar de la colección Items, porque Items solo contiene los elementos disponibles antes de ejecutarse el evento y GetItemById es capaz de localizar el nuevo elemento.

BreakRoleInheritance: Rompe la herencia del contendor (la lista), para poder editar nuestros propios permisos. El valor True indica que copie todos los grupos que hereda de la lista, un valor False ´lo dejaría sin grupos. Un detalle, cada vez que utilicemos esta función tendremos que hacer un Dispose del Parent del elemento, tal y como se comenta en el blog de Roger Lamb’s

RoleAssignments: Es la colección de grupos de sharepoint asignados automáticamente al elemento.

Opciones del QuickLaunch de Wss3 mediante SPNavitaionProvider

Un problema muy común en WSS3 es el de conseguir un menú de navegación que muestre las opciones en función del usuario y las unidades de negocio que estén activas en el portal.

El menú de navegación de wss3 nos permite navegar a las distintas listas o bibliotecas que creemos. También nos permite dar de alta nuestras propias opciones desde la página de configuración de inicio rápido.

El problema que tendremos en un proyecto real, será que las opciones que tengan que aparecer dependerán del usuario logado y los elementos de negocio implementados, y no podrán ser opciones estáticas.

 

Ejemplo del problema en el mundo real

En Renacimiento estamos trabajando en un cliente que tiene que desplegar más de 150 portales con una infraestructura Wss3 (no MOSS), cada portal puede tener activo una serie de features que en conjunto implementan las funcionalidades requeridas por el negocio. Al activarse cada elemento muestra en el menú principal una serie de opciones propias del elemento o unidad de negocio. Además, en función del rol del usuario puede visualizar o no esas opciones. Esto implica que el mantenimiento de las opciones por el menú de inicio rápido sea bastante limitado para el proyecto.

Para ello, hemos utilizado nuestro propio SPNavigationProvider que consume una lista personalizada de Wss3 como repositorio de las opciones del QuickLaunch Menu.

 

SPNavigationProvider

Es un proveedor de navegación que se ocupa de proporcionar las opciones que visualizará un menú de navegación. Este clase no se encarga de visualizar las opciones, sino de obtenerlas de un repositorio.

SPNavigationProvider es una clase que hereda de SiteMapProvider estándar en Asp.Net2.

System.Object
System.Configuration.Provider.ProviderBase
System.Web.SiteMapProvider
Microsoft.SharePoint.Navigation.SPNavigationProvider

 

How To

Para conseguir que nuestro QuickLaunch Menu obtenga las opciones de una lista personalizada tendremos que crear una clase que herede de SPNavitaionProvider e implementar el método GetChildNodes, después configurar el nuevo proveedor y modificar nuestra masterpage para que utilice el proveedor.

 

Custom SPNavitaionProvider

Crearemos una clase que herede de SPNavitaionProvider. Para hacerlo crearemos un proyecto Dll de windows.

Agregaremos al proyecto las referencias:

System.Configuration

System.Web

Microsoft.Sharepoint

 

Importaremos las referencias en nuestra clase:
using System.Collections.Generic;
using System.Web;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Navigation;

 

A continuación definimos nuestra clase, hacemos que herede de SPNavitaionProvider, y sobre-escribimos el método GetChildNodes.

using System;
using System.Collections.Generic;
using System.Data;
using System.Text;
using System.Web;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Navigation;

namespace MyMenuProvider
{
    public class MyMenuProvider : SPNavigationProvider
    {
        public override System.Web.SiteMapNodeCollection GetChildNodes(System.Web.SiteMapNode node)
        {
           ……

        }
    }
}

 

En nuestros caso de ejemplo hemos creado una lista de wss3 con un nivel de profundidad:

Lista

Opciones

 

namespace MyMenuProvider
{
    public class MyMenuProvider : SPNavigationProvider
    {
       

        public override System.Web.SiteMapNodeCollection GetChildNodes(System.Web.SiteMapNode node)
        {
            System.Web.SiteMapNodeCollection opciones = new System.Web.SiteMapNodeCollection();;
            try
            {
                if (node.Title == “Inicio rápido”)
                {
                    System.Web.SiteMapNode rootOpcion = new System.Web.SiteMapNode(this, “Mis Opciones”, “/”, “Mis Opciones”, “”);
                    opciones.Add(rootOpcion);
                }
                if (node.Title == “Mis Opciones”)
                {
                    SPWeb site = SPContext.Current.Web;
                    SPList listaOpciones = site.Lists[“Opciones”];

                    SPQuery query = new SPQuery();
                    query.ViewFields = “<FieldRef Name=’Title’ /><FieldRef Name=’Url’ /><FieldRef Name=’Orden Opcion’ />”;

                    SPListItemCollection opcionesLeidas = listaOpciones.GetItems(query);
                    foreach (SPListItem opcionLeida in opcionesLeidas)
                    {
                        System.Web.SiteMapNode nuevaOpcion = new System.Web.SiteMapNode(this, (string)opcionLeida[“Title”], (string)opcionLeida[“Url”], (string)opcionLeida[“Title”], (string)opcionLeida[“Title”]);
                        opciones.Add(nuevaOpcion);
                    }
                }
            }
            catch (Exception ex)
            {
                System.Web.SiteMapNode exOpcion = new System.Web.SiteMapNode(this, “Excepción”, “”, ex.Message, ex.Message);
                opciones.Add(exOpcion);
            }
            return opciones;
        }

    }
}

 

A continuación firmamos la dll con un strong name y construimos nuestra dll.

 

Configurar el Web Config

Tendremos que indicar en el Web.Config de nuestra Web Application la configuración del nuevo provider de navegación. Abriremos el web.config y lo editaremos.

Para que esté disponible el proveedor es imprescindible que la aplicación de wss3 sepa encontrar la dll, para ello podremos registrar la dll en el GAC (Lo más recomendable), o copiarlo en el directorio “bin” de la aplicación web. Si haceis modificaciones en la dll una vez registrada en el web.config tendreis que hacer un iisreset para que obtenga los cambios.

 

Abriremos entonces el WebConfig y editaremos la sección “siteMap”:

<siteMap defaultProvider=”SPSiteMapProvider” enabled=”true”>
      <providers>
        <add name=”SPNavigationProvider” type=”Microsoft.SharePoint.Navigation.SPNavigationProvider, Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c” />
        <add name=”SPSiteMapProvider” type=”Microsoft.SharePoint.Navigation.SPSiteMapProvider, Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c” />
        <add name=”SPContentMapProvider” type=”Microsoft.SharePoint.Navigation.SPContentMapProvider, Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c” />
        <add name=”SPXmlContentMapProvider” siteMapFile=”_app_bin/layouts.sitemap” type=”Microsoft.SharePoint.Navigation.SPXmlContentMapProvider, Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c” />
        <add name=”MyMenuProvider” type=”MyMenuProvider.MyMenuProvider, MenuProvider, Version=1.0.0.0, Culture=neutral, PublicKeyToken=160b265bda309c7a” />
      </providers>
    </siteMap>

 

Un error muy común es confundirse en el Namespace y en nombre del assembly, revisarlo bien antes de continuar.

 

Configurar el QuickLaunch Menu

Ya tendremos entonces disponible nuestro provider de navegación, ahora solo tendremos que indicar en nuestra masterpage al SPNavigationManager el nuevo Provider de navegación.

En nuestro ejemplo hemos creado una nueva masterpage y hemos hecho las modificaciones sobre ella.

Buscamos la sección “Sharepoint:SPNavigationManager” y modificamos la propiedad “SiteMapProvider” del siteMapDatasource por el nombre de nuestro provider.

<Sharepoint:SPNavigationManager
                id=”QuickLaunchNavigationManager”
                runat=”server”
                QuickLaunchControlId=”QuickLaunchMenu”
                ContainedControl=”QuickLaunch”
                EnableViewState=”false”>
                <div>
                    <SharePoint:DelegateControl runat=”server”
                        ControlId=”QuickLaunchDataSource”>
                     <Template_Controls>
                        <asp:SiteMapDataSource
                        SiteMapProvider=”MyMenuProvider”
                        ShowStartingNode=”False”
                        id=”QuickLaunchSiteMap”
                        StartingNodeUrl=”sid:1025″
                        runat=”server”
                        />
                    …………………………………
                </div>
                </Sharepoint:SPNavigationManager>

Una vez configurado en el masterpage, las páginas que la utilicen tendrán opciones de menú distintas a las del menú de inicio rápido.

Ahora solo tendremos que añadir en nuestra lista las opciones que necesitemos y configurar los permisos de cada una para cada grupo de usuarios.Con esto habremos conseguido que sea Wss3 quien gestione que opciones puede ver cada usuario.

 opcionesmenu

 

Es importante que reviséis el  código del provider, ya que se va a ejecutar varias veces por cada petición de una misma página. Por cada nodo que añadamos ejecutará el método Getchild, por lo que habrá que controlar la recursividad con el parámetro de la función y el método FindSiteMapNode.

 

Otras opciones

Existen más opciones para conseguir que nuestro menú de navegación muestre las opciones en función de las reglas de negocio y seguridad.

Algunos utilizan listas como repositorio de opciones, pero para visualizarlo utilizan un DataFormWebPart y modifican el xsl para dejarlo con el diseño del portal.

Otros prefieren utilizar listas como repositorio, pero en lugar del menú estándar crean su propio control de usuario y lo añaden a la masterpage.

También existe la copión de hacerse un menú de ASp.NEt a medida.

 

Existen muchas opciones, cada uno que elija la que mejor le venga. Pero como conclusión, el trabajar con proveedores como comentaba Jorge Diéguez nos aporta bastante grado de modularidad.