SharePoint

o365 Groups, My Hatred of it

I hate PowerShell, I hate o365 (more specifically o365 Groups / Outlook Groups), and I hate SharePoint in the Office 365 Tenant. 

I know that is a strong way to start off a statement on managing o365 Groups but after banging my head against the wall for too long around o365 Groups I can’t think of a more appropriate statement regarding my feelings for Microsoft’s new product line.  So let me break down my frustration and give a few insights.

I am going to start with my hatred of SharePoint.  First off, the platform is a pig and it is a bloated pig, but I am not going to get into that right now.  With Microsoft’s new direction for SharePoint in building other products on top of it such as o365 Groups or Planner, Microsoft is stepping in the world of SharePoint and making a muck of the SharePoint environment with other products that trample over the storage and URL-space of the SharePoint tenant.  So when you create an o365 Group or a Planner plan, then you have created a SharePoint site collection in the tenant that you don’t see in the dashboard or have direct control over the quota (without PowerShell). 

Moving into my hatred of o365 Groups.  Here is a product that Microsoft is building as a collaborative communication tool (what about Yammer) and they are encouraging users to create their own groups, but they have little governance on how the groups are created (and that is just coming in now).  One exciting thing about groups is that the users can create whatever they want… and guess what, it shows up in the GAL.  A user could effectively create a group saying “Union Formation Group” and what could the company do to stop it, they could create a lot of legal problems in the company instantly and there is no way to really stop it short of turning off Groups.  Keep in mind that these Groups use quota from SharePoint too.  Here is just another though on Groups, the name.  We have AD Groups, Azure Groups, o365 Groups, just group Groups, and where will this end.  Let’s stop using generic names for products (Word?) which causes so much confusion when using the Google to try to return information (“the Google” was not a typo, it is a being now).

Lastly, I am going to complain about PowerShell or more specifically the implementation of PowerShell and the documentation for the o365 Groups.  Microsoft’s documentation is scattered and not clear on top of the fact that when you search for “Office 365 Group” or “o365 Group” you get everything from groups about o365 to permission groups in SharePoint (back to naming).  Even when you find the documentation that shows things like optional parameters for the PowerShell it is very incomplete.  For example, many of the options that are available on “Set-UnifiedGroup” (did you notice that one?  All of a sudden an o365 Group is called a “Unified Group”) are available too on “New-UnifiedGroup” even though it doesn’t show up there in the documentation.  Here is another fun one, if you want to set the quota for the file storage, it is a SharePoint PowerShell command and does not involve the “Unified Group.”  My favorite of all, I was trying to interact with the “Description” field for the o365 Group but that is the “Notes” field in PowerShell, what the hell?  Why do you even use named properties when you change the meaning of the names or the names in the UI. 

Well, there was my rant.  I doubt there will be anything helpful, but if you want the complete current list of PowerShell parameters I have it below.  Please look back in a few weeks for some examples on connecting to PowerShell through C# to create a new Unified Group or whatever the hell Microsoft changes the name to next.

 

  • New-UnifiedGroup
    • [-AccessType <Public | Private>]
    • [-Alias <String>]
    • [-AutoSubscribeNewMembers <SwitchParameter>]
    • [-Confirm [<SwitchParameter>]]
    • [-Database <DatabaseIdParameter>]
    • [-DisplayName <String>]
    • [-DomainController <Fqdn>]
    • [-EmailAddresses <ProxyAddressCollection>]
    • [-ExecutingUser <RecipientIdParameter>]
    • [-ExternalDirectoryObjectId <String>]
    • [-FromSyncClient <SwitchParameter>]
    • [-Language <CultureInfo>]
    • [-ManagedBy <RecipientIdParameter[]>]
    • [-Members <RecipientIdParameter[]>]
    • [-Name <String>]
    • [-Notes <String>]
    • [-Organization <OrganizationIdParameter>]
    • [-OrganizationalUnit <OrganizationalUnitIdParameter>]
    • [-OverrideRecipientQuotas <SwitchParameter>]
    • [-PrimarySmtpAddress <SmtpAddress>]
    • [-RecipientIdType <Unknown | ExternalId | Smtp | LegacyDn>]
    • [-RequireSenderAuthenticationEnabled <$true | $false>]
    • [-SharePointResources <MultiValuedProperty>]
    • [-SuppressWarmupMessage <SwitchParameter>]
    • [-ValidationOrganization <String>]
    • [-WhatIf [<SwitchParameter>]]
    • [<CommonParameters>]

