Trikks

The digital adventures of Eric Herlitz

No node exists with id ’0′ when using uSiteBuilder

Posted by trikks on March 30, 2013

So I’m experimenting with uSiteBuilder (from Vega IT) to bring some of my sites up to a more enterprise level. I did release the tool “Umbraco Masterpage CodeFile Fixer” in early 2012 and updated it with uSiteBuilder support recently.

In some of my experiments I got the error “No node exists with id ’0′”. Well that sucks.

My resolution

Quite simple for me since I build and deploy a separate binary with all uSiteBuilder stuff. Remove the binary from the bin folder and run the install process by navigation to http://yoursite/install/. This will setup or fix the root node. When done, you may try to place the uSiteBuilder binary in place again.

Cheers.

Posted in uSiteBuilder | Tagged: , | 2 Comments »

Fixing the SharePoint 2013 Search Error “The content processing pipeline failed to process the item.”

Posted by trikks on March 19, 2013

So I was indexing some external sites with my brand new SharePoint. Fun said I, nay said SharePoint and threw the error “The content processing pipeline failed to process the item.” like 3548 times or so.

Error

The fix

Add the account running the Search Service to the local admin group. If you are uncertain it’s the user that runs the noderunner.exe processes. I assume this should be done when registering the service accounts and don’t consider this as a bug, the search service stores the index on disk when it is being organized and baked before sending it to the database so this step seems natural to me (but I may be wrong on this as well).

processes

Did this work for you as well? Let us know.

Posted in SharePoint 2013 | Leave a Comment »

Where is the SVN binary in Mac OS X Mountain Lion 10.8? Aha! there it is.

Posted by trikks on March 18, 2013

The 10.8 release of Mac OS was great. So great that Apple even somewhere decided to remove the originally built in svn binaries. Well it figures since 99.9% of all Mac OS users never will know what it is. Instead they moved the binaries into Xcode – yay! This means that we have to drag down Xcode from the App Store of a whopping 3.8 GB if we want the previous “built in” version of SVN. Well that’s reasonable since the svn-package is like 348 KB on disk. Good thing that I’m not short on SSD space.

Anyway, if you go the Apple way (I did, but I’m using Xcode every now and then anyway) download and install Xcode. The svn binary is located in the folder

/Applications/Xcode.app/Contents/Developer/usr/bin/

The available SVN-binaries are

/Applications/Xcode.app/Contents/Developer/usr/bin/svn
/Applications/Xcode.app/Contents/Developer/usr/bin/svnadmin
/Applications/Xcode.app/Contents/Developer/usr/bin/svndumpfilter
/Applications/Xcode.app/Contents/Developer/usr/bin/svnlook
/Applications/Xcode.app/Contents/Developer/usr/bin/svnserve
/Applications/Xcode.app/Contents/Developer/usr/bin/svnsync
/Applications/Xcode.app/Contents/Developer/usr/bin/svnversion

To make SVN usable simply do something like this

sudo ln -s /Applications/Xcode.app/Contents/Developer/usr/bin/svn /usr/local/bin/svn

Thats it, have fun

Posted in Mac OS X | Tagged: | Leave a Comment »

Installing PHP 5.4 to Mac OS X (and Mountain Lion)

Posted by trikks on February 12, 2013

Hello there!

PHP 5.4 gave as a few new features, I was a bit blah at first until I got news about the built-in webserver – YES!!! Well that was like a year ago and all .NET and SharePoint development have consumed the most of my life, until now!

Installing (or upgrading) your Mac OS X box to use PHP 5.4

Alright, this is simple and only takes a few minutes. First up install the new version of PHP 5.4, I used the routines from http://php-osx.liip.ch/, really sweet! Just do this

sudo curl -s http://php-osx.liip.ch/install.sh | bash -s 5.4

This installed PHP 5.4 into this folder, you should probably verify that you got the same path

/usr/local/php5-20130210-223618/

Setup the built-in Apache to use PHP 5.4

Still in the terminal? Good, lets open the config for apache

sudo nano /etc/apache2/httpd.conf

Locate the LoadModule for PHP, it’s usually in the end of the list of LoadModules. Uncomment the old php module by putting a hash sign (#) in front of it and put the new module on a new line, in my case it looks like this

LoadModule php5_module /usr/local/php5-20130210-223618/libphp5.so

Save and exit (CTRL + o + enter) (CTRL + x), restart apache

sudo apachectl restart

Set the PHP library path

We do this so the new PHP binaries will be default on command line execution, it can be done in a numerous of ways. Here is my approach.

sudo nano /etc/paths

Enter the path to the bin folder of your new PHP library, mine now looks like this.

/usr/local/php5-20130210-223618/bin/
/usr/bin
/bin
/usr/sbin
/sbin
/usr/local/bin

Save and exit, you should probably restart bash at this point.

Simple test

Create and open a file, this is the default location of the webserver in os x

sudo nano /Library/WebServer/Documents/php.php

Enter the following

<?php
phpinfo();
?>

Save and close, go to your file by entering something like http://localhost/php.php

Screen Shot 2013-02-12 at 20.51.30

 

And we are done!

Posted in Mac OS X, PHP | Tagged: , | 5 Comments »

Enable SharePoint document conversions in PowerShell

Posted by trikks on February 8, 2013

Some of the document management features in SharePoint require certain features to be enabled. The most simple way to du such settings is by using PowerShell

Here is a snippet how to enable the DocumentConversions functionality

$app = Get-SPWebApplication http://enigma
$app.DocumentConversionsEnabled = 1
$app.Update()

Cheers

Posted in PowerShell, Sharepoint, SharePoint 2013, Uncategorized | Tagged: , , | Leave a Comment »

No way to enable the SharePoint 2013 Design Manager in SharePoint Foundation

Posted by trikks on January 12, 2013

So I was looking for a way to enable the Design Manager in  SharePoint 2013 Foundation. The Design Manager becomes available by enabling the “SharePoint Server Publishing Infrastructure” in Site Collection Features, the name here tells us this is a SharePoint Server feature.

So sorry, but there will be no way to use this sweet feature in Foundation setups.

Posted in SharePoint 2013 | Tagged: , | Leave a Comment »

Mapping Dictionary to Typed object using c#

Posted by trikks on December 31, 2012

There are some good frameworks that may help you mapping dictionaries and alike to typed objects but in some cases you simply want something simpler or custom for that matter.

The most important thing here is to transfer our dictionary to a typed object.

Example dictionary

Dictionary<string, string> dictionary = new Dictionary<string, string>();
dictionary.Add("Id", "1");
dictionary.Add("Name", "Trikks");
dictionary.Add("EMail", "info@example.com");

Example class

public class ExampleClass
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string EMail { get; set; }
}

