Archive

Posts Tagged ‘Seguridad’

Cómo guardar contenido cuando el usuario no tiene permisos en una lista de SharePoint


Cómo guardar contenido cuando el usuario no tiene permisos en una lista

He observado con mi equipo de trabajo, colegas y sharepointeros en general que una de los problemas que tenemos cuando comenzamos a usar el modelo de objetos de SharePoint es el tema de la seguridad. De hecho a mí mismo me pasaba que tenía que crear un WebPart, el cual obtendría datos de una lista o biblioteca, los renderiza, y al final presenta alguna acción que causa una actualización en dicha fuente. Por supuesto, en ambiente de desarrollo todo jala de pelos, porque usamos la cuenta del administrador. Pero tan pronto publicamos para que el cliente pruebe, ¡boom! Un SecurityException es nuestra recompensa.

Por supuesto, el problema en estos casos suele ser que el usuario que ha ingresado no tiene permisos para acceder a una biblioteca. El caso suele ser sencillo: darle permisos a dicho usuario, pues si no los tiene, su razón ha de tener. En otras palabras, si el usuario no tiene permiso es por la lógica de seguridad implementada para la lista o biblioteca en cuestión.

Sin embargo hay ocasiones en las que el cliente quiere compartir sólo cierta información. Por ejemplo, imagina una lista donde se guarda información sobre los proveedores de una empresa y la tarifa que se les paga a cada uno por unidad de servicio. Por otro lado, el jefe de compras decide que quiere exponer una forma para que los usuarios de otras áreas busquen información de los proveedores, pero no quiere que la tarifa quede expuesta. Además, si el proveedor no buscado no existe, el usuario debería poder darlo de alta (nuevamente, sin capturar la tarifa). ¿Qué podemos hacer?

No podemos simplemente dar permisos de lectura a los usuarios generales porque entonces podrían visualizar la página AllItems.aspx de la lista y les mostraría la tarifa. Luego, lo que tenemos que hacer es un WebPart que consulte la lista y haga la búsqueda ahí, aunque el usuario autenticado no tenga permisos de lectura en la misma. Y lo mismo ocurre con el alta de un proveedor: el usuario deberá ingresar la información sin tener permisos para hacerlo.

Afortunadamente, el modelo de objetos de SharePoint nos proporciona una forma de hacer el “by-pass” de seguridad. La clase SPSecurity (ubicada en el espacio de nombres Microsoft.SharePoint) contiene un método llamado RunWithElevatedPrivileges. Éste toma un sólo parámetro: un delegado de un método sin parámetros que regresa void.

protected override void CreateChildControls()
{ 
    SPGridView myView = new SPGridView();
    ...

    SPSecurity.RunWithElevatedPrvileges( () => {

        // aunque el usuario autenticado no tenga
        // permisos para leer o modificar la lista "Proveedores",
        // al ejecutarse este delegado anónimo sí que
        // los tiene. 
        SPList list = SPContext.Current.Lists["Proveedores"];
        myView.DataSource = list.Items;
        myView.DataBind();
    });

    Controls.Add(myView);
}

El código anterior muestra la forma básica de utilizar dicho método. Como ves, le pasamos un método anónimo. Si el usuario que carga el WebPart no tiene permisos para ingresar, éste código le garantizará el pase. Lo mismo para actualizar datos.

protected void OnOK(object sender, EventArgs args)
{
    SPSecurity.RunWithElevatedPrivileges(() => {

        SPList list = SPContext.Current.Lists["Proveedor"];
        SPListItem item = list.Items[0];
        item["Título"] = "Nuevo título";
        item.Update();
    });
}

Ahora bien, hay que aclarar un asunto. SPSecurity.RunWithElevatedPrivileges no es que de un paso libre así tal cual. Lo que hace en realidad es darle los privilegios del usuario que corre el proceso. Así, si corremos el código anterior dentro de un WebPart, éste se ejecutará bajo los permisos que tenga la cuenta asociada al Application Pool (y usualmente será el administrador de la granja). Pero por otro lado, si corriésemos un código similar en, digamos, un temporizador de SharePoint o un Workflow, la cuenta que tendrá será la asociada a estos procesos (digamos, al servicio SPTimerv4).

Sin embargo, por lo anterior, hay que tener cuidado ya que no siempre funcionará. Si por ejemplo, ejecutamos desde un temporizador y la cuenta asociada a ésta no tiene permiso en el recurso que queremos consultar (i.e. la lista “Proveedor” en nuestro ejemplo) tendremos un SPException como si no tuviésemos permiso. Lo mismo ocurre si ejecutamos dicho código desde una aplicación (de consola o Windows Forms): el usuario que ejecuta la aplicación será quien deba tener permisos en la lista.

Así que ya sabes, no siempre será garantía su empleo. Sin embargo, para WebParts, páginas de aplicación y controles, siempre será una muy buena opción.

Trabajar con identificadores únicos


El uso de tecnologías COM popularizó el concepto de identificadores únicos, o GUIDs (Globally Unique Identifier). Los GUIDs son valores de 128 bits usualmente agrupados en cuatro bloques:

1.- Un bloque de 32 bits.

2.- Un bloque de 16 bits.

3.- Un bloque de 16 bits.

4.- Un bloque de 64 bits (agrupado en ocho segmentos de un byte).

Usualmente se representan en texto de la siguiente forma:

{21EC2020-3AEA-1069-A2DD-08002B30309D}

donde los primeros ocho caracteres comprendidos entre las llaves son el primer bloque, luego vienen cuatro y cuatro caracteres para el segundo y tercer bloque, y finalmente un bloque de cuatro caracteres seguido por uno más de doce, que en conjunto representan el último bloque de 64 bits.