SharePoint Login as a Different User

What the hell Microsoft? Why did you decide to remove the ability for a user to logon as a different user in SharePoint 2013? It was there in 2010, but this feature is not there in 2013.

Microsoft's recommendation is to right-click on Internet Explorer and then select run-as and then enter your username and password. This is not a good option for most users and in our environment it is not practical. So to get this functionality back, we will create a SharePoint feature that is deployed at the farm level. There are some sites out there that detail this but there is one problem with all of them, they do not redirect the user back to the same site where they did the logout / login function. I have included the specific code to solve the problem but I am not including the details on how to create the feature. Once the feature is created add an empty element to the project and paste in the code below. The LogoffAndLogin() javascript function will get the current subsite and pass it in as a parameter to then allow the logoff webservice to redirect the user back to the original page after login.

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <Elements xmlns="http://schemas.microsoft.com/sharepoint/">
  3. <CustomAction
  4. Id="LoginScript"
  5. ScriptBlock="function LogoffAndLogin(){ if (typeof SP != 'undefined') { var siteCollUrl = '';SP.SOD.executeFunc('SP.js', 'SP.ClientContext', function(){var clientContext = new SP.ClientContext.get_current();var site = clientContext.get_site();clientContext.load(site);clientContext.executeQueryAsync(Function.createDelegate(this, function(){siteCollUrl = site.get_url();var fullURL = siteCollUrl + '/_layouts/closeConnection.aspx?loginasanotheruser=true&amp;amp;Source=' + siteCollUrl; window.location = fullURL;}))})} else { alert('An error occurred during the logoff process, please try again.');}};"
  6. Location="ScriptLink">
  7. </CustomAction>
  8. <CustomAction
  9. Id="LogInAsUser"
  10. GroupId="PersonalActions"
  11. Location="Microsoft.SharePoint.StandardMenu"
  12. Sequence="998"
  13. Title="Sign in as a Different User"
  14. Description="Sign Out and Login as a Different User">
  15. <UrlAction Url="javascript:LogoffAndLogin();"/>
  16. </CustomAction>
  17. </Elements>

I have the code all in one line and I realize that it might be a bit difficult to follow. I have included the code broken out as multiple lines to help you follow what is going on.

  1. LogoffAndLogin(){
  2. if (typeof SP != 'undefined') {
  3. var siteCollUrl = '';
  4. SP.SOD.executeFunc('SP.js', 'SP.ClientContext', function(){
  5. var clientContext = new SP.ClientContext.get_current();
  6. var site = clientContext.get_site();
  7. clientContext.load(site);
  8. clientContext.executeQueryAsync(Function.createDelegate(this, function(){
  9. siteCollUrl = site.get_url();
  10. var fullURL = siteCollUrl + '/_layouts/closeConnection.aspx?loginasanotheruser=true&amp;amp;Source=' + siteCollUrl;
  11. window.location = fullURL;
  12. }))
  13. })
  14. }
  15. else {
  16. alert('An error occurred during the logoff process, please try again.');
  17. }
  18. };

This has worked well in our environment. This is a stupid problem that MS created, but this solution should work well to solve it. Good luck !!



SharePoint security trimmed site list

Okay, so it has been a bit since I have posted anything, mainly due to that I have not anything notable to post.  This is not due to me not doing anything notable but that the things that I have done that were notable were proprietary and I did not feel comfortable disclosing in a public forum. 