Method

private static T DictionaryToObject<T>(IDictionary<string, string> dict) where T : new()
{
    var t = new T();
    PropertyInfo[] properties = t.GetType().GetProperties();
 
    foreach (PropertyInfo property in properties)
    {
        if (!dict.Any(x => x.Key.Equals(property.Name, StringComparison.InvariantCultureIgnoreCase)))
            continue;
 
        KeyValuePair<string, string> item = dict.First(x => x.Key.Equals(property.Name, StringComparison.InvariantCultureIgnoreCase));
 
        // Find which property type (int, string, double? etc) the CURRENT property is...
        Type tPropertyType = t.GetType().GetProperty(property.Name).PropertyType;
 
        // Fix nullables...
        Type newT = Nullable.GetUnderlyingType(tPropertyType) ?? tPropertyType;
 
        // ...and change the type
        object newA = Convert.ChangeType(item.Value, newT);
        t.GetType().GetProperty(property.Name).SetValue(t, newA, null);
    }
    return t;
}

Full example

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
 
namespace ConsoleApplication
{
    class Program
    {
        static void Main(string[] args)
        {
            Dictionary<string, string> dictionary = new Dictionary<string, string>();
            dictionary.Add("Id", "1");
            dictionary.Add("Name", "Trikks");
            dictionary.Add("EMail", "info@example.com");
 
            ExampleClass example = DictionaryToObject<ExampleClass>(dictionary);
        }
 
        private static T DictionaryToObject<T>(IDictionary<string, string> dict) where T : new()
        {
            var t = new T();
            PropertyInfo[] properties = t.GetType().GetProperties();
 
            foreach (PropertyInfo property in properties)
            {
                if (!dict.Any(x => x.Key.Equals(property.Name, StringComparison.InvariantCultureIgnoreCase)))
                    continue;
 
                KeyValuePair<string, string> item = dict.First(x => x.Key.Equals(property.Name, StringComparison.InvariantCultureIgnoreCase));
 
                // Find which property type (int, string, double? etc) the CURRENT property is...
                Type tPropertyType = t.GetType().GetProperty(property.Name).PropertyType;
 
                // Fix nullables...
                Type newT = Nullable.GetUnderlyingType(tPropertyType) ?? tPropertyType;
 
                // ...and change the type
                object newA = Convert.ChangeType(item.Value, newT);
                t.GetType().GetProperty(property.Name).SetValue(t, newA, null);
            }
            return t;
        }
    }
 
    public class ExampleClass
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string EMail { get; set; }
    }
}

Thats it, happy coding!

Posted in C# | Leave a Comment »

SharePoint 2013 TokenHelper.cs source code

Posted by trikks on December 30, 2012

This file is commonly referenced in the SharePoint 2013 documentation but the file it self is undocumented and the only way to have a look at it is by creating an “App for SharePoint 2013″ in Visual Studio.

Here is the code

using Microsoft.IdentityModel.S2S.Protocols.OAuth2;
using Microsoft.IdentityModel.S2S.Tokens;
using Microsoft.SharePoint.Client;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Globalization;
using System.IdentityModel.Selectors;
using System.IdentityModel.Tokens;
using System.IO;
using System.Linq;
using System.Net;
using System.Security.Cryptography.X509Certificates;
using System.Security.Principal;
using System.Text;
using System.Web;
using System.Web.Configuration;
using System.Web.Script.Serialization;
 
namespace SharePointApp1Web
{
 
    public class TokenHelper
    {
 
        #region public methods
 
        /// <summary>
        /// Configures .Net to trust all certificates when making network calls.  This is used so that calls 
        /// to an https SharePoint server without a valid certificate are not rejected.  This should only be used during 
        /// testing, and should never be used in a production app.
        /// </summary>
        public static void TrustAllCertificates()
        {
            //Trust all certificates
            System.Net.ServicePointManager.ServerCertificateValidationCallback =
                ((sender, certificate, chain, sslPolicyErrors) => true);
        }
 
        /// <summary>
        /// Retrieves the context token string from the specified request by looking for well-known parameter names in the 
        /// POSTed form parameters and the querystring. Returns null if no context token is found.
        /// </summary>
        /// <param name="request">HttpRequest in which to look for a context token</param>
        /// <returns>The context token string</returns>
        public static string GetContextTokenFromRequest(HttpRequest request)
        {
            string[] paramNames = { "AppContext", "AppContextToken", "AccessToken", "SPAppToken" };
            foreach (string paramName in paramNames)
            {
                if (!string.IsNullOrEmpty(request.Form[paramName])) return request.Form[paramName];
                if (!string.IsNullOrEmpty(request.QueryString[paramName])) return request.QueryString[paramName];
            }
            return null;
        }
 
        /// <summary>
        /// Validate that a specified context token string is intended for this application based on the parameters 
        /// specified in web.config. Parameters used from web.config used for validation include ClientId, 
        /// HostedAppHostName, ClientSecret, and Realm (if it is specified). If the <paramref name="appHostName"/> is not 
        /// null, it is used for validation instead of the web.config's HostedAppHostName. If the token is invalid, an 
        /// exception is thrown. If the token is valid, TokenHelper's static STS metadata url is updated based on the token contents
        /// and a JsonWebSecurityToken based on the context token is returned.
        /// </summary>
        /// <param name="contextTokenString">The context token to validate</param>
        /// <param name="appHostName">The URL authority, consisting of  Domain Name System (DNS) host name or IP address and the port number, to use for token audience validation.
        /// If null, HostedAppHostName web.config setting is used instead.</param>
        /// <returns>A JsonWebSecurityToken based on the context token.</returns>
        public static SharePointContextToken ReadAndValidateContextToken(string contextTokenString, string appHostName = null)
        {
            JsonWebSecurityTokenHandler tokenHandler = CreateJsonWebSecurityTokenHandler();
            SecurityToken securityToken = tokenHandler.ReadToken(contextTokenString);
            JsonWebSecurityToken jsonToken = securityToken as JsonWebSecurityToken;
            SharePointContextToken token = SharePointContextToken.Create(jsonToken);
 
            string stsAuthority = (new Uri(token.SecurityTokenServiceUri)).Authority;
            int firstDot = stsAuthority.IndexOf('.');
 
            GlobalEndPointPrefix = stsAuthority.Substring(0, firstDot);
            AcsHostUrl = stsAuthority.Substring(firstDot + 1);
 
            tokenHandler.ValidateToken(jsonToken);
 
            if (appHostName == null)
            {
                appHostName = HostedAppHostName;
            }
 
            string realm = Realm ?? token.Realm;
            string principal = GetFormattedPrincipal(ClientId, appHostName, realm);
            if (!StringComparer.OrdinalIgnoreCase.Equals(token.Audience, principal))
            {
                throw new Microsoft.IdentityModel.Tokens.AudienceUriValidationFailedException(
                    String.Format(CultureInfo.CurrentCulture,
                    "\"{0}\" is not the intended audience \"{1}\"", principal, token.Audience));
            }
 
            return token;
        }
 