Dado que los GUIDs son valores de 128 bits, el número de combinaciones existentes es de 2128, lo cual raya en los 3.4 x 1038 (3.4 seguido de 37 ceros). La posibilida de que aleatoriamente se generen valores iguales es ínfima, además de que el algoritmo para generarlos pone candados para evitar que esto ocurra.

Los GUIDs se usan en muchos lados. COM los usa para identificar sus componentes, Windows los usa para identificar los nombres de archivos, carpetas, usuarios y otros símbolos de sistema, e incluso son usados en bases de datos para identificar registros únicos.

Tan usados son que el .NET Framework viene con su propia clase, System.Guid, para trabajar con éstos. Pero ¿qué pasa con C y C++? No hay clase alguna como en.NET… pero sí existe una estructura, GUID, la cual se define así:

typedef struct _GUID {
    unsigned long  Data1;
    unsigned short Data2;
    unsigned short Data3;
    unsigned char  Data4[ 8 ];
} GUID;

Ahora, la estructura vacía no ayuda mucho. Sin embargo existen funciones del API de Windows para trabajar con ésta. Resumo algunas a continuación.

1.- UuidCreateNil. Crea un GUID vacío (i.e. {00000000-0000-0000-0000-000000000000}).

2.- UuidFromString. Crea una estructura GUID a partir de una cadena de texto. Por ejemplo:

const wchar_t* sz = L"{21EC2020-3AEA-1069-A2DD-08002B30309D}";
GUID guid;
::UuidFromString((unsigned short*)sz, &guid);

3.- UuidCreate. Crea una estructura GUID de acuerdo al algoritmo para la creación de identificadores únicos. Easy peasy:

GUID guid;
::UuidCreate(&guid);

Nota que el GUID usa el primer algoritmo para la creación de GUIDs, el cual se basa en la dirección MAC de la tarjeta de red. Dado que esto crea un hueco de seguridad (cualquier GUID puede rastrear la MAC del equipo donde se creó), se recomienda crear UuidCreateSequential, función que usa un algoritmo más reciente.

4.- UuidToString. Hace lo contrario a UuidFromString: nos devuelve la representación de texto a partir de un GUID.

GUID guid;
::UuidCreate(&guid);

wchar_t* pstr;
::UuidToString(&guid, (unsigned short**)&pstr);
// hacer algo con pstr
::RpcStringFree(pstr);

5.- UuidEqual. Nos permite determinar si dos GUIDs son iguales.

RPC_STATUS status;
GUID guid1 = ...;
GUID guid2 = ...;
BOOL equals = ::UuidEqual(&guid1, &guid2, &status);

6.- UuidCompare. Similar a UuidEqual, solo que nos dice si un GUID es mayor o menor a otro.

RPC_STATUS status;
GUID guid1 = ...;
GUID guid2 = ...;
int compare = ::UuidCompare(&guid1, &guid2, &status);

7.- UuidIsNil. Regresa TRUE si el GUID es vacío. Es similar a usar UuidEquals con un GUID, y con un valor devuelto por UuidCreateNil.

8.- UuidCreateSequential. Similar a UuidCreate, sólo que usa el último algoritmo para creación de GUIDs, el cual no se basa en la dirección MAC de la tarjeta de red. Checa el valor devuelto por la función, ya que puede devolverte tres valores:

  • RPC_S_OK si todo salió bien.
  • RPC_S_UUID_LOCAL_ONLY si el GUID es único sólo en la máquina actual.
  • RPC_S_UUID_NO_ADDRESS si el hardware del equipo no permite la creación de un valor único.

Puedes buscar más información en MSDN

Categorías:Apunte, C++, WIN-32 Etiquetas: ,

Consulta, descarga y exportación de perfiles de usuarios en SharePoint


Puedes consultar mi último código en la galería de código de MSDN: consulta, descarga y exportación de perfiles de usuarios en SharePoint. Algunos extractos a continuación.

Introducción

El presente ejemplo muestra cómo utilizar los servicios web para construir una aplicación que permita consultar, descargar y exportar los perfiles de usuario de SharePoint Server 2010, así como información suplemental.

Construyendo el ejemplo

El código de ejemplo es una solución hecha con Visual Studio 2010 para .NET Framework 3.5. No tiene alguna otra dependencia. Hace referencia a dos servicios web, cuyas clases proxy se generaron a partir de los servicios _vti_bin/UserGroup.asmx y _vti_bin/UserProfileService.asmx. El primer servicio está disponible en SharePoint Foundation 2010 y el segundo en SharePoint Server 2010. Nota: este programa funciona también con versiones anteriores de SharePoint, aunque es posible que algunas características no estén disponibles.

Descripción

El programa cuenta con un componente llamado ProfileSource. Éste actúa como fuente de datos para el programa, y contiene los métodos que obtienen la información de los perfiles mediante consultas hechas a los servicios web. Este componente implementa gran parte de la lógica de la aplicación. En particular el método OnDoWork es quien se encarga de descargar la información de los perfiles. A continuación se muestran extractos del mismo.

_worker.ReportProgress(0, Resources.ProfileSource_StartingServices); 
userGroup = new UserGroup(); 
PrepareService(userGroup, _connectionProperties.UserGroupWebService); 
userProfile = new UserProfileService(); 
PrepareService(userProfile, _connectionProperties.UserProfileWebService); 
 
_worker.ReportProgress(0, Resources.ProfileSource_DownloadSchematics);  
Table = CreateTable(userProfile);