So the problem that I was given was that when people landed on the root site within the root site collection of our managed path / application the user hit a page that was essentially blank.  From there they did not know where they should go and did not really know what they would be able to access.  So here comes a webpart that can help solve the problem.  What the goal of this webpart was is to create a security trimmed list of sites the user would have access at the root level.  I am not going to go through each and every step for creating a webpart but I will give some high level info since these are steps that I struggled with when creating the webpart.

 

  1. So first off, when you are creating this webpart select the "Visual Web Part option" in the project type.
  2. You MUST select DEPLOY AS FARM SOLUTION.  The sandboxed solution will not have access to the necessary objects needed for this webpart.  Specifically getting all site collections in a web application (managed path).
  3. To start we will create two classes.  SiteLookup and SPSiteInfo. 

    SiteLookup

    1. using Microsoft.SharePoint;
    2. using System;
    3. using System.Collections.Generic;
    4. using System.Linq;
    5. using System.Text;
    6. using System.Threading.Tasks;
    7.  
    8. namespace SPSiteListing.ListSPSites
    9. {
    10. class SiteLookup
    11. {
    12. private Boolean _EnableTrimming;
    13. private SPContext _Context;
    14. private string _CurrentUserName;
    15. public SiteLookup(SPContext context, Boolean enablePermissionTrimming)
    16. {
    17. _EnableTrimming = enablePermissionTrimming;
    18. _Context = context;
    19. _CurrentUserName = context.Web.CurrentUser.LoginName;
    20. }
    21. public List<SPSiteInfo> GetSites()
    22. {
    23. if (IsRootInApplication())
    24. {
    25. var list = new List<SPSiteInfo>();
    26. list.AddRange(GetSitesUnderCurrentWeb());
    27. list.AddRange(GetSitesUnderManagedPath());
    28. return list;
    29. }
    30. else
    31. return GetSitesUnderCurrentWeb();
    32. }
    33. public Boolean IsRootInApplication()
    34. {
    35. if (_Context.Site.RootWeb.Url != _Context.Site.Url)
    36. return false;
    37. if (_Context.Site.WebApplication.Sites[0].Url != _Context.Site.Url)
    38. return false;
    39. return true;
    40. }
    41. public List<SPSiteInfo> GetSitesUnderManagedPath()
    42. {
    43. var sites = new List<SPSiteInfo>();
    44. var applicationSites = _Context.Site.WebApplication.Sites;
    45. foreach (SPSite item in applicationSites)
    46. {
    47. //you must set disable catching access exceptions to prevent sharepoint from catching it
    48. item.CatchAccessDeniedException = false;
    49. try
    50. {
    51. if (item.RootWeb.DoesUserHavePermissions(_CurrentUserName, SPBasePermissions.Open) || !_EnableTrimming)
    52. sites.Add(new SPSiteInfo(item));
    53. }
    54. catch (UnauthorizedAccessException)
    55. {
    56. //The user does not have access to check their access. So an exception will be thrown.
    57. //This will not cause a problem to not do anything with it, since we are security trimming
    58. //we do not want this one listed anyway.
    59. }
    60. }
    61. return sites;
    62. }
    63. public List<SPSiteInfo> GetSitesUnderCurrentWeb()
    64. {
    65. var sites = new List<SPSiteInfo>();
    66. if (_EnableTrimming)
    67. {
    68. foreach (SPWeb item in _Context.Web.GetSubwebsForCurrentUser())
    69. {
    70. sites.Add(new SPSiteInfo(item));
    71. }
    72. }
    73. else
    74. {
    75. foreach (SPWeb item in _Context.Site.AllWebs)
    76. {
    77. sites.Add(new SPSiteInfo(item));
    78. }
    79. }
    80. return sites;
    81. }
    82. }
    83. }

    SPSiteInfo

    1. using Microsoft.SharePoint;
    2. using System;
    3. using System.Collections.Generic;
    4. using System.Linq;
    5. using System.Text;
    6. using System.Threading.Tasks;
    7.  
    8. namespace SPSiteListing.ListSPSites
    9. {
    10. class SPSiteInfo
    11. {
    12. public string SiteName { get; private set; }
    13. public string SiteUrl { get; private set; }
    14. public string HTMLLink
    15. {
    16. get
    17. {
    18. return String.Format(@"<a href=""{0}"">{1}</a>",
    19. SiteUrl,
    20. String.IsNullOrEmpty(SiteName) ? SiteUrl : SiteName);
    21. }
    22. }
    23. public SPSiteInfo(SPSite site)
    24. {
    25. try
    26. {
    27. SiteName = site.RootWeb.Title;
    28. }
    29. catch (UnauthorizedAccessException)
    30. {
    31. //since the user does not have access to get the title, we can do something
    32. //here if we want with demonstrating that. but it isn't necessary
    33. }
    34. SiteUrl = site.Url;
    35. }
    36. public SPSiteInfo(SPWeb site)
    37. {
    38. SiteName = site.Name;
    39. SiteUrl = site.Url;
    40. }
    41. }
    42. }
  4. So now that we have the classes written to get the data, we need a something to display the data in the webpart. To do that we will use the ascx file created already (just adding a ul with an id and a runat) and add some code in the page_load method.

    ASCX file

    1. <ul id="siteList" runat="server">
    2.  
    3. </ul>

    ASCX code behind

    1. protected void Page_Load(object sender, EventArgs e)
    2. {
    3. SiteLookup query = new SiteLookup(SPContext.Current, true);
    4. foreach (var item in query.GetSites())
    5. {
    6. var li = new HtmlGenericControl("li");
    7. li.InnerHtml = item.HTMLLink;
    8. siteList.Controls.Add(li);
    9. }
    10. }