        /// <summary>
        /// Retrieves an access token from ACS to call the source of the specified context token at the specified 
        /// targetHost. The targetHost must be registered for principal the that sent the context token.
        /// </summary>
        /// <param name="contextToken">Context token issued by the intended access token audience</param>
        /// <param name="targetHost">Url authority of the target principal</param>
        /// <returns>An access token with an audience matching the context token's source</returns>
        public static OAuth2AccessTokenResponse GetAccessToken(SharePointContextToken contextToken, string targetHost)
        {
 
            string targetPrincipalName = contextToken.TargetPrincipalName;
 
            // Extract the refreshToken from the context token
            string refreshToken = contextToken.RefreshToken;
 
            if (String.IsNullOrEmpty(refreshToken))
            {
                return null;
            }
 
            string realm = Realm ?? contextToken.Realm;
 
            string resource = GetFormattedPrincipal(targetPrincipalName, targetHost, realm);
            string clientId = GetFormattedPrincipal(ClientId, null, realm);
 
            OAuth2AccessTokenRequest oauth2Request =
                OAuth2MessageFactory.CreateAccessTokenRequestWithRefreshToken(
                    clientId,
                    ClientSecret,
                    refreshToken,
                    resource);
 
            // Get token
            OAuth2S2SClient client = new OAuth2S2SClient();
            OAuth2AccessTokenResponse oauth2Response;
            try
            {
                oauth2Response =
                    client.Issue(AcsMetadataParser.GetStsUrl(realm), oauth2Request) as OAuth2AccessTokenResponse;
            }
            catch (WebException wex)
            {
                using (StreamReader sr = new StreamReader(wex.Response.GetResponseStream()))
                {
                    string responseText = sr.ReadToEnd();
                    throw new WebException(wex.Message + " - " + responseText, wex);
                }
            }
 
            return oauth2Response;
        }
 
        /// <summary>
        /// Uses the specified authorization code to retrieve an access token from ACS to call the specified principal 
        /// at the specified targetHost. The targetHost must be registered for target principal.  If specified realm is 
        /// null, the "Realm" setting in web.config will be used instead.
        /// </summary>
        /// <param name="authorizationCode">Authorization code to exchange for access token</param>
        /// <param name="targetPrincipalName">Name of the target principal to retrieve an access token for</param>
        /// <param name="targetHost">Url authority of the target principal</param>
        /// <param name="targetRealm">Realm to use for the access token's nameid and audience</param>
        /// <returns>An access token with an audience of the target principal</returns>
        public static OAuth2AccessTokenResponse GetAccessToken(
            string authorizationCode,
            string targetPrincipalName,
            string targetHost,
            string targetRealm,
            Uri redirectUri)
        {
 
            if (targetRealm == null)
            {
                targetRealm = Realm;
            }
 
            string resource = GetFormattedPrincipal(targetPrincipalName, targetHost, targetRealm);
            string clientId = GetFormattedPrincipal(ClientId, null, targetRealm);
 
            // Create request for token. The RedirectUri is null here.  This will fail if redirect uri is registered
            OAuth2AccessTokenRequest oauth2Request =
                OAuth2MessageFactory.CreateAccessTokenRequestWithAuthorizationCode(
                    clientId,
                    ClientSecret,
                    authorizationCode,
                    redirectUri,
                    resource);
 
            // Get token
            OAuth2S2SClient client = new OAuth2S2SClient();
            OAuth2AccessTokenResponse oauth2Response;
            try
            {
                oauth2Response =
                    client.Issue(AcsMetadataParser.GetStsUrl(targetRealm), oauth2Request) as OAuth2AccessTokenResponse;
            }
            catch (WebException wex)
            {
                using (StreamReader sr = new StreamReader(wex.Response.GetResponseStream()))
                {
                    string responseText = sr.ReadToEnd();
                    throw new WebException(wex.Message + " - " + responseText, wex);
                }
            }
 
            return oauth2Response;
        }
 
        /// <summary>
        /// Uses the specified refresh token to retrieve an access token from ACS to call the specified principal 
        /// at the specified targetHost. The targetHost must be registered for target principal.  If specified realm is 
        /// null, the "Realm" setting in web.config will be used instead.
        /// </summary>
        /// <param name="refreshToken">Refresh token to exchange for access token</param>
        /// <param name="targetPrincipalName">Name of the target principal to retrieve an access token for</param>
        /// <param name="targetHost">Url authority of the target principal</param>
        /// <param name="targetRealm">Realm to use for the access token's nameid and audience</param>
        /// <returns>An access token with an audience of the target principal</returns>
        public OAuth2AccessTokenResponse GetAccessToken(
            string refreshToken,
            string targetPrincipalName,
            string targetHost,
            string targetRealm)
        {
 
            if (targetRealm == null)
            {
                targetRealm = Realm;
            }
 
            string resource = GetFormattedPrincipal(targetPrincipalName, targetHost, targetRealm);
            string clientId = GetFormattedPrincipal(ClientId, null, targetRealm);
 
            OAuth2AccessTokenRequest oauth2Request = OAuth2MessageFactory.CreateAccessTokenRequestWithRefreshToken(clientId, ClientSecret, refreshToken, resource);
 
            // Get token
            OAuth2S2SClient client = new OAuth2S2SClient();
            OAuth2AccessTokenResponse oauth2Response;
            try
            {
                oauth2Response =
                    client.Issue(AcsMetadataParser.GetStsUrl(targetRealm), oauth2Request) as OAuth2AccessTokenResponse;
            }
            catch (WebException wex)
            {
                using (StreamReader sr = new StreamReader(wex.Response.GetResponseStream()))
                {
                    string responseText = sr.ReadToEnd();
                    throw new WebException(wex.Message + " - " + responseText, wex);
                }
            }
 
            return oauth2Response;
        }
 