El método PrepareService establece algunas propiedades básicas del servicio web, como la URL de donde se encuetra y las credenciales. Nota que éstas últimas varían dependiendo de si el usuario ha seleccionado usar las credenciales por defecto o no. Por otra parte, el método CreateTable utiliza el servicio UserProfileService y su método GetUserProfileSchema para descargar todos los campos disponibles desde el servidor, y con base en ellos generar el esquema de un objeto DataTable.

XmlNode mainNode = null; 
if (ConnectionProperties.AllSiteUsers) 
{ 
    _worker.ReportProgress(0, Resources.ProfileSource_DownloadAllUserInfo); 
    mainNode = userGroup.GetAllUserCollectionFromWeb(); 
} 
else 
{ 
    _worker.ReportProgress(0, Resources.ProfileSource_DownloadUserInfo); 
    mainNode = userGroup.GetUserCollectionFromWeb(); 
} 
_worker.ReportProgress(0, Resources.ProfileSource_UserInfoDownloaded);

El extracto anterior obtiene los usuarios disponibles del sitio (todos los registrados, o bien cualquiera que haya ingresado), mediante la llamada a los métodos GetAllUserCollectionFromWeb o GetUserCollectionFromWeb del servicio web UserGroup, según sea el caso.

XmlDocument doc = new XmlDocument(); 
doc.LoadXml(mainNode.OuterXml); 
XmlNamespaceManager nsmgr = new XmlNamespaceManager(doc.NameTable); 
nsmgr.AddNamespace("defns", "http://schemas.microsoft.com/sharepoint/soap/directory/"); 
 
string xpath; 
if (ConnectionProperties.AllSiteUsers) 
    xpath = "defns:GetAllUserCollectionFromWeb/defns:Users/descendant::defns:User"; 
else 
    xpath = "defns:GetUserCollectionFromWeb/defns:Users/descendant::defns:User"; 
XmlNodeList userNodes = doc.SelectNodes(xpath, nsmgr); 

Una vez descargados los usuarios, necesitamos analizar el documento XML regresado. Para ello hacemos uso de XPath. El extracto anterior muestra el código que genera la consutla XPath, y que nos devuelve una lista con todos los nodos que contienen la información de los usuarios.

Posteriormente, recorremos cada elemento de la lista y llamamos al método GetUserProfile, el cual usará al método GetUserProfileByName del servicio web UserProfileService. Los resultados obtenidos los insertamos como una fila en nuestra tabla, como se muestra en el siguiente extracto.

foreach (XmlNode userNode in userNodes) 
{ 
    double stepDouble = ((double)++count / (double)maxCount) * 100.0; 
    int stepInt32 = Math.Min((int)Math.Round(stepDouble, 0), 100); 
    string loginName = userNode.Attributes["LoginName"].Value; 
    _worker.ReportProgress(stepInt32, string.Format(Resources.ProfileSource_DownloadingProfile, loginName, stepInt32)); 
    DataRow row = GetUserProfile(userProfile, loginName); 
    Table.Rows.Add(row); 
 
    if (count >= maxCount) 
        break; 
}

Esta porción de código muestra cómo el método GetUserProfile obtiene el perfil de un usuario.

PropertyData[] allData = service.GetUserProfileByName(loginName); 
foreach (PropertyData data in allData) 
{ 
    if (row.Table.Columns.Contains(data.Name)) 
    { 
        if (data.Values.Length > 0 && data.Values[0].Value != null) 
            row[data.Name] = data.Values[0].Value.ToString(); 
        else 
            row[data.Name] = "[Empty]"; 
    } 
}

Adicional a los métodos mencionados, el componente cuenta con los siguientes miembros.

  • Table. Un objeto DataTable que contiene la información descargada de los perfiles.
  • ConnectionProperties. Un componente ConnectionProperties, el cual contiene la información necesaria para establecer la conexión y descarga de elementos (i.e. usuario, contraseña, URLs, etc.).
  • GetColleagues. Consulta al servicio web UserProfileService por los colegas de un usuario determinado.
  • GetInCommon. Consulta al servicio web UserProfileService por información que el usuario actual tenga en común con otro usuario determinado.
  • GetLinks. Consulta al servicio web UserProfileService por los enlaces de un usuario determinado.
  • GetMemberships. Consulta al servicio web UserProfileService por las membrecías de un usuario determinado.
  • GetOrganizations. Consulta al servicio web UserProfileService por las organizaciones a las que pertenece un usuario determinado.
  • GetPinnedLinks. Consulta al servicio web UserProfileService por los enlaces marcados de un usuario determinado.
  • SaveAs. Exporta los perfiles de usuario hacia un archivo de valores separados por coma (CSV) o a un XML.
  • Loaded. Evento que se dispara cuando el componente ha terminado de descargar la información de perfiles.
  • LoadProgress. Evento que se dispara cuando hay un avance significativo durante la descarga de información de perfiles.
  • ProfileFailure. Evento que se dispara cuando se intenta obtener el perfil de un usuario y éste falla (i.e. por cuestiones de permisos, porque no existe, etc.), en cuyo caso se puede continuar con el siguiente perfil (comportamiento por defecto) o bien terminar con el proceso.
  • Failure. Evento que se dispara cuando la carga de información de usuarios falla y lanza una excepción.

Como última precisión, comentar que el programa está localizado y puede visualizarse en inglés y en español.

Cómo obtener todos los usuarios de un sitio


