Thursday, October 25, 2018

SharePoint Online: How to add unrestricted access to a dedicated site collection for external people and prevent them from accessing other sites


I had an interesting case related to granting unrestricted access to a specific site collection for external specialists while restricting them from accessing any other sites in our SharePoint Online instance.

Case


We had deployed a Financial SharePoint App from one of the software vendors into a new site collection. Some time later, Head of our IT department asked me to grant unrestricted access for maintenance specialists of that vendor only to this site collection and ensure they cannot access anything else to comply with GDPR enforcement.

After a short discussion on available options, we discovered a couple of issues there
  • As you probably know, adding external accounts to site Owners group does not provide absolutely unrestricted access to system areas of this site. The account must be added namely to Site Collection Administrators.
  • Creating temporary regular “internal” O365 accounts was not an option as they would immediately become members of the virtual group “Everyone except external users”. In our case, this group had Read permissions configured in many other site collections including HR data for the employees, IT Service Desk, etc. There’s no way to remove an internal O365 account from “Everyone except external users”.


Established solution


Steps below assume you are a Global Administrator in your O365 Tenant.

1. Create a dummy Gmail or Outlook Online account then create a new Microsoft Account based on it.
  • For example, vendorname@gmail.com
  • You can register a new Microsoft Account in O365 sign-in window; there’s a link to do it.

2. Configure O365 Tenant-wide external sharing
  • https://portal.office.com/adminportal/home#/homepage > Search for “Sites external sharing” > Adjust settings if necessary.

3. Configure external sharing in your target Site Collection to allow New and existing external users.
  • https://portal.office.com/adminportal/home#/homepage > Search for “Manage sites” > Find your site collection > Adjust Sharing status to “New and existing external users (sign-in required)” and save changes. 
  • Make sure changes have been actually applied (it may take some time).

4. SharePoint Online does not support adding new external accounts directly to Site Collection Administrators. However, there’s a simple workaround. Open your site collection and add the Microsoft Account from p.1 to any SharePoint group of your site, for example, to site Visitors.
  • You can find it under https://<your site collection>/_layouts/15/user.aspx
  • If you are unable to add the external user to the group, check the settings of external sharing (see p.2 and 3 above).

5. After that, you will be able to add your Microsoft Account to Site Collection Administrators.
  • Use https://<your site collection> /_layouts/15/mngsiteadmin.aspx
  • After that, remember to remove this account from a site group to which you added it in p.4.

6. Adjust external sharing settings for your site collection to support Only existing external users.
  • https://portal.office.com/adminportal/home#/homepage > Search for “Manage sites” > Find your site collection > Adjust Sharing status to “Only existing external users (sign-in required)” and save changes. 
  • Make sure changes have been actually applied (it may take some time).
  • This sharing level provides correct functioning of external site collection administrators but prevents them from attempts to share the site collection with other externals.

7. Now try to sign-in using the Microsoft Account from p.1. This account should have unrestricted control over the target site collection while access to any other sites will be denied.


Known issues


Term Store does not seem to have any options to restrict Read access for external users. In our case, it did not have confidential data.

Monday, February 5, 2018

Alter Modern UI of SharePoint Online pages using just a conventional JavaScript

In one of my recent projects, I have discovered a relatively quick and simple way to extend Modern UI of SharePoint pages without deploying any tailor-made SPFx-component.

As you probably know, Microsoft has prohibited execution of conventional JS-code in Modern pages of SharePoint Online / 2019 "on-prem".

There is an official option to enable running JS-code in Modern SharePoint sites - by turning off NoScript feature - but it only permits running JS-code in classic pages.

It's pity but Modern UI pages of SharePoint do not support execution of conventional JavaScript by design, even with NoScript feature disabled.

To address this problem, Microsoft usually suggests coding relatively time-consuming SPFx components. And, of course, you need to study how to code them. In many cases, it simply does not worth to mess with SPFx parts for the sake of making a tiny dynamic change or correction in Modern UI. For example, to inject a short informational block at the end of the current navigation in O365 Group.

However, I have found an interesting undocumented breach in Modern UI of SharePoint, which permits direct injection and execution of conventional JavaScript in modern pages.

Shortly about the technique


1. Create a classic page in some classic site of SharePoint, which permits execution of JavaScripts in it. This site can have different URL but it should be in the same tenant.
- For example, /siteassets/modern/pages/classic.aspx. Hint: you can use /_layouts/15/spcf.aspx to add a new web-part page to a desired library and then move it to another folder.

2. Add a standard Embed Client Web Part to a Modern page of SharePoint, which you want to extend with conventional JavaScript.

3. Configure Embed-part to load the classic page from p.1. Embed-part is rendered a regular IFrame by default.

4. Add a custom JS-code file to a SharePoint library
- For example, to be available as /siteassets/modern/js/custom-modern-ui.js

5. Add this JS-code to the classic page. This code will inject itself and any additional JS-scripts into its parent Modern page via IFrame. And the injected code can do any changes on the Modern page!

It's technically possible to do because the parent page with IFrame and the classic page loaded into the IFrame's body have the identical origin in the same tenant.