        /// <summary>
        /// Retrieves an app-only access token from ACS to call the specified principal 
        /// at the specified targetHost. The targetHost must be registered for target principal.  If specified realm is 
        /// null, the "Realm" setting in web.config will be used instead.
        /// </summary>
        /// <param name="targetPrincipalName">Name of the target principal to retrieve an access token for</param>
        /// <param name="targetHost">Url authority of the target principal</param>
        /// <param name="targetRealm">Realm to use for the access token's nameid and audience</param>
        /// <returns>An access token with an audience of the target principal</returns>
        public static OAuth2AccessTokenResponse GetAppOnlyAccessToken(
            string targetPrincipalName,
            string targetHost,
            string targetRealm)
        {
 
            if (targetRealm == null)
            {
                targetRealm = Realm;
            }
 
            string resource = GetFormattedPrincipal(targetPrincipalName, targetHost, targetRealm);
            string clientId = GetFormattedPrincipal(ClientId, HostedAppHostName, targetRealm);
 
            OAuth2AccessTokenRequest oauth2Request = OAuth2MessageFactory.CreateAccessTokenRequestWithClientCredentials(clientId, ClientSecret, resource);
            oauth2Request.Resource = resource;
 
            // Get token
            OAuth2S2SClient client = new OAuth2S2SClient();
 
            OAuth2AccessTokenResponse oauth2Response;
            try
            {
                oauth2Response =
                    client.Issue(AcsMetadataParser.GetStsUrl(targetRealm), oauth2Request) as OAuth2AccessTokenResponse;
            }
            catch (WebException wex)
            {
                using (StreamReader sr = new StreamReader(wex.Response.GetResponseStream()))
                {
                    string responseText = sr.ReadToEnd();
                    throw new WebException(wex.Message + " - " + responseText, wex);
                }
            }
 
            return oauth2Response;
        }
 
        /// <summary>
        /// Retrieves an access token from ACS using the specified authorization code, and uses that access token to 
        /// create a client context
        /// </summary>
        /// <param name="targetUrl">Url of the target SharePoint site</param>
        /// <param name="targetPrincipalName">Name of the target SharePoint principal</param>
        /// <param name="authorizationCode">Authorization code to use when retrieving the access token from ACS</param>
        /// <param name="targetRealm">Realm to use for the access token's nameid and audience</param>
        /// <returns>A ClientContext ready to call targetUrl with a valid access token</returns>
        public static ClientContext GetClientContextWithAuthorizationCode(
            string targetUrl,
            string targetPrincipalName,
            string authorizationCode,
            string targetRealm,
            Uri redirectUri)
        {
            Uri targetUri = new Uri(targetUrl);
 
            string accessToken =
                GetAccessToken(authorizationCode, targetPrincipalName, targetUri.Authority, targetRealm, redirectUri).AccessToken;
 
            return GetClientContextWithAccessToken(targetUrl, accessToken);
        }
 
        /// <summary>
        /// Uses the specified access token to create a client context
        /// </summary>
        /// <param name="targetUrl">Url of the target SharePoint site</param>
        /// <param name="accessToken">Access token to be used when calling the specified targetUrl</param>
        /// <returns>A ClientContext ready to call targetUrl with the specified access token</returns>
        public static ClientContext GetClientContextWithAccessToken(string targetUrl, string accessToken)
        {
            Uri targetUri = new Uri(targetUrl);
 
            ClientContext clientContext = new ClientContext(targetUrl);
 
            clientContext.AuthenticationMode = ClientAuthenticationMode.Anonymous;
            clientContext.FormDigestHandlingEnabled = false;
            clientContext.ExecutingWebRequest +=
                delegate(object oSender, WebRequestEventArgs webRequestEventArgs)
                {
                    webRequestEventArgs.WebRequestExecutor.RequestHeaders["Authorization"] =
                        "Bearer " + accessToken;
                };
 
            return clientContext;
        }
 
        /// <summary>
        /// Retrieves an access token from ACS using the specified context token, and uses that access token to create
        /// a client context
        /// </summary>
        /// <param name="targetUrl">Url of the target SharePoint site</param>
        /// <param name="contextTokenString">Context token received from the target SharePoint site</param>
        /// <param name="appHostUrl">Url authority of the hosted app.  If this is null, the value in the HostedAppHostName
        /// of web.config will be used instead</param>
        /// <returns>A ClientContext ready to call targetUrl with a valid access token</returns>
        public static ClientContext GetClientContextWithContextToken(
            string targetUrl,
            string contextTokenString,
            string appHostUrl)
        {
            SharePointContextToken contextToken = ReadAndValidateContextToken(contextTokenString, appHostUrl);
 
            Uri targetUri = new Uri(targetUrl);
 
            string accessToken = GetAccessToken(contextToken, targetUri.Authority).AccessToken;
 
            return GetClientContextWithAccessToken(targetUrl, accessToken);
        }
 
        /// <summary>
        /// Returns the SharePoint url to which the app should redirect the browser to request consent and get back
        /// an authorization code.
        /// </summary>
        /// <param name="contextUrl">Absolute Url of the SharePoint site</param>
        /// <param name="scope">Space-delimited permissions to request from the SharePoint site in "shorthand" format 
        /// (e.g. "Web.Read Site.Write")</param>
        /// <returns>Url of the SharePoint site's OAuth authorization page</returns>
        public static string GetAuthorizationUrl(string contextUrl, string scope)
        {
            return string.Format(
                "{0}{1}?IsDlg=1&client_id={2}&scope={3}&response_type=code",
                EnsureTrailingSlash(contextUrl),
                AuthorizationPage,
                ClientId,
                scope);
        }
 
