Create Yammer group as another user

We have a use-case in which we’re provisioning Office365 site collections, on behalf of a user request (via a SQL database – don’t worry about that bit, it’s kinda inconsequential).

We’re using some Azure C# website/webjob magic to put it all together.

BUT – the problem we were facing was this :

  • Cannot create a Yammer group ‘as another user’
  • Cannot add a user TO a group
  • Cannot add an admin to a group

We could create a group as an “farm-admin” user, but when creating a private group, the requesting user couldn’t access it – and needed the god-mode user to grant access !!

So much for a self-service site creation + Yammer – which was our goal.

Long post ahead, but the short story is that you need to ‘impersonate’ another user, when you attempt to create a new Yammer group  :

  • Call a Yammer REST endpoint to get all users
  • Loop through the users to find the requesting user (eg. Jodie)
  • Call a Yammer REST endpoint to get the user by email address – rather than get ALL users
  • Call a Yammer REST endpoint to get the security token for that user
  • Call a Yammer REST endpoint to create a group – passing the user’s access token

This does EXACTLY what we need it to do – and creates a group ‘as the user’ – and as a by-product, the user is made an admin – perfect !

A fair chunk of this approach was sourced from a blog post from Paul Matthews (cannonfodder) who helped me on the right track…

And also Steve Peschka (samlman)’s blogpost about Using Impersonation with the Yammer APIs.

Here’s the details of what *I* implemented – and the outcome achieved…….

Register a Yammer App – get client Id/Secret/Token

Before you begin, you need to get a client id / secret – much like a SharePoint app.

Refer to my previous post which covers this – and describes the group creation – which was our initial approach.

NB.  I have a helper method to GET/POST to Yammer :

private static string CallYammerJson(string url, string accessToken, string method)
{
    //make the request
    string json = null;
    HttpWebRequest request = WebRequest.Create(url) as HttpWebRequest;
    request.Method = method;
    request.Headers.Add('Authorization', 'Bearer' + ' ' + accessToken);
    using (HttpWebResponse response = request.GetResponse() as HttpWebResponse)
    {
        Encoding encode = Encoding.GetEncoding('utf-8');
        StreamReader reader = new StreamReader(response.GetResponseStream(), encode);
        json = reader.ReadToEnd();
    }
    return json;
}

This method can be used when you craft a URL – and just pass a token, and a GET or POST as the method.

Get Yammer Users

This code then retrieves a set of the users within Yammer – as a JSON structure.   I’m using a YammerUser class – you can grab this from the PnP example.

string yammerUserAccountEmail = 'i:0#.f|membership|jodie@whatever.com.au';
int yamUserId = 0;

//get all users - and find the yammer user id
string url = 'https://www.yammer.com/api/v1/users.json';
var response = CallYammerJson(url, YAMMER_API_TOKEN, 'GET');
List[YammerUser] users = JsonUtility.Deserialize[List[YammerUser]](response);

foreach (YammerUser u in users)
{
    string yamEmail = u.contact.email_addresses[0].address.ToLower();

    if (yammerUserAccountEmail.ToLower().Contains(yamEmail) )
    {
        yamUserId = u.id;
        break;
    }
}

UPDATE > Change to [users/by_email.json]

We’ve since discovered that you can get the user details ‘by email address’ – rather than needing to get *ALL* users, which could be many thousands…    (a big concern)

Instead, we can call the REST endpoint with an email address parameter.  This gets us the ONE user, in the same JSON chunk.    This is much MUCH more elegant.

//yammerUserAccountEmail  >>  i:0#.f|membership|999999@domain.com.au
string[] emailArray = yammerUserAccountEmail.Split('|');

//0 > i:0#.f
//1 > membership
//2 > 999999@domain.com.au
string emailO365 = emailArray[2];

const string YAMMER_USERS_BY_EMAIL_URL = 'https://www.yammer.com/api/v1/users/by_email.json?email={0}';
string url = string.Format(YAMMER_USERS_BY_EMAIL_URL, emailO365);

//get the user - and find the yammer user id
var response = GetYammerJson(url, YAMMER_API_TOKEN);
List[YammerUser] users = JsonUtility.Deserialize[List[YammerUser]](response);

//found the users collection - will only have ONE user 
if (users.Count > 0)
{
    //get the token and return it
    YammerUser u = users[0];
    yamUserId = u.id;
}

Get user auth token

After you have an Yammer “ID” of the person (eg. Jodie) – you can then call another REST endpoint, to determine their access token.

You need to include the CLIENT ID in the url – from when you registered your App – as well as the ID of the user.   The token is the same ‘admin’ token (at this stage).