Si estás haciendo un desarrollo para SharePoint y de pronto quisieras saber cuáles son los usuarios que hay en un sitio, ¿qué hacer? Easy peasy. Comenzamos por referenciar nuestro Microsoft.SharePoint.dll de siempre e importamos el espacio de nombres Microsoft.SharePoint. Luego obtenemos un objeto SPWeb (ya sea mediante el SPContext.Current o creando un nuevo SPSite).

Si te das cuenta, SPWeb cuenta con al menos tres propiedades interesantes, todas las cuales regresan un objeto SPUserCollection (una colección de objetos SPUser), a saber:

    ¿Cuál es la diferencia entre estas tres propiedades? Pensando un poquito, SiteUsers salta a la vista y en efecto es lo que sospechas: esa propiedad regresa a todos los usuarios existentes en la colección de sitios. ¿Cuál será la diferencia entre las otras dos? Pues, en esencia, que Users te regresa todos los usuarios registrados en el sitio, mientras que AllUsers te regresa no nada más los usuarios registrados en el sitio, sino todos aquellos que hayan entrado al sitio alguna vez, o incluso que hayan sido referenciados en alguna lista (digamos, alguna alerta o algo por el estilo).

    Puede obtenerse un usuario específico por el índice de la colección, o bien por el nombre de usuario (de la forma “dominio\\nombreUsuario”), usando el indizador. O bien alguno de los métodos expuestos, como GetByID o GetByEmail.

    SPWeb web = SPContext.Current.Web;
    
    // propiedades de SPUser
    foreach (SPUser user in web.AllUsers)
    {
        string email = user.Email;
        string name = user.Name;
        // etc
    }
    
    // Obtener un usuario específico por correo electrónico
    SPUser user = web.AllUsers.GetByEmail("fernando.gomez@dominio.com");
    
    // Buscar todos los usuarios que comiencen con "Fer"
    var query = from SPUser user in web.AllUsers
                         where user.Name.BeginsWith("Fer")
                         select user;
    SPUser[] ferUsers = query.ToArray();
    
    // Easy Peasy
    

¿Y qué pasa si queremos consultar la información del perfil, desde un lugar fuera del servidor (i.e. una aplicación externa o desde Silverlight)? Pues usamos un servicio web, en concreto: UserGroup.asmx.

Este servicio web cuenta con tres métodos que te resultarán familiares:

  • UserGroup.GetAllUserCollectionFromWeb.
  • UserGroup.GetUserCollectionFromSite.
  • UserGroup.GetUserCollectionFromWeb.

¿Te suenan? Pues claro, son los equivalentes a SPWeb.AllUsers, SPWeb.SiteUsers y SPWeb.Users. Los métodos regresan un objeto tipo XmlNode, el cual contiene un XML más o menos con este esquema:

<GetUserCollectionFromWeb xmlns="http://schemas.microsoft.com/sharepoint/soap/directory/">
    <Users>
        <User ID="190" Sid="S-1-5-21-944133582-289434890-317593308-21562" 
            Name="Fernando Gómez" LoginName="dominio\fernando.gomez" 
            Email="fernando.gomez@dominio.com" Notes="" IsSiteAdmin="False" 
            IsDomainGroup="False" Flags="0" /> 
        <User ID="111" Sid="S-1-5-21-944133582-289434890-317593308-22617" 
            Name="Fulano De Tal" LoginName="dominio\fulano.detal" 
            Email="fulano.detal@dominio.com" Notes="" IsSiteAdmin="False" 
            IsDomainGroup="False" Flags="0" /> 
        <!-- etc -->
    </Users>
</GetUserCollectionFromWeb>

En esencia, el XML devuelto tiene un primer nodo, cuyo nombre corresponde al nombre del método (por ejemplo, de haber llamado a GetAllUserCollectionFromWeb, el nodo habría tenido este otro nombre), seguido de un nodo Users, el cuál contiene una colección de nodos User. Éstos últimos contienen atributos, cada uno representando información sobre el usuario. En el ejemplo te muesto algunos, como el ID interno, el nombre, la cuenta (LoginName) y el correo electrónico.

Cargar esta información, pues, es relativamente sencilla, dado que es el mismo esquema. Puedes cargarlo iterando sobre el nodo, o bien mediante LINQ to XML. Pero a mí me gusta usar XPath.

Es relativamente sencillo, de hecho, usar XPath. A primera instancia, puede que pienses en llamar a SelectNodes sobre el XmlNode, quizás algo así:

UserGroup service = new UserGroup();
service.Url = "http://labwf";
service.Credentials = CredentialCache.DefaultCredentials;

XmlNode rootNode =  service.GetAllUserCollectionFromWeb();
string xpath = "GetAllUserCollectionFromWeb/Users/descendant::User";
XmlNodeList nodes = rootNode.SelectNodes(xpath);
foreach (XmlNode node in nodes)
{
    string loginName = node.Attributes["LoginName"];
    string email = node.Attributes["Email"];
    // etc
}

service.Dispose();

Pero pues toma chango tu banano, lo anterior te regresará un XmlNodeList vacío. La razón es que el XML devuelto por el servicio web contiene un espacio de nombres especificado. Pero no uno cualquiera, sino el que se usa por default: xmlns.

Para solventar este problema tenemos que llamar SelectNodes con un gestionador de espacio de nombres (XmlNamespaceManager), al cual le añadiremos el espacio de nombres que viene en el XML junto con un prefijo con el que lo queramos identificar (en mi caso, he escogido “defns”). Pero el gestionador necesita de una tabla de nombres en su constructor, y la forma más sencilla de obtenerla, según pude ver, es crear un XmlDocument y usar su propiedad NameTable.