        /// <summary>
        /// Returns the SharePoint url to which the app should redirect the browser to request consent and get back
        /// an authorization code.
        /// </summary>
        /// <param name="contextUrl">Absolute Url of the SharePoint site</param>
        /// <param name="scope">Space-delimited permissions to request from the SharePoint site in "shorthand" format
        /// (e.g. "Web.Read Site.Write")</param>
        /// <param name="redirectUri">Uri to which SharePoint should redirect the browser to after consent is 
        /// granted</param>
        /// <returns>Url of the SharePoint site's OAuth authorization page</returns>
        public static string GetAuthorizationUrl(string contextUrl, string scope, string redirectUri)
        {
            return string.Format(
                "{0}{1}?IsDlg=1&client_id={2}&scope={3}&response_type=code&redirect_uri={4}",
                EnsureTrailingSlash(contextUrl),
                AuthorizationPage,
                ClientId,
                scope,
                redirectUri);
        }
 
        /// <summary>
        /// Returns the SharePoint url to which the app should redirect the browser to request a new context token.
        /// </summary>
        /// <param name="contextUrl">Absolute Url of the SharePoint site</param>
        /// <param name="redirectUri">Uri to which SharePoint should redirect the browser to with a context token</param>
        /// <returns>Url of the SharePoint site's context token redirect page</returns>
        public static string GetAppContextTokenRequestUrl(string contextUrl, string redirectUri)
        {
            return string.Format(
                "{0}{1}?client_id={2}&redirect_uri={3}",
                EnsureTrailingSlash(contextUrl),
                RedirectPage,
                ClientId,
                redirectUri);
        }
 
        /// <summary>
        /// Retrieves an S2S access token signed by the application's private certificate on behalf of the specified 
        /// WindowsIdentity and intended for the SharePoint at the targetApplicationUri. If no Realm is specified in 
        /// web.config, an auth challenge will be issued to the targetApplicationUri to discover it.
        /// </summary>
        /// <param name="targetApplicationUri">Url of the target SharePoint site</param>
        /// <param name="identity">Windows identity of the user on whose behalf to create the access token</param>
        /// <returns>An access token with an audience of the target principal</returns>
        public static string GetS2SAccessTokenWithWindowsIdentity(
            Uri targetApplicationUri,
            WindowsIdentity identity)
        {
            string realm = string.IsNullOrEmpty(Realm) ? GetRealmFromTargetUrl(targetApplicationUri) : Realm;
 
            JsonWebTokenClaim[] claims = GetClaimsWithWindowsIdentity(identity);
 
            return GetS2SAccessTokenWithClaims(targetApplicationUri.Authority, realm, claims);
        }
 
        /// <summary>
        /// Retrieves an S2S client context with an access token signed by the application's private certificate on 
        /// behalf of the specified WindowsIdentity and intended for application at the targetApplicationUri using the 
        /// targetRealm. If no Realm is specified in web.config, an auth challenge will be issued to the 
        /// targetApplicationUri to discover it.
        /// </summary>
        /// <param name="targetApplicationUri">Url of the target SharePoint site</param>
        /// <param name="identity">Windows identity of the user on whose behalf to create the access token</param>
        /// <returns>A ClientContext using an access token with an audience of the target application</returns>
        public static ClientContext GetS2SClientContextWithWindowsIdentity(
            Uri targetApplicationUri,
            WindowsIdentity identity)
        {
            string realm = string.IsNullOrEmpty(Realm) ? GetRealmFromTargetUrl(targetApplicationUri) : Realm;
 
            JsonWebTokenClaim[] claims = GetClaimsWithWindowsIdentity(identity);
 
            string accessToken = GetS2SAccessTokenWithClaims(targetApplicationUri.Authority, realm, claims);
 
            return GetClientContextWithAccessToken(targetApplicationUri.ToString(), accessToken);
        }
 
        #endregion
 
        #region private fields
 
        //
        // Configuration Constants
        //
 
        private const string SHAREPOINT_PID = "00000003-0000-0ff1-ce00-000000000000";
 
        private const string AccessTokenParameterName = "AccessToken";
        private const string AuthorizationPage = "_layouts/15/OAuthAuthorize.aspx";
        private const string RedirectPage = "_layouts/15/AppRedirect.aspx";
        private const string AcsPrincipalName = "00000001-0000-0000-c000-000000000000";
        private const string AcsMetadataEndPointRelativeUrl = "metadata/json/1";
        private const string DelegationService = "DelegationService1.0";
        private const string AcsManagementServiceRelativeUrl = "r4/mgmt/service/";
        private const string AcsManagementServiceAppliesTo = "v2/mgmt/service/";
        private const string AcsOAuthRelativeUrl = "v2/OAuth2-13";
        private const string S2SProtocol = "OAuth2";
        private const string DelegationIssuance = "DelegationIssuance1.0";
        private const string DelegationIssuanceRequestTokenUrlSuffix = "RequestAccessToken";
        private const string NameIdentifierClaimType = JsonWebTokenConstants.ReservedClaims.NameIdentifier;
        private const string IdentityProviderClaimType = JsonWebTokenConstants.ReservedClaims.IdentityProvider;
        private const string TrustedForImpersonationClaimType = "trustedfordelegation";
        private const string ApplicationContextClaimType = JsonWebTokenConstants.ReservedClaims.AppContext;
        private const string ActorTokenClaimType = JsonWebTokenConstants.ReservedClaims.ActorToken;
        private const string SmtpClaimType = "smtp";
        private const string SipClaimType = "sip";
        private const int TokenLifetimeMinutes = 1000000;
 
        //
        // Environment Constants
        //
 
        private static string GlobalEndPointPrefix = "accounts";
        private static string AcsHostUrl = "accesscontrol.windows.net";
 
        //
        // Hosted app configuration
        //
        private static readonly string ClientId = string.IsNullOrEmpty(WebConfigurationManager.AppSettings.Get("ClientId")) ? WebConfigurationManager.AppSettings.Get("HostedAppName") : WebConfigurationManager.AppSettings.Get("ClientId");
        private static readonly string HostedAppHostName = WebConfigurationManager.AppSettings.Get("HostedAppHostName");
        private static readonly string ClientSecret = string.IsNullOrEmpty(WebConfigurationManager.AppSettings.Get("ClientSecret")) ? WebConfigurationManager.AppSettings.Get("HostedAppSigningKey") : WebConfigurationManager.AppSettings.Get("ClientSecret");
        private static readonly string Realm = WebConfigurationManager.AppSettings.Get("Realm");
        private static readonly string ServiceNamespace = WebConfigurationManager.AppSettings.Get("Realm");
 