Using the described technique, it becomes easy to utilize, for example, classic SharePoint modal dialogs in Modern UI pages of O365 Groups (see the screenshots). I know that many of my fellow developers and users would still want to have them in Modern UI.

What's important, this technique works independently on NoScript feature. It does not matter if it's turned on or off.

People, company, and event names shown in the screenshots below are fictitious. Any similarities to real ones are merely coincidental.


Sample JavaScript injector


  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
(function () {
  // --------------------------------------------------------------------------------------------------------
  // SharePoint Online conventional JavaScript extender for Modern UI pages.
  // --------------------------------------------------------------------------------------------------------
  // Copyright: Paul Borisov, 2018. You can use the code below "as is" or modified without any limitations.
  // Should you find this module useful in your projects/tasks please keep this copyright header intact.
  // --------------------------------------------------------------------------------------------------------
  // Usage:
  // Put this code to some place in your SharePoint Online Tenant. 
  // For example, make it simple and put it to /siteassets/modern/js/custom-modern-ui.js
  // - Note/change the reference at line 56: /siteassets/modern/js/custom-modern-ui.js
  // Add a reference to the JS-file into a classic page of a classic SharePoint site situated in the same
  // SharePoint Online tenant (i.e. it should have the same origin as your Modern UI page).
  // For example, /siteassets/modern/pages/classic.aspx
  // - Note/change references at lines 202 and 212:
  // iframe[src*='\/classic\.aspx'], /siteassets/modern/pages/classic.aspx
  // Note you can also use a modern SharePoint site with disabled NoScript feature. 
  // After that, edit your Modern UI page, add OOB Embed-part, and configure its IFrame source 
  // to point to your classic page (see above). Save changes, publish the page, and reload it.
  // Expected results: the code should inject itself in a Modern UI page, and extend its current navigation
  // with a tiny informational block. This block will contain a button.
  // Click on this button should open a classic SharePoint dialog being in the context of Modern UI.
  // - Correct its URL at line 194.
  // --------------------------------------------------------------------------------------------------------
  // Author: Paul Borisov, http://paulborisov.blogspot.fi/2018/02/a-fancy-option-to-alter-modern-ui-of.html
  // --------------------------------------------------------------------------------------------------------
  var _MaxLoadAttempts = 50;
  var _CurrentLoadAttempt = 0;
  
  var _EditGroupInfo = 
  '<div class="pb-edit-button">' +
    '<input type="button" value="Edit" />' +
  '</div>';

  var _MessageGroupInfoNotFound = 
  '<div class="pb-container">' +
    '<div class="pb-start-of-table">Group Information</div>' +
    '<div class="pb-noresults ms-cellstyle ms-vb2">There is no data to show.</div>' +
  '</div>';

  var _Css = 
    '<style type="text/css">' + 
    ' .pb-container {}' +
    ' .pb-start-of-table {font-weight:bold;font-size:16px;display:block !important;margin-left:18px;}' +
    ' .pb-noresults {margin-left:14px;}' +
    ' .pb-edit-button {}' +
    ' .pb-edit-button input {width:85%;margin-left:19px;margin-bottom:10px;}' +
    ' .ms-Nav {overflow:hidden;}' +
    ' .sp-livePersonaCardAdapter-root {background-color:transparent !important;}' +
    ' .sp-livePersonaCardAdapter-root canvas {width:32px;}' +
    ' div[class*="spAvatar"] {margin-right:15px;}' +
    '</style>';
  
  var _SiteResources = [
    "//cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"
    ,"/siteassets/modern/js/custom-modern-ui.js"
  ];
  var _ResourcesToSupportModalDialogInModernPages = [
    "/_layouts/15/init.js"
    ,"/_layouts/15/sp.init.js"
    ,"/_layouts/15/1033/initstrings.js"
    ,"/_layouts/15/1033/sp.res.js"
    ,"/_layouts/15/msajaxbundle.js"
    ,"/_layouts/15/sp.ui.dialog.js"
    ,"/_layouts/15/core.js"
    ,"/_layouts/15/1033/styles/corev15.css"

    // There is a bug in MS loaders: sometimes initialization fails and requires a repeatable load.
    // In a repeatable load the same initialization always succeeded.
    ,"/_layouts/15/init.js"
    ,"/_layouts/15/sp.init.js"
    ,"/_layouts/15/1033/initstrings.js"
    ,"/_layouts/15/1033/sp.res.js"
    ,"/_layouts/15/msajaxbundle.js"
    ,"/_layouts/15/sp.ui.dialog.js"
    ,"/_layouts/15/core.js"
    ,"/_layouts/15/1033/styles/corev15.css"
  ];
  var _ResourcesToSupportModalDialogInModernPagesLoaded = false;
  var _SiteResourcesLoaded = false;
  var _IFrameContext = false;

  function LoadSingleResourceFile( fileUrl, force ) {
    if( fileUrl == null ) return;

    var el = null;
    if( fileUrl.match(/\.css/i) ) {
      var alreadyLoaded = document.querySelector("link[href*='" + fileUrl + "']") != null;
      if( !alreadyLoaded || force ) {
        el = document.createElement("link");
        el.rel = "stylesheet";
        el.type = "text/css";
        el.href = fileUrl;
      }
    } else if( fileUrl.match(/\.js/i) ) {
      var alreadyLoaded = document.querySelector("script[src*='" + fileUrl + "']") != null;
      if( !alreadyLoaded || force ) {
        el = document.createElement("script");
        el.type = "text/javascript";
        el.src = fileUrl;
      }
    }
    if( el ) {
      window.parent.document.getElementsByTagName("head")[0].appendChild(el);
      console.log(fileUrl + " loaded in " + window.parent.location.href);
    }
  }

  function LoadSingleResourceFile2( fileUrl, force ) {
    if( fileUrl == null ) return;

    var el = null;
    if( fileUrl.match(/\.css/i) ) {
      var alreadyLoaded = document.querySelector("link[href*='" + fileUrl + "']") != null;
      if( !alreadyLoaded || force ) {
        el = document.createElement("link");
        el.rel = "stylesheet";
        el.type = "text/css";
        el.href = fileUrl;
      }
    } else if( fileUrl.match(/\.js/i) ) {
      var alreadyLoaded = document.querySelector("script[src*='" + fileUrl + "']") != null;
      if( !alreadyLoaded || force ) {
        el = document.createElement("script");
        el.type = "text/javascript";
        el.src = fileUrl;
      }
    }
    if( el ) {
      jQuery("head").append(jQuery(el));
      console.log(fileUrl + " loaded in " + window.parent.location.href);
    }
  }
  
  function LoadResources(urlArray) {
    if( urlArray == null ) return;
    
    for( var key in urlArray ) {
      var fileUrl = urlArray[key];
      try {
        LoadSingleResourceFile2(fileUrl, true);
      } catch(e) {
        console.log("ERROR loading " + fileUrl + ": " + e.message);
      }
    }
  }

  function LoadSiteResources() {
    if( !_SiteResourcesLoaded ) {
      for( var key in _SiteResources ) {
        var fileUrl = _SiteResources[key];
        try {
          LoadSingleResourceFile(fileUrl, true);
        } catch(e) {
          console.log("ERROR loading " + fileUrl + ": " + e.message);
        }
      }
    }
    _SiteResourcesLoaded = true;
  }

  function LoadDynamicContent() {
    _CurrentLoadAttempt++;
    if( typeof jQuery == "undefined" ) {
      if( _CurrentLoadAttempt <= _MaxLoadAttempts) {
        setTimeout(LoadDynamicContent,100);
        return;
      } else {
        console.log("jQuery was not loaded in " + (_CurrentLoadAttempt-1) + " attempts. Processing stopped.");
        return;
      }
    }
    
    jQuery(document).ready(function(){
      if( _IFrameContext ) {
        // Inject the main resources on execution inside the IFrame
        // Note it also injects this JS-file a new instance of which is then executed in the main window (parent of iframe).
        LoadSiteResources();
      } else {
        if( document.querySelector(".pb-container") ) return;
        // Inject custom CSS.
        jQuery("head").append(jQuery(_Css));
        // Inject the additional resources like prerequisites for SP modal dialog on execution inside the main parent window.
        if( !_ResourcesToSupportModalDialogInModernPagesLoaded ) {
          LoadResources(_ResourcesToSupportModalDialogInModernPages);
        }
        _ResourcesToSupportModalDialogInModernPagesLoaded = true;

        var edit = jQuery(_EditGroupInfo);
        edit.on("click", function() {
          SP.SOD.executeFunc('sp.ui.dialog.js', 'SP.UI.ModalDialog',
            function(){
              var _options = {
                url:'http://paulborisov.blogspot.fi/2018/02/a-fancy-option-to-alter-modern-ui-of.html'
              };
              SP.UI.ModalDialog.showModalDialog(_options);
            }
          )
        });
        jQuery("#sideNavBox,nav.ms-Nav").append(edit);
        jQuery("#sideNavBox,nav.ms-Nav").append(jQuery(_MessageGroupInfoNotFound));
        var iframe = document.querySelector("iframe[src*='\/classic\.aspx']");
        if( iframe != null ) {
          jQuery(iframe).parents(".ControlZone:first").detach();
        }
      }
    });
  }
  
  //if( document.location.href.match(/GroupSummary\.aspx/i) ) {
  if( document.location.href.toLowerCase() != window.parent.document.location.href.toLowerCase() ) {
    // This part is executed when the page /siteassets/modern/pages/classic.aspx
    // is loaded inside the iframe hosted in a modern site's page.
    // After that, it injects necessary scripts including jQuery, this script, and some custom.css.
    // from the folder /siteassets/modern into the same modern site's page, which contains the iframe.
    _IFrameContext = true;
    LoadDynamicContent();
  } else {
    // This part is executed after the file was injected into the modern site's page.
    // For example, into https://customer.sharepoint.com/sites/SomeO365group/SitePages/Home.aspx
    if( document.location.href.match(/Mode=Edit/i) == null ) {
      // If the page is not in edit mode
      _IFrameContext = false;
      LoadDynamicContent();
    }
  }
})();