//get the user token
string userToken = string.Empty;
string tokenUrl = 'https://www.yammer.com/api/v1/oauth/tokens.json?consumer_key={0}&user_id={1}';
string url = string.Format(tokenUrl, YAMMER_CLIENT_ID, yammerUserId.ToString());

var response = GetYammerJson(url, YAMMER_API_TOKEN);
List[YammerUser] users = JsonUtility.Deserialize[List[YammerUser]](response);

if (users.Count != 0)
{
    YammerUser u = users[0];
    userToken = u.token;
}

Create group, impersonating another user

bool privateYammerGroup = true;    // or false - if you want a public group
string privateYammerGroup = 'Hunting and Fishing';

string url = 'https://www.yammer.com/api/v1/groups.json?name={0}&private={1}';
string urlCreateGroup = String.Format(url, yammerGroupName, privateYammerGroup.ToString().ToLower());

string jsonCreateGroup = CallYammerJson(urlCreateGroup, userToken, 'POST');

That’s about it !

It’s not actually TOO bad – it took us a lot longer to determine the approach, than to actual do it.

We’d initially tried to create a group, and then assign an admin user – after watching the HTTP traffic via Fiddler, and some emails with the product team, it was clear that we were barking up the wrong tree.

The above solution works nicely – we’re now rolling this into a production tenant with Azure/O365.

Users can simply enter the name of a ‘site’ and get a new site collection, and yammer group also – nice !

🙂

UPDATE : Missing Code :

Sorry – had skipped this function – I used it for GET and POST – just by passing the word as ‘method’.