        private static readonly string ClientSigningCertificatePath = WebConfigurationManager.AppSettings.Get("ClientSigningCertificatePath");
        private static readonly string ClientSigningCertificatePassword = WebConfigurationManager.AppSettings.Get("ClientSigningCertificatePassword");
        private static X509Certificate2 ClientCertificate = (string.IsNullOrEmpty(ClientSigningCertificatePath) || string.IsNullOrEmpty(ClientSigningCertificatePassword)) ? null : new X509Certificate2(ClientSigningCertificatePath, ClientSigningCertificatePassword);
        private static Microsoft.IdentityModel.SecurityTokenService.X509SigningCredentials SigningCredentials = (ClientCertificate == null) ? null : new Microsoft.IdentityModel.SecurityTokenService.X509SigningCredentials(ClientCertificate, SecurityAlgorithms.RsaSha256Signature, SecurityAlgorithms.Sha256Digest);
 
        #endregion
 
        #region private methods
 
        private static string GetAcsMetadataEndpointUrl()
        {
            return Path.Combine(GetAcsGlobalEndpointUrl(), AcsMetadataEndPointRelativeUrl);
        }
 
        private static string GetFormattedPrincipal(string principalName, string hostName, string realm)
        {
            if (!String.IsNullOrEmpty(hostName))
            {
                return String.Format(CultureInfo.InvariantCulture, "{0}/{1}@{2}", principalName, hostName, realm);
            }
            else
            {
                return String.Format(CultureInfo.InvariantCulture, "{0}@{1}", principalName, realm);
            }
        }
 
        private static string GetAcsPrincipalName(string realm)
        {
            return GetFormattedPrincipal(AcsPrincipalName, new Uri(GetAcsGlobalEndpointUrl()).Host, realm);
        }
 
        private static string GetAcsGlobalEndpointUrl()
        {
            return String.Format(CultureInfo.InvariantCulture, "https://{0}.{1}/", GlobalEndPointPrefix, AcsHostUrl);
        }
 
        private static JsonWebSecurityTokenHandler CreateJsonWebSecurityTokenHandler()
        {
            JsonWebSecurityTokenHandler handler = new JsonWebSecurityTokenHandler();
            handler.Configuration = new Microsoft.IdentityModel.Tokens.SecurityTokenHandlerConfiguration();
            handler.Configuration.AudienceRestriction = new Microsoft.IdentityModel.Tokens.AudienceRestriction(AudienceUriMode.Never);
            handler.Configuration.CertificateValidator = X509CertificateValidator.None;
 
            byte[] key = Convert.FromBase64String(ClientSecret);
            handler.Configuration.IssuerTokenResolver =
                SecurityTokenResolver.CreateDefaultSecurityTokenResolver(
                new ReadOnlyCollection<SecurityToken>(new List<SecurityToken>(
                    new SecurityToken[]
                            {
                                new SimpleSymmetricKeySecurityToken( key )
                            })),
                false);
            SymmetricKeyIssuerNameRegistry issuerNameRegistry = new SymmetricKeyIssuerNameRegistry();
            issuerNameRegistry.AddTrustedIssuer(key, GetAcsPrincipalName(ServiceNamespace));
            handler.Configuration.IssuerNameRegistry = issuerNameRegistry;
            return handler;
        }
 
        private static string GetRealmFromTargetUrl(Uri targetApplicationUri)
        {
            WebRequest request = HttpWebRequest.Create(targetApplicationUri.ToString() + "/_vti_bin/client.svc");
            request.Headers.Add("Authorization: Bearer ");
 
            try
            {
                using (WebResponse response = request.GetResponse())
                {
                }
            }
            catch (WebException e)
            {
                string bearerResponseHeader = e.Response.Headers["WWW-Authenticate"];
                string realm = bearerResponseHeader.Substring(bearerResponseHeader.IndexOf("Bearer realm=\"") + 14, 36);
 
                Guid realmGuid;
 
                if (Guid.TryParse(realm, out realmGuid))
                {
                    return realm;
                }
 
            }
            return null;
        }
 
        private static string GetS2SAccessTokenWithClaims(
            string targetApplicationHostName,
            string targetRealm,
            IEnumerable<JsonWebTokenClaim> claims)
        {
            return IssueToken(
                ClientId,
                targetRealm,
                SHAREPOINT_PID,
                targetRealm,
                targetApplicationHostName,
                true,
                claims,
                false);
        }
 
        private static JsonWebTokenClaim[] GetClaimsWithWindowsIdentity(WindowsIdentity identity)
        {
            JsonWebTokenClaim[] claims = new JsonWebTokenClaim[]
            {
                new JsonWebTokenClaim(TokenHelper.NameIdentifierClaimType, identity.User.Value.ToLower()),
                new JsonWebTokenClaim("nii", "urn:office:idp:activedirectory")
            };
            return claims;
        }
 
        private static string IssueToken(
            string sourceApplication,
            string sourceRealm,
            string targetApplication,
            string targetRealm,
            string targetApplicationHostName,
            bool trustedForDelegation,
            IEnumerable<JsonWebTokenClaim> claims,
            bool appOnly = false)
        {
            if (null == SigningCredentials)
            {
                throw new InvalidOperationException("SigningCredentials was not initialized");
            }
 
            #region Actor token
 
            string sourceIdentifier = string.IsNullOrEmpty(sourceRealm) ? sourceApplication : string.Format("{0}@{1}", sourceApplication, sourceRealm);
 
            string issuer = sourceIdentifier;
            string nameid = sourceIdentifier;
            string audience = string.Format("{0}/{1}@{2}", targetApplication, targetApplicationHostName, targetRealm);
 
            List<JsonWebTokenClaim> actorClaims = new List<JsonWebTokenClaim>();
            actorClaims.Add(new JsonWebTokenClaim(JsonWebTokenConstants.ReservedClaims.NameIdentifier, issuer));
            if (trustedForDelegation && !appOnly)
            {
                actorClaims.Add(new JsonWebTokenClaim(TokenHelper.TrustedForImpersonationClaimType, "true"));
            }
 
            // Create token
            JsonWebSecurityToken actorToken = new JsonWebSecurityToken(
                issuer: issuer,
                audience: audience,
                validFrom: DateTime.UtcNow,
                validTo: DateTime.UtcNow.AddMinutes(TokenLifetimeMinutes),
                signingCredentials: SigningCredentials,
                claims: actorClaims);
 
            string actorTokenString = new JsonWebSecurityTokenHandler().WriteTokenAsString(actorToken);
 
            if (appOnly)
            {
                // App-only token is the same as actor token for delegated case
                return actorTokenString;
            }
 
            #endregion Actor token
 
            #region Outer token
 
            List<JsonWebTokenClaim> outerClaims = null == claims ? new List<JsonWebTokenClaim>() : new List<JsonWebTokenClaim>(claims);
            outerClaims.Add(new JsonWebTokenClaim(ActorTokenClaimType, actorTokenString));
 
            JsonWebSecurityToken jsonToken = new JsonWebSecurityToken(
                issuer,
                audience,
                DateTime.UtcNow,
                DateTime.UtcNow.AddMinutes(10),
                outerClaims);
 
            string accessToken = new JsonWebSecurityTokenHandler().WriteTokenAsString(jsonToken);
 
            #endregion Outer token
 
            return accessToken;
        }
 