Una vez hecho esto, podremos usar el SelectNodes sober el XmlDocument, pero tendremos que cambier el XPath para que referencie el prefijo que añadimos al gestionador de espacios de nombres. Por ejemplo, en mi caso, el XPath quedaría como:

defns:GetAllUserCollectionFromWeb/defns:Users/descendant::defns:User

Así luciría nuestro código.

// preparar el servicio web
UserGroup service = new UserGroup();
service.Url = "http://labwf";
service.Credentials = CredentialCache.DefaultCredentials;

// generar el documento xml con los usuarios
XmlNode rootNode =  service.GetAllUserCollectionFromWeb();
XmlDocument doc = new XmlDocument();
doc.LoadXml(rootNode.OuterXml);

// crear el gestionador de espacios de nombres 
XmlNamespaceManager manager = new XmlNamespaceManager(doc.NameTable);
manager.AddNamespace("defns", "http://schemas.microsoft.com/sharepoint/soap/directory/");

// ejecutar nuestro xpath...
string xpath = "defns:GetAllUserCollectionFromWeb/defns:Users/descendant::defns:User";
XmlNodeList nodes = rootNode.SelectNodes(xpath);

//... y ahora sí iterar sobre los resultados
foreach (XmlNode node in nodes)
{
    string loginName = node.Attributes["LoginName"];
    string email = node.Attributes["Email"];
    // etc
}

service.Dispose();

Bien, pues eso es todo por el momento. Ya veremos más cosas de usuarios en el siguiente post…

Perfiles de usuario e información social


En esta era donde las redes sociales inundan nuestras vidas, es cada vez más común ver empresas y corporaciones que piden características de éstas en sus propios sitios corporativos.

SharePoint 2010 ha dado pasos significativos en esa dirección mediante la provisión de mecanismos para habilitar características similares a las encotnradas en redes sociales. Así pues, cuando un cliente nos pida desarrollar tal o cual característica social, sabremos que podemos apoyarnos en lo que SharePoint nos ofrece.

Quizás lo primero que viene a la mente, al respecto, sean los perfiles de usuario. Es decir, dado un usuario cualquiera en SharePoint, quisiéramos obtener su información particular: nombre, teléfonos, departamentos, móviles, jefe, direcciones, etc. En SharePoint 2010 es posible obtener esta información, si se tienen los permisos necesarios. Existen dos formas de hacerlo: mediante el modelo de objetos de SharePoint, o bien mediante los servicios web que se exponen en /_vti_bin/ de cada sitio. En esta ocasión usaremos los servicios web.

O mejor dicho, usaremos un servicio web en particular: UserProfileService.asmx. Este lo podemos referenciar desde http://misitio/_vti_bin/UserProfileService.asmx. Al hacerlo, nos generará varias clases proxy, entre las que se encuentra UserProfileService.

Vamos por partes. Lo primero que nos interesa es obtener el perfil dado un usuario determinado. Pues bien, es cuestión de llamar a UserProfileService.GetUserProfileByName. Este método toma como parámetro el nombre de la cuenta de un usuario de la forma “dominio\cuenta” (digamos, “ASGARD\thor”) y regresa una colección de objetos PropertyData (esta clase, por cierto, es una clase proxy generada al momento de referenciar el servicio web). Cada PropertyData representa una propiedad del usuario, así que para obtener todas hay que iterar sobre el array.

El siguiente ejemplo muestra cómo obtener la propiedad PreferredName.

using System;
using System.Linq;
using System.Net;
using UserProfileServiceProxy; // el espacio de nombre de tu servicio web
...

UserProfileService service = new UserprofileService();
service.Url = "http://labwf/_vti_bin/UserProfileService.asmx";
service.Credentials = CredentialCache.DefaultCredentials;

PropertyData[] properties = service.GetUserProfileByName("LABWF\\thor");
service.Dispose();

var query = from property in properties
                     where propety.Name.Equals("PreferredName")
                         && property.Values.Length > 0
                         && property.Values[0] != null
                         && property.Values[0].Value != null
                     select property.Values[0].Value.ToString();

string preferredName = query.FirstOrDefault();

                     

Algunas de las propiedades más interesantes (i.e. PropertyData.Name) son las siguientes: AccountName, ADGuid, CellPhone, Department, FirstName, HomePhone, LastName, Manager, PictureURL, PreferredName, SPSBirthday, SPSHireDate, SPSJobTitle, Title, UserName, WebSite, WorkEmail y WorkPhone. Pero hay otras, ¡muchas más!

Si estamos en una aplicación en consola, podemos iterar sobre cada elemento de forma más o menos genérica:

using System;
using System.Linq;
using System.Net;
using UserProfileServiceProxy; // el espacio de nombre de tu servicio web
...

UserProfileService service = new UserprofileService();
service.Url = "http://labwf/_vti_bin/UserProfileService.asmx";
service.Credentials = CredentialCache.DefaultCredentials;

PropertyData[] properties = service.GetUserProfileByName("LABWF\\thor");
service.Dispose();

foreach (PropertyData property in properties)
{
    string name = property.Name;
    string value;
    if (property.Values.Length > 0
        && property.Values[0] != null
        && property.Values[0].Value != null)
        value = property.Values[0].Value.ToString();
    else
        value = string.Empty;

    Console.WriteLine("Propiedad: {0} Valor: {1}", name, value);
}

Ahora bien, aunque es mucha información relacionada al usuario, y ciertamente reviste importancia, no lo es todo en una red social. En particular, nos interesará ver otros elementos que estén asociados a un usuario particular. SharePoint provee, a través del mismo WebService, información adicional. Veamos una por una.