Eg.  PostYammerJson(“http://url”, “lskdjshdfkjhs”, “GET”)

==================

private static string PostYammerJson(string url, string accessToken, string method)
{
//make the request
string json = null;
HttpWebRequest request = WebRequest.Create(url) as HttpWebRequest;
request.Method = method;
request.Headers.Add(“Authorization”, “Bearer” + ” ” + accessToken);

try
{
using (HttpWebResponse response = request.GetResponse() as HttpWebResponse)
{
Encoding encode = Encoding.GetEncoding(“utf-8”);
StreamReader reader = new StreamReader(response.GetResponseStream(), encode);
json = reader.ReadToEnd();
}
}
catch(Exception ex)
{
json = ex.Message;
}

return json;
}

Advertisements

Convert IP Address to numeric (JS)

I’ve been using some fancy functions in SQL to convert to/from an IP Address and Mask – but have needed to do the same within JavaScript.

This is a requirement based on some MySQL functions – eg. INET_NTOA and INET_ATON.

This first one will take an input value – eg. 255.240.0.0 – and return a number (BIGINT / INT64 for calculations – or storing in a database – as we’re needing to do.

    function convertIpToNumeric(ipAddress) {
        var arrIp = ipAddress.split(".");

        var segment1 = parseInt(arrIp[0]);
        var segment2 = parseInt(arrIp[1]);
        var segment3 = parseInt(arrIp[2]);
        var segment4 = parseInt(arrIp[3]);

        //reverse order calc 
        //eg. 255.255.240.0     <-- start at the end
        // 0 + (240*256) + (255*65536) + (255*16777216)
        var calc = segment4 + (segment3 * 256) + (segment2 * 65536) + (segment1 * 16777216);

        return calc;

    }

And – if needing to do the opposite – you can use the following function.

This will take an input value – eg. 167772161 – and return the resultant IP Address :

function convertNumericToIp(bigNumber) {

    var Octet1 = Math.floor(bigNumber / 16777216)
    var RestOfIP = bigNumber - (Octet1 * 16777216)

    var Octet2 = Math.floor(RestOfIP / 65536)
    var RestOfIP = RestOfIP - (Octet2 * 65536)

    var Octet3 = Math.floor(RestOfIP / 256)
        
    var Octet4 = RestOfIP - (Octet3 * 256)

    var returnValue = Octet1 + "." + Octet2 + "." + Octet3 + "." + Octet4; 

    return returnValue;

}

Turn OFF flag for HIDE PHYSICAL URL’s

We have a set of publishing pages that are not correctly showing if/when the following flag is set to ON :

image

This is to do with the SEO and vanity URL’s stuff – but we want this “OFF”.

Looking at the field via the REST URL, you can see the default value.    But, the field/column is actually a SEALED column, and so you can’t update it via the UI.

https://tenant.sharepoint.com/sites/MyFancySiteColl/_api/Web/AvailableFields(guid’50631c24-1371-4ecf-a5ae-ed41b03f4499′)

image 

So – it’s some fancy PowerShell to force this value (after connecting to O365) :

#get the fields at the root site level
$rootWeb = $clientContext.Web
$fields = $rootWeb.Fields
$clientContext.Load($fields)
$clientContext.ExecuteQuery()

#grab the field 
$fld = $fields.GetByInternalNameOrTitle("PublishingIsFurlPage")
$clientContext.Load($fld)
$clientContext.ExecuteQuery()

$fld

$fld.DefaultValue = $false
$fld.Update()
$clientContext.ExecuteQuery()

$rootWeb.Update()
$clientContext.ExecuteQuery()

After running this PowerShell, the field is SET when viewing the column via REST :

image

And now, when users create a page – regardless of PAGE LAYOUT – this value will be ticked to OFF by default – and standard URL’s will be used – too easy.

The main issue we had was with a Content Search WebPart that was NOT showing the pages that had the checkbox ON.   We needed users to make sure to set it off – making it DEFAULT to off was the best option.

🙂

SQL Server INET_ATON IP Address + CIDR

Most IT folk would understand that an IP Address is a 4-octset set – eg. 192.168.53.123

There’s another piece that is the subnet mask – eg.  255.255.252.0

These are usually managed as numerical values – BIGINT values (Int64).

Within MySQL, you can determine the IP Address to/from a number using the functions INET_ATON and INET_NTOA.

For an equivalent in SQL Server – you can use the following UDF’s :

Integer to IP Address :

CREATE FUNCTION [dbo].[IntegerToIPAddress] (@IP AS bigint)
RETURNS varchar(15)
AS
BEGIN
 DECLARE @Octet1 bigint
 DECLARE @Octet2 tinyint
 DECLARE @Octet3 tinyint
 DECLARE @Octet4 tinyint
 DECLARE @RestOfIP bigint
 
 SET @Octet1 = @IP / 16777216
 SET @RestOfIP = @IP - (@Octet1 * 16777216)
 SET @Octet2 = @RestOfIP / 65536
 SET @RestOfIP = @RestOfIP - (@Octet2 * 65536)
 SET @Octet3 = @RestOfIP / 256
 SET @Octet4 = @RestOfIP - (@Octet3 * 256)
 
 RETURN(CONVERT(varchar, @Octet1) + '.' +
        CONVERT(varchar, @Octet2) + '.' +
        CONVERT(varchar, @Octet3) + '.' +
        CONVERT(varchar, @Octet4))
END

IP Address to Integer

CREATE FUNCTION [dbo].[IPAddressToInteger] (@IP AS varchar(15))
RETURNS bigint
AS
BEGIN
 RETURN (CONVERT(bigint, PARSENAME(@IP,1)) +
         CONVERT(bigint, PARSENAME(@IP,2)) * 256 +
         CONVERT(bigint, PARSENAME(@IP,3)) * 65536 +
         CONVERT(bigint, PARSENAME(@IP,4)) * 16777216)
 
END

Yet another piece of the puzzle is related to a CIDR – which looks like this :  192.30.250.00/18

It’s an acronym for “Classless Inter-Domain Routing” – I had to look it up too…!

I’ve created another function that determines the “/18” for a given numeric sequence (in T-SQL).

CREATE FUNCTION [dbo].[CIDRFromMask] (@mask AS BIGINT)
RETURNS VARCHAR(5)
AS
BEGIN

	 DECLARE @maskCalc BIGINT
	 SELECT @maskCalc = dbo.IPAddressToInteger('255.255.255.255') - @mask + 1

	 DECLARE @logCalc int
	 SELECT @logCalc = (32 - LOG(@maskCalc, 2))

	 RETURN '/' + CAST(@logCalc AS VARCHAR(5))

END

This helped me with the retrieval of an IP Address – and CIDR – fairly complex, but easy to use in the end – eg.  255.255.252.0/22

select dbo.IntegerToIPAddress('4294966272') + dbo.CIDRFromMask('4294966272')

image

A Web Part with this ID has already been added to this page.

Within a SharePoint site that we’ve upgraded, we have an error showing when users navigate to a page :

image

Using some PowerShell, I was able to list the specific webparts on the page :

$web = Get-SPWeb http://intranet/Services/Finance 
$list = $web.Lists["Pages"] 
  
#get the page (listitem) 
$item = $list.Items[0] 
 
#get the webpart manager 
$wpm = $item.File.GetLimitedWebPartManager('Shared') 
 
$wpm.WebParts | select id, title 

And somehow, there is a duplicate GUID for a certain WebPart :

image

The next step is to remove one of those webparts from the page – following on from the PowerShell shown above :

#get the webpart using #3
$wp = $wpm.WebParts[3] 

#check which one we've got
$wp | select id, title
 
#delete from the page - and update 
$wpm.DeleteWebPart($wp) 
$item.Update()

 

That outta do it – and your users will be able to browse to the page.

NB.  You might have to checkout the page also – and then verify which webpart you need to remove – be careful !

Quick connect to O365 using PowerShell

I’ve been doing lots of posts about connecting to Office365, and a bunch of scripts, so I thought I’d share my easy script – just gotta update the URL, and PASSWORD.

** NB.  You need a folder with the DLL’s – this is my set of files :

image

This is the basic script – to add a reference to the necessary DLL’s (above) – and then define the user/password – and make a connection – and then load the WEB object.

$path =  'C:\o365';

$urlSite = 'https://[tenant].sharepoint.com/sites/[SITECOLLECTION]'

$user = '[USER]@[TENANT].onmicrosoft.com';
$password = 'Password1234';

[Reflection.Assembly]::LoadFile('$path\Libraries\Microsoft.SharePoint.Client.dll')
[Reflection.Assembly]::LoadFile('$path\Libraries\Microsoft.SharePoint.Client.Runtime.dll')

add-type -Path $path'\Libraries\Microsoft.SharePoint.Client.dll'
add-type -Path $path'\Libraries\Microsoft.SharePoint.Client.Runtime.dll'

$passwordSecureString = ConvertTo-SecureString -string $password -AsPlainText -Force
$credential = New-Object -TypeName System.Management.Automation.PSCredential -argumentlist $user, $passwordSecureString

$spoCtx = New-Object Microsoft.SharePoint.Client.ClientContext($urlSite)
$spoCredentials = New-Object -TypeName Microsoft.SharePoint.Client.SharePointOnlineCredentials -argumentlist $user, $passwordSecureString
$spoCtx.Credentials = $spoCredentials
$spoCtx.RequestTimeout = '500000'

$web = $spoCtx.web
$spoCtx.Load($web)
$spoCtx.ExecuteQuery();

Please let me know if that’s helpful – feel free to copy+use – I’ve used this a LOT in quick testing scripts…

🙂

SetDefaultPageLayout CSOM

As part of my new Office 365 site collection provisioning (via PowerShell), I’d like to be able to set the ‘default page layout’ :

image

BUT – within the object model for SharePoint.Client.Publishing, there are only a handful of methods for the PublishingWebClass – and so I can’t use CSOM or POWERSHELL to set the default page layout.

Click here to see on MSDN :

image

But – after a quick poke into the DLL for SharePoint.Publishing.DLL (using ILSPY), I could see that the functionality is just to add a property – which is EASY via CSOM…>!

image

image

And – sure enough, if you look using the REST endpoint, there it is :

https://tenant.sharepoint.com/sites/D001N/_api/web/AllProperties

image

It looks like the value that I need – for the Page Layout of “ArticleLeft” is :

<layout guid=”8520f570-356b-462d-8976-1e58b936c65e” url=”_catalogs/masterpage/ArticleLeft.aspx” />

Unfortunately – that GUID is different, as you move to another site collection – even for the same page layout – doh !

Another look at ILSPY shows that the GUID is the ID of the listitem :

image

So – the PowerShell needs to determine the specific item for the page layout I want – and then construct the little XML chunk – and update the property for __DefaultPageLayout.

There’s a REST call that shows these guys :

https://tenant.sharepoint.com/sites/D001N/_api/Web/GetFolderByServerRelativeUrl(‘/sites/D001N/_catalogs/masterpage’)/Files

And – you can actually grab the specific file :

https://tenant.sharepoint.com/sites/D001N/_api/Web/GetFileByServerRelativeUrl(‘/sites/D001N/_catalogs/masterpage/ArticleLeft.aspx’)?$select=UniqueId

image

But – from a POWERSHELL command line, this is what you need – once you’ve established a context to the specific site collection :

$rootWeb = $spoCtx.web
$spoCtx.Load($rootWeb)
$spoCtx.ExecuteQuery();

$spoCtx.Load($rootWeb.AllProperties)
$spoCtx.ExecuteQuery(); 

$defaultPageLayout = '_catalogs/masterpage/ArticleLeft.aspx'

#get the page item - and grab the GUID of it
$urlRelative = $rootWeb.ServerRelativeUrl + "/" + $defaultPageLayout
$pageLayoutFile = $rootWeb.GetFileByServerRelativeUrl($urlRelative) 
$spoCtx.Load($pageLayoutFile)
$spoCtx.ExecuteQuery();

$pageGuid = $pageLayoutFile.UniqueId;

#set the xmlchunk for the property
$xmlPageLayout = "<layout guid='{0}' url='{1}' />"
$xmlPageLayout = $xmlPageLayout.Replace("{0}", $pageGuid);
$xmlPageLayout = $xmlPageLayout.Replace("{1}", $defaultPageLayout);

$rootWeb.AllProperties["__DefaultPageLayout"] = $xmlPageLayout;
$rootWeb.Update()
$spoCtx.ExecuteQuery(); 

That works nicely – phew !     🙂