        private static string EnsureTrailingSlash(string url)
        {
            if (!String.IsNullOrEmpty(url) && url[url.Length - 1] != '/')
            {
                return url + "/";
            }
            else
            {
                return url;
            }
        }
 
        #endregion
 
        #region AcsMetadataParser
 
        // This class is used to get MetaData document from the global STS endpoint. It contains
        // methods to parse the MetaData document and get endpoints and STS certificate.
        public static class AcsMetadataParser
        {
            public static X509Certificate2 GetAcsSigningCert(string realm)
            {
                JsonMetadataDocument document = GetMetadataDocument(realm);
 
                if (null != document.keys && document.keys.Count > 0)
                {
                    JsonKey signingKey = document.keys[0];
 
                    if (null != signingKey && null != signingKey.keyValue)
                    {
                        return new X509Certificate2(Encoding.UTF8.GetBytes(signingKey.keyValue.value));
                    }
                }
 
                throw new Exception("Metadata document does not contain ACS signing certificate.");
            }
 
            public static string GetDelegationServiceUrl(string realm)
            {
                JsonMetadataDocument document = GetMetadataDocument(realm);
 
                JsonEndpoint delegationEndpoint = document.endpoints.SingleOrDefault(e => e.protocol == DelegationIssuance);
 
                if (null != delegationEndpoint)
                {
                    return delegationEndpoint.location;
                }
                else
                {
                    throw new Exception("Metadata document does not contain Delegation Service endpoint Url");
                }
            }
 
            private static JsonMetadataDocument GetMetadataDocument(string realm)
            {
                string acsMetadataEndpointUrlWithRealm = String.Format(CultureInfo.InvariantCulture, "{0}?realm={1}",
                                                                        GetAcsMetadataEndpointUrl(),
                                                                        realm);
                byte[] acsMetadata;
                using (WebClient webClient = new WebClient())
                {
 
                    acsMetadata = webClient.DownloadData(acsMetadataEndpointUrlWithRealm);
                }
                string jsonResponseString = Encoding.UTF8.GetString(acsMetadata);
 
                JavaScriptSerializer serializer = new JavaScriptSerializer();
                JsonMetadataDocument document = serializer.Deserialize<JsonMetadataDocument>(jsonResponseString);
 
                if (null == document)
                {
                    throw new Exception("No metadata document found at the global endpoint " + acsMetadataEndpointUrlWithRealm);
                }
 
                return document;
            }
 
            public static string GetStsUrl(string realm)
            {
                JsonMetadataDocument document = GetMetadataDocument(realm);
 
                JsonEndpoint s2sEndpoint = document.endpoints.SingleOrDefault(e => e.protocol == S2SProtocol);
 
                if (null != s2sEndpoint)
                {
                    return s2sEndpoint.location;
                }
                else
                {
                    throw new Exception("Metadata document does not contain STS endpoint url");
                }
            }
 
            private class JsonMetadataDocument
            {
                public string serviceName { get; set; }
                public List<JsonEndpoint> endpoints { get; set; }
                public List<JsonKey> keys { get; set; }
            }
 
            private class JsonEndpoint
            {
                public string location { get; set; }
                public string protocol { get; set; }
                public string usage { get; set; }
            }
 
            private class JsonKeyValue
            {
                public string type { get; set; }
                public string value { get; set; }
            }
 
            private class JsonKey
            {
                public string usage { get; set; }
                public JsonKeyValue keyValue { get; set; }
            }
        }
 
        #endregion
    }
 
    /// <summary>
    /// A JsonWebSecurityToken generated by SharePoint to authenticate to a 3rd party application and allow callbacks using a refresh token
    /// </summary>
    public class SharePointContextToken : JsonWebSecurityToken
    {
        public static SharePointContextToken Create(JsonWebSecurityToken contextToken)
        {
            return new SharePointContextToken(contextToken.Issuer, contextToken.Audience, contextToken.ValidFrom, contextToken.ValidTo, contextToken.Claims);
        }
 
        public SharePointContextToken(string issuer, string audience, DateTime validFrom, DateTime validTo, IEnumerable<JsonWebTokenClaim> claims)
            : base(issuer, audience, validFrom, validTo, claims)
        {
        }
 
        public SharePointContextToken(string issuer, string audience, DateTime validFrom, DateTime validTo, IEnumerable<JsonWebTokenClaim> claims, SecurityToken issuerToken, JsonWebSecurityToken actorToken)
            : base(issuer, audience, validFrom, validTo, claims, issuerToken, actorToken)
        {
        }
 
        public SharePointContextToken(string issuer, string audience, DateTime validFrom, DateTime validTo, IEnumerable<JsonWebTokenClaim> claims, SigningCredentials signingCredentials)
            : base(issuer, audience, validFrom, validTo, claims, signingCredentials)
        {
        }
 
        public string NameId
        {
            get
            {
                return GetClaimValue(this, "nameid");
            }
        }
 
        /// <summary>
        /// The principal name portion of the context token's "appctxsender" claim
        /// </summary>
        public string TargetPrincipalName
        {
            get
            {
                string appctxsender = GetClaimValue(this, "appctxsender");
 
                if (appctxsender == null)
                {
                    return null;
                }
 
                return appctxsender.Split('@')[0];
            }
        }
 
        /// <summary>
        /// The context token's "refreshtoken" claim
        /// </summary>
        public string RefreshToken
        {
            get
            {
                return GetClaimValue(this, "refreshtoken");
            }
        }
 