Quizá una de las más usadas es ver qué colegas tiene un usuario determinado. Un colega es un usuario que es marcado como conocido de otro usuario. Así, un usuario determinado puede tener a varios colegas. Esa información se obtiene usando el método UserProfileService.GetUserColleagues, pasando como parámetro la cuenta del usuario en cuestión. Easy peasy:

using System;
using System.Linq;
using System.Net;
using UserProfileServiceProxy; // el espacio de nombre de tu servicio web
...

UserProfileService service = new UserprofileService();
service.Url = "http://labwf/_vti_bin/UserProfileService.asmx";
service.Credentials = CredentialCache.DefaultCredentials;

ContactData[] allData = service.GetUserColleagues("LABWF\\thor");
service.Dispose();

foreach (ContactData data in allData)
{
    Console.WriteLine("Colega: {0}", data.AccountName);
    Console.WriteLine("*** Email: {0}", data.Email);
    Console.WriteLine("*** Group: {0}", data.Group);
    Console.WriteLine("*** ID : {0}", data.ID);
    Console.WriteLine("*** IsInWorkGroup : {0}", data.IsInWorkGroup);
    Console.WriteLine("*** Name : {0}", data.Name);
    Console.WriteLine("*** Privacy : {0}", data.Privacy);
    Console.WriteLine("*** Title : {0}", data.Title);
    Console.WriteLine("*** Url : {0}", data.Url);
    Console.WriteLine("*** UserProfileID : {0}", data.UserProfileID);
    Console.WriteLine();
}

El método GetUserColleagues regresa una colección de ContactData. Ésta clase (igual, proxy generada al referenciar el servicio web) tiene varias propiedades, relacionadas con el colega. Cada elemento de la colección, obvio, representa uno.

Otro aspecto importante que SharePoint ofrece es la posibilidad de que cada usuario tenga un enlace (link), digamos, como favorito. O que otro usuario le envíe un enlace como referencia. Bueno, pues UserProfileService cuenta con dos métodos para llevar a cabo lo anterior: GetUserLink y GetUserPinnerLinks. La diferencia entre ambos es que GetUserPinnerLniks son aquellos enlaces que el usuario ha marcado como favoritos, de manera explícita. El procedimiento es muy similar: ambos métodos regresan una colección de clases proxy, QuickLinkData y PinnedLinkData, respectivamente.

using System;
using System.Linq;
using System.Net;
using UserProfileServiceProxy; // el espacio de nombre de tu servicio web
...

UserProfileService service = new UserprofileService();
service.Url = "http://labwf/_vti_bin/UserProfileService.asmx";
service.Credentials = CredentialCache.DefaultCredentials;

QuickLinkData[] quickLinks = service.GetUserLinks("LABWF\\thor");
foreach (QuickLinkData data in quickLinks)
{
    Console.WriteLine("Enlace: {0}", data.Name);
    Console.WriteLine("*** Group: {0}", data.Group);
    Console.WriteLine("*** ID : {0}", data.ID);
    Console.WriteLine("*** Privacy : {0}", data.Privacy);
    Console.WriteLine("*** Url : {0}", data.Url);
    Console.WriteLine();
}

PinnedLinkData[] pinnedLinks = service.GetUserPinnedLinks("LABWF\\thor");
foreach (PinnedLinkData data in pinnedLinks)
{
    Console.WriteLine("Enlace marcado: {0}", data.Name);
    Console.WriteLine("*** ID : {0}", data.ID);
    Console.WriteLine("*** Url : {0}", data.Url);
    Console.WriteLine();
}

service.Dispose();

Las membresías son otro aspecto interesante que nos ofrece nuestro SharePoint social. Es decir, a qué grupos pertenece tal usuario. Similar a esto, también nos interesa saber a qué organizaciones pertenece el usuario. Para hacerlo, usaremos GetUserMemberships y GetUserOrganization, los cuales regresan (como cabría esperar) una colección de objetos de tipo MembershipData y OrganizationProfileData, respectivamente (¿comienzas a ver el patrón?).

using System;
using System.Linq;
using System.Net;
using UserProfileServiceProxy; // el espacio de nombre de tu servicio web
...

UserProfileService service = new UserprofileService();
service.Url = "http://labwf/_vti_bin/UserProfileService.asmx";
service.Credentials = CredentialCache.DefaultCredentials;

MembershipData[] memberships = service.GetUserMemberships("LABWF\\thor");
foreach (MembershipData data in memberships)
{
    Console.WriteLine("Membrecía: {0}", data.DisplayName);
    Console.WriteLine("*** Group: {0}", data.Group);
    Console.WriteLine("*** ID : {0}", data.ID);
    Console.WriteLine("*** MailNickname: {0}", data.MailNickname);
    Console.WriteLine("*** MemberGroup: {0}", data.MemberGroup.SourceReference);
    Console.WriteLine("*** MemberGroupID: {0}", data.MemberGroupID);
    Console.WriteLine("*** Privacy : {0}", data.Privacy);
    Console.WriteLine("*** Source: {0}", data.Source);
    Console.WriteLine("*** Url : {0}", data.Url);
    Console.WriteLine();
}

OrganizationProfileData[] organizations = service.GetUserOrganizations("LABWF\\thor");
foreach (OrganizationProfileDatadata in organizations)
{
    Console.WriteLine("Organización: {0}", data.DisplayName);
    Console.WriteLine("*** ID : {0}", data.RecordID);
    Console.WriteLine();
}

service.Dispose();