So that is basically it. There are some details that are not covered in this post, but this should get you past the things that I struggled with when I created my webpart.


Good Luck !!

What is the point of out of the box SharePoint?

SharePoint is a powerful platform, but is SharePoint really anything without custom coding and is an out of the box solution worth the trouble?  The short answer is if you want a web based file share out of the box is fine…but if you need anything else, then plan on coding and jumping through a few hoops.

So what do you get out of the box?

  • File sharing

  • Basic (very basic) versioning control

  • Change the background and the color

So when does coding being?  It begins when you want to change the “SharePoint” word or logo at the top left of the screen.  When it comes to SharePoint the thought of it as a CMS needs to go away and it needs to be thought of as a platform for development. 

SharePoint demonstrates its extensibility and need for custom development when it comes to utilizing functionality that is promised in the out of the box functionality but is not delivered.  One area that this is very apparent is with version control and retention policies.  Version control works well out of the box but it does not do anything with retention policies meaning that if you need to maintain the current version’s retention policy independent from the historical version’s retention policy custom code will be needed.

To accomplish this functionality there are five requirements.

  1. A document library to hold the current documents.

  2. A content type that can be used for the record with applicable metadata.

  3. A record center with a record library to capture the versions.

  4. The record center must be setup as a Send To location for the SharePoint farm.

  5. Some code with an event receiver to capture the versions.

Using this methodology when a user begins the process to update a document the current version is immediately sent to the event receiver and based on the logic within there is a determination whether or not to archive.  The document is then sent to Send To location (the record center) which will handle the routing to the correct record library.  The record library can have whatever retention policies required by the documents applied.

Functionally there is only a few lines of code that are required for this to function:

  1. public override void ItemUpdating(SPItemEventProperties properties)
  2. {
  3. SPFile file = properties.ListItem.File;
  4. string strOut = "";
  5. OfficialFileResult retVal = file.SendToOfficialFile(out strOut);
  6. string value = retVal.ToString();
  7. }

However there is a bit more code that is required to make this a fully functioning option.  Click Here to see the full code (not including the installer for the feature).