        /// <summary>
        /// The context token's "CacheKey" claim
        /// </summary>
        public string CacheKey
        {
            get
            {
                string appctx = GetClaimValue(this, "appctx");
                if (appctx == null)
                {
                    return null;
                }
 
                ClientContext ctx = new ClientContext("http://tempuri.org");
                Dictionary<string, object> dict = (Dictionary<string, object>)ctx.ParseObjectFromJsonString(appctx);
                string cacheKey = (string)dict["CacheKey"];
 
                return cacheKey;
            }
        }
 
        /// <summary>
        /// The context token's "SecurityTokenServiceUri" claim
        /// </summary>
        public string SecurityTokenServiceUri
        {
            get
            {
                string appctx = GetClaimValue(this, "appctx");
                if (appctx == null)
                {
                    return null;
                }
 
                ClientContext ctx = new ClientContext("http://tempuri.org");
                Dictionary<string, object> dict = (Dictionary<string, object>)ctx.ParseObjectFromJsonString(appctx);
                string cacheKey = (string)dict["SecurityTokenServiceUri"];
 
                return cacheKey;
            }
        }
 
        /// <summary>
        /// The realm portion of the context token's "audience" claim
        /// </summary>
        public string Realm
        {
            get
            {
                string aud = this.Audience;
                if (aud == null)
                {
                    return null;
                }
 
                string tokenRealm = aud.Substring(aud.IndexOf('@') + 1);
 
                return tokenRealm;
            }
        }
 
        private static string GetClaimValue(JsonWebSecurityToken token, string claimType)
        {
            if (token == null)
            {
                throw new ArgumentNullException("token");
            }
 
            foreach (JsonWebTokenClaim claim in token.Claims)
            {
                if (StringComparer.Ordinal.Equals(claim.ClaimType, claimType))
                {
                    return claim.Value;
                }
            }
 
            return null;
        }
 
    }
 
    public class OAuthTokenPair
    {
        public string AccessToken;
        public string RefreshToken;
    }
}

Posted in Sharepoint, SharePoint 2013 | Leave a Comment »

Error “Multiple management objects were found for identity” when enabling users in Lync

Posted by trikks on December 17, 2012

Having an error similar to this Enable-CsUser : Multiple management objects were found for identity “Håkan Karlsson”.?

In my case this occurred when having multiple AD’s that didn’t sync as they should and when moving a user around in my OU’s it got copied at one point instead of moved. In an AD with thousands of users this may become a bit problematic.

Solution 1 – find the duplicate user and remove it

Open PowerShell as admin and (this should be done at the AD server)

PS C:\> Import-Module ActiveDirectory
PS C:\> Get-ADUser -filter {(GivenName -eq "Håkan") -and (Surname -eq "Karlsson")}

This gave me

DistinguishedName : CN=Håkan Karlsson,OU=UnitName,OU=Customers,DC=domain,DC=se
Enabled           : True
GivenName         : Hakan
Name              : Håkan Karlsson
ObjectClass       : user
ObjectGUID        : 047cf709-0a3f-42f5-a459-b6885c99aace
SamAccountName    : hakan
SID               : S-1-5-21-54898309-311788698-4246315985-1142
Surname           : Karlsson
UserPrincipalName : hakan@domain.se
 
DistinguishedName : CN=Håkan Karlsson,OU=Sales,OtherUnit,OU=Customers,DC=domain,DC=se
Enabled           : True
GivenName         : Håkan
Name              : Håkan Karlsson
ObjectClass       : user
ObjectGUID        : 0bae8230-eb34-4119-bc97-bfde80a3dfe4
SamAccountName    : User13
SID               : S-1-5-21-54898309-311788698-4246315985-1265
Surname           : Karlsson
UserPrincipalName : User13@domain.se

There, it’s very easy to track and remove any of these users if I’d like to.

Solution 2 – Keep both users and add by DistinguishedName instead

Even though it’s rarely mentioned you can add users to Lync by their DistinguishedName.

Any given day you would type something like

Enable-CsUser -Identity "Håkan Karlsson" -RegistrarPool "lync.domain.se" -SipAddressType SamAccountName -SipDomain domain.se

Now, that was the command that didn’t work since you have two users by that name. Instead do this

Enable-CsUser –Identity "CN=Håkan Karlsson,OU=Sales,OtherUnit,OU=Customers,DC=domain,DC=se" 
-RegistrarPool "lync.domain.se" -SipAddressType SamAccountName -SipDomain domain.se

That’s it.

Lessons learned from this? Always use the DistinguishedName when adding users to lync!

Posted in Lync, PowerShell, Uncategorized | Tagged: , , | Leave a Comment »

Updating Lync Media Configuration using PowerShell

Posted by trikks on December 16, 2012

This will describe how to work with the CsMediaConfiguration cmdlets.

This particular guide assumes you have a rather simple setup, for more complex setups you may need to tweak my suggestions a bit.

Let’s have a look at your current config

Open PowerShell and run the Get-CsMediaConfiguration command, a default setup of lync should look like this

PS c:\> Get-CsMediaConfiguration
 
Identity            : Global
EnableQoS           : False
EncryptionLevel     : RequireEncryption
EnableSiren         : False
MaxVideoRateAllowed : VGA600K

The CsMediaConfiguration utilizes the Microsoft.Rtc.Management.WritableConfig.Settings.Media.MediaSettings object as I describe here http://trikks.wordpress.com/2012/12/16/the-undocumented-mediasettings-class-from-microsoft-rtc/

Allowing HD video

Note the -Identity property

PS C:\> Set-CsMediaConfiguration -Identity site:Global -MaxVideoRateAllowed hd720p15m

Using PowerShell a bit

In this example I store the current configuration object in a $config variable and uses the identity in the object. The settings are also changed to allow HD and to SupportEncryption rather than to require it.

 
PS C:\> $config = Get-CsMediaConfiguration
PS C:\> Set-CsMediaConfiguration -Identity $config.Identity -MaxVideoRateAllowed hd720p15m -EncryptionLevel SupportEncryption
 
PS C:\> Get-CsMediaConfiguration
 
Identity            : Global
EnableQoS           : False
EncryptionLevel     : SupportEncryption
EnableSiren         : False
MaxVideoRateAllowed : Hd720p15M

Posted in Lync, PowerShell | Tagged: , | Leave a Comment »

 
Follow

Get every new post delivered to your Inbox.