El perfil de un usuario, más su información relacionada (en este caso: colegas, organizaciones, membrecías, enlaces y favoritos) cubre gran parte de la información que uno cabría esperar de una red social (corporativa). Pero SharePoint va un paso más allá: no nada más nos provee la posibilidad de consultar esta información, sino también permite compararla con la de otros usuarios. Por ejemplo: ¿qué usuarios tenemos en común? ¿Qué membrecías compartimos? ¿Cuál es nuestro jefe común?

El servicio web expone el método GetInCommon, el cual toma la cuenta de un usuario como parámetro. Este método compara dicho usuario contra el usuario actual (en este caso, el usuario almacenado en las credenciales del servicio web, o dicho de otra forma, el usuario que manda llamar al servicio web), y rergesa una colección (sorpresa sorpresa) de objetos tipo InCommonData. Esta clase proxy tiene tres miembros: una colección de ContactData, Colleagues, que representa los colegas en común; una colección de MembershipData, Memberships, que representa las membrecías e común; y una propiedad de tipo ContactData, Manager, que representa el jefe en común.

using System;
using System.Linq;
using System.Net;
using UserProfileServiceProxy; // el espacio de nombre de tu servicio web
...

UserProfileService service = new UserprofileService();
service.Url = "http://labwf/_vti_bin/UserProfileService.asmx";
service.Credentials = CredentialCache.DefaultCredentials;

InCommonData data = service.GetInCommon("LABWF\\thor");
service.Dispose();

Console.WriteLine("Jefe común: {0}", data.Manager.Name);

Console.WriteLine("Colegas en común:");
foreach (ContactData colleague in data.Colleagues)
    Console.WriteLine("*** {0}", colleague.Name);

Console.WriteLine("Membrecías en común:");
foreach (MembershipData membership in data.Memberships)
    Console.WriteLine("*** {0}", membership.DisplayName);

Console.WriteLine();

Alternativamente, puedes llamar a los métodos GetCommonManager, GetCommonColleagues y GetCommonMemberships, por separado, si no quieres traerte toda la información.

Pues bien, como puedes ver UserProfileService es el primer paso para crear aplicaciones sociales en SharePoint. Por supuesto, no es la única forma. Ahora hemos usado servicios web, pero también podríamos usar el modelo de objetos. Eso será asunto de otra entrada.

Determinar si un usuario es dueño, miembro o visitante del sitio


Hace algunas lunas estábamos platicando sobre usuarios en SharePoint y y grupos de usuarios. Por un lado vimos cómo identificar si un usuario pertenece a un grupo determinado o no. Y unos días después vimos una forma sencilla para determinar si el usuario autenticado pertenece o no a un grupo de SharePoint en particular. .

Continuando con esta línea, me gustaría ahora platicar sobre tres grupos que, por defecto, tienen todos los sitios que creamos en SharePoint. Estos son:

1.- Dueños del sitio (Site Owners)

2.- Miembros del sitio (Site Members)

3.- Visitantes (Site Visitors).

Por defecto, los dueños del sitio tendrán permisos en la totalidad del sitio, y podrán crear contenido, páginas, añadir usuarios a otros grupos, crear y eliminar bibliotecas, etc. Por su parte, los miembros del sitio pueden añadir y eliminar documentos, configurar alertas para sí mismos y, en suma, añadir y eliminar contenido de las bibliotecas y listas personalizadas. Por último, los visitantes sólo podrán leer el contenido del sitio, nunca modificarlo.

Por supuesto, uno puede crear más grupos, cambiar los roles de los existentes o eliminarlos. Sin embargo, los tres antes mencionados revisten una importancia particular para nosotros como programadores: los tres grupos están mapeados en propiedades del objeto SPWeb.

En efecto, SPWeb tiene tres propiedades: AssociatedOwnerGroup, AssociatedMemberGroup y AssociatedVisitorGroup, las tres de tipo SPGroup, y hacen referencia al grupo de dueños, miembros y visitantes del sitio, respectivamente. Por supuesto, si borras los sitios por defecto, o los cambias, las tres propiedades regresarán null.

image

Esto abre nuevas posibilidades, respecto a lo que veíamos en los puestos anteriores. Por ejemplo, ahora determinar si el usuario actualmente autenticado es administrador, dueño del sitio o visitante es tan sencillo como esto:

SPWeb web = SPContext.Current.Web;
if (web.CurrentUser.IsSiteAdmin) {
    // el usuario es el administrador del sitio
} else if (web.AssociatedOwnerGroup.ContainsCurrentUser) {
    // el usuario es dueño del sitio, tiene acceso entero a todo...
} else if (web.AssociatedMemberGroup.ContainsCurrentUser) {
    // el usuario es miembro, puede añadir contenido a listas y documentos
} else if (web.AssociatedVisitorGroup.ContainsCurrentUser) {
    // el usuario es visitante, puede ver pero no modificar el contenido
} else {
    // el usuario está en algún otro grupo
}

Easy Peasy. Obvio querrás validar primero que las propiedades no sean nulas, pero bueno, es suficiente para ejemplificar.

Más información sobre grupos y seguridad en SharePoint 2010:Determine permission levels and groups.

Cómo ofuscar nuestras contraseñas


Hola queridos amantes de la programación. En estos momentos me encuentro escribiendo la segunda parte del cuestionario sobre expresiones regulares. Pero no quería dejar pasar otro día sin compartir este pequeño consejo.

Sucede que cuando trabajamos en procesos de autenticación para nuestras aplicaciones muchas veces contamos con una tabla en una base de datos donde se guarda la información del usuario, así como su contraseña. Es una muy mala idea dejar las contraseñas así tal cual ya que si un atacante accede a nuestra base de datos, tendrá las contraseñas de todos los usuarios para hacer y deshacer el sistema a su antojo. Si bien el método más seguro sería encriptarlas (simétrica o asimétricamente) también es cierto que ésto conlleva complejidad ya que se requieren claves de encriptación que los usuarios deben conocer, o bien que deben ser estáticas y guardarse en el sistema de alguna forma. Así, en ocasiones no es posible realizar encriptación de las contraseñas. Pero debemos hacer un intento por ofuscarlas, cuando menos, para que sea difícil para el atacante obtenerlas.

Una primera aproximación para ello es codificar texto a base 64. Pero seamos honestos, esto sería muy ingenuo. Cualquiera que vea un texto con caracteres ininteligibles reconocerá el base 64 si ve caracteres = ó == al final de la cadena.

Pero podemos utilizar un algoritmo hash. .NET Framework, en su espacio de nombres System.Security.Cryptography nos provee de algunos algoritmos, como el MD 5 (clase MD5CryptoServiceProvider), el MD160 (RIPEMD160Managed), el SHA1 (SHA1CryptoServiceProvider) o el SHA256 (SHA256Managed). La forma de llevar a cabo esta ofuscación es la siguiente.

  1. Crear un objeto que represente algún algoritmo de hash.
  2. Guardar la contraseña como un array de bytes.
  3. Llamar al método ComputeHash del objeto del algoritmo y guardar el valor de retorno en lugar de la contraseña plana.
  4. Opcionalmente, la propiedad Hash de dicho objeto contendrá el hash utilizado, por si lo necesitas para algo más.

Como detalle adicional, dado que ComputeHash regresa un array de bytes, podemos convertirlo a su representación de texto en base 64. Ya no nos importa que sepan que es base 64 porque de todas formas estará hasheado, y el atacante no sabrá por dónde ir. Y si lo sabe, le tomará unos cuantos años averiguar la contraseña.

El siguiente ejemplo muestra lo anterior a través de un método que toma una contraseña, la ofusca y regresa su representación en base 64.

using System;
using System.Security.Cryptography;

static string ObfuscatePassword(string password)
{
  MD5 hash = new MD5CryptoServiceProvider();
  byte[] passwordBuffer = Encoding.UTF8.GetBytes(password);
  byte[] hashedPassword = hash.ComputeHash(passwordBuffer);
  string obfuscatedPassword = Convert.ToBase64String(hashedPassword);

  return obfuscatedPassword;
}

En la línea 6 creamos un objeto de algoritmo hash MD5, de los más básicos. Línea 7, codificamos la contraseña y la transformamos en bytes. Siguiente línea, aplicamos el hash sobre dichos bytes, y línea 9, convertimos dichos bytes a base 64.

Luego, cuando queramos validar al usuario solo tenemos que ofuscar la contraseña provista y compararla contra el valor previamente ofuscado: una simple comparación de texto bastará, dado que dos textos iguales regresan el mismo hash. Sirva el siguiente programa de ejemplo.

using System;
using System.Text;
using System.Security.Cryptography;

namespace Fermasmas.Wordpress.Com
{
  class Program
  {
    static string ObfuscatePassword(string password)
    {
      MD5 hash = new MD5CryptoServiceProvider();
      byte[] passwordBuffer = Encoding.UTF8.GetBytes(password);
      byte[] hashedPassword = hash.ComputeHash(passwordBuffer);
      string obfuscatedPassword = Convert.ToBase64String(hashedPassword);

      return obfuscatedPassword;
    }

    static void Main(string[] args)
    {
      string originalPassword = ObfuscatePassword("P@ssw0rd_123");
      Console.WriteLine("Hash de la contraseña original: {0}", 
          originalPassword);
      Console.WriteLine();

      ConsoleKey key = ConsoleKey.Q;
      do
      {
        Console.WriteLine();
        Console.WriteLine("Ingrese contraseña: ");
        
        string newPassword = ObfuscatePassword(Console.ReadLine());
        Console.WriteLine("Hash de la contraseña actual: {0}", newPassword);

        if (originalPassword == newPassword)
          Console.WriteLine("Acceso concedido...");
        else
          Console.WriteLine("Accesso denegado...");
        
        Console.WriteLine(
          "Presione cualquier tecla para continuar o 'Q' para terminar.");
        key = Console.ReadKey(true).Key;
      }
      while (key != ConsoleKey.Q);
    }
  }
}

Al correrlo, ingreso dos contraseñas inválidas y una tercera válida. Esta es la salida en la consola.

Hash de la contraseña original: lxCD4E8CcaDFA3M7017FwA==


Ingrese contraseña:
sorbetedelimon
Hash de la contraseña actual: 7vbXes/q8UMUylv34xop6A==
Accesso denegado...
Presione cualquier tecla para continuar o 'Q' para terminar.

Ingrese contraseña:
patatús
Hash de la contraseña actual: xpP+vt1fJz5OzX7DDyHyGA==
Accesso denegado...
Presione cualquier tecla para continuar o 'Q' para terminar.

Ingrese contraseña:
P@ssw0rd_123
Hash de la contraseña actual: lxCD4E8CcaDFA3M7017FwA==
Acceso concedido...
Presione cualquier tecla para continuar o 'Q' para terminar.

Eso es todo. Esta forma sencilla puede mejorar la seguridad de tu aplicación. Y también la puedes aplicar para cuando guardes información sensitiva en tus recursos o en tu web.config o app.config o cualquier otro archivo de configuración.

Categorías:.NET Framework, C#, Cómo hacer Etiquetas: ,