Showing posts with label Powershell. Show all posts
Showing posts with label Powershell. Show all posts

Friday, March 11, 2022

Tip: Reuse authentication of MFA secured account over multiple Connect-PnPOnline calls

A best security practice to connect into SharePoint Online is configure MultiFactor Authentication (MFA). When connecting from PowerShell to SharePoint Online this can give some challenges, as the default 'Credentials' based logon is not MFA aware. Resolution for this is to use either '-Interactive' or '-PnPO365ManagementShell' flag: both result that you are enabled to interactive address the MFA challenge.
Need to address the MFA challenge is acceptable to do one-time. But in an administration context, it might be that you need to execute settings over a set of sitecollections. Then it is not a pleasant experience to everytime need to (re)logon including MFA challenge. Common way to address this is by piping the authenticated SPO connection into the subsequent PnP calls. But Connect-PnPOnline does not support the '-Connection' flag (other PnP cmdlets do support it; PnP is not consistent across all its cmdlets). But I found an alternative that works:
  • $connection = Connect-PnPOnline -Url $spoAdminUrl -PnPO365ManagementShell -ReturnCollection
  • Connect-PnPOnline -Url <Url to other site collection> -Interactive -ClientId $connection.ClientId

Friday, April 10, 2020

Tip: How-To determine whether guest email already in-usage in your Azure AD tenant

Invitation of a new Azure AD B2B guest is blocked in case the identifying guest email is already present in the inviting Azure AD. A typical situation in which this occurs is that the intended guest is already present as regular Member account, and has to that the external email associated. To avoid the error on invoking Invite Api and instead send an informed response to the requestor, the B2B invitation automation can check first on any occurence of the external email.
The codeline for that is:
Get-AzureADUser -Filter "proxyAddresses/any(c:c eq 'smtp:external.user@externalComp.com')" | Select UserType, UserState

Needed Api Permission for Service Principal to invoke Get-AzureADUser

This week I extended our custom B2B invitation handling build as Azure Automation Runbook to check upfront whether guest account is not present yet. The way to check is by quering for potential presence of requested Guest account via the UPN:
if ($guestAccRequests.length -gt 0) {
    # Get the Service Principal connection details for the Connection name
    $servicePrincipalConnection = Get-AutomationConnection -Name $servicePrincipalConnName   

    # Logging in to Azure AD with Service Principal
    Connect-AzureAD -TenantId $servicePrincipalConnection.TenantId -ApplicationId $servicePrincipalConnection.ApplicationId -CertificateThumbprint $servicePrincipalConnection.CertificateThumbprint

    ...

            # 1. Check whether already present as guest account
            $guestUpn = ConvertExternalEmailToUpn -guestEmail $guestEmail
            $existingAccount =  Get-AzureADUser -Filter "userPrincipalName eq '$guestUpn'" | Select UserType, UserState
            if ($existingAccount) {
                ... 
            if ($continueToInvite) {
                try {
                    $inviteResult = New-AzureADMSInvitation -InvitedUserEmailAddress $guestEmail -InvitedUserDisplayName $guestDisplayNameWithOrg -InvitedUserMessageInfo $messageInfo -SendInvitationMessage $False -InviteRedirectUrl "http://www.wvstrien.com" -ErrorAction Stop
                    $inviteurl = $inviteResult.InviteRedeemUrl

The PowerShell coding part was quickly up-and-running. More time-consuming was identifying the proper Api Permission to grant to the Service Connection that connects into Azure AD. We already assigned the 'User.Invite.All' Api Permission from the Microsoft Graph Api, and one would expect that also permission for Get-AzureADUser is within the Graph Api permissions. However, granting the likely permissions 'User.Read.All', 'Directory.Read.All', 'Domain.Read.All'; none of these authorizes the Service Connection.
The information provided by Microsoft in identifying the needed Api Permission is not very good, or at least not easy to find. The StackOverflow post Graph API - Insufficient privileges to complete the operation put me on the right track. Turns out that for this cmdlet you still need to assign permission from the deprecated Azure Active Directory Api.

Saturday, January 13, 2018

Approach to a more trusted Azure AD B2B invite handling

Simple approach to invite guest users is via the Azure AD admin portal. Disadvantage is that you have no control at all over the invite email that is being sent:
  • It comes from the invites@microsoft.com mail origin instead of the inviting company
  • It is sent on behalf of the account of Azure AD Admin (or other admin account that possesses the 'Guest inviter role'), iso an account of the actual business user at the inviting company
  • The mail is in a standard format, without branded to the inviting company
  • The mail includes a suspicious looking "Get Started" button
  • The mail includes a "Click here to unsubscribe" link, which again looks suspicious as the receiver never subscribed to invites from Microsoft.com.
This results that - and in my opinion, in current days very understandable and justified - invited persons may not trust the invite email, suspect it is a phishing mail.
My first approach to improve on this is to invite the guest users via 'New-AzureADMSInvitation' PowerShell cmdlet, and make up the email text itself. However, you still have very little control over the mail setup: the only thing you can do it to specify the invite message. And this is limited to plain text, you cannot make a rich format including company logo. All of the above drawbacks still hold.
On questioning to Microsoft, their advice for now is to use the Azure AD B2B Invite API to make a company-customized invite experience. But this requires an Azure App to be designed, developed + deployed. Although sure would be fun to do, current I lack the time for that.
Therefore I decide for an approach in the middle. Still use the PowerShell cmdlet to create the guest accounts. But instead of direct sending the invite email, I let the PowerShell script make up a 'mailto:' specification that includes the guest user redeem-url. These specifications are exported per invited user via a .csv file. Next the Azure AD admin sends the .csv file to the responsible business user of the inviting company. And he or she can then direct from the received .csv file with the 'mailto:'specifications make up an invite mail in their own email outbox, make some final changes and additions (e.g. add personal mail signature; including items like company logo, contact details of the business user, ...); and next send out the mail. The invited guest then receives the mail from the mail domain of the inviting company, and of a person (s)he likely already is in contact with. As extra bonus I apply Office 365 email encryption, to give the invited user the trust that only he or she is able to read the invite mail.

Friday, December 29, 2017

How-To: enable Azure Active Directory for Powershell on Windows 7 system

The AzureAD PowerShell module contains multiple convenient cmdlet's to manage an Azure AD tenant, a.o. to invite external accounts (e.g.: Automatically Provision Azure AD B2B Guest Accounts). When you try to use the AzureAD PowerShell module on a Windows 7 system, you may run into issue: The specified module 'AzureAD' was not loaded because no valid module file was found in any module directory
If so, the solution is import the module file via it's exacte filepath (inspiration for this: Azure RM PowerShell Module / Other Installation methods)
And the AzureAD cmdlets can be succesful utilized:

Thursday, December 14, 2017

Automatic Azure AD B2B redemption is not feasible

Azure AD Business-2-Business is the enterprise-ready and secure new approach to enable SharePoint Online external sharing. In this approach, externals are added as guest users in the Azure AD of the inviting company. The guest account in the host Azure AD functions as placeholder to the actual account of the invited guest in external identity administration. Azure AD B2B is an implementation of federated Identity Management.
The process to authorize an external person for access in shared SharePoint Online site consists of 2 steps:
  1. First, Azure AD admin (or anyone who has the “Guest Inviter” role) has to add a guest account to the host Azure AD
  2. Next, site owner can invite the guest account to the external shared site
However, it turns out that there is some usage unclarity and sequence dependency in this process:
  • Usage unclarity: For the first step, the guest receives invite in mailbox to accept / redempt the invite. But next the guest user is redirected to empty Apps page in the tenant of inviting organization ==> no authorizations are granted yet;
  • Sequence dependency: The site owner cannot execute the 2nd step until the Azure AD invite is redempted ==> 'Sharing failed: Sharing to external users is not supported' (which is a misleading / incorrect error message; sharing is supported, yet not to the particular guest account as long (s)he has not redempt the invitation to the hosting Azure AD)
To address the first drawback, which may result in negative first impression with the invited guest ("I only see an empty Apps page, cannot do anything"), you can utilize the PowerShell Azure AD cmdlet 'New-AzureADMSInvitation' with '-SendInvitationMessage' parameter set to false. The result is that the invited guest is not informed yet of the invite to Azure AD, that from the perspective of the guest user is useless anyhow: one can only do something after authorized to a SharePoint site. But the indirect result is that the Azure AD invite is not redeemed - the guest is not made aware nor asked to perform the redemption -, and this results that the site owner cannot add the guest user. A catch22 situation.
A potential way out would be to automatic redempt the Azure AD invitation on behalf of the invited guest. However, this is only possible in case the organization of invited guest itself has an Azure AD tenant. Although the usage of Azure is growing in the market, there is and will remain a large set of organizations that have not themselves an Azure subscription + tenant. And then the automatic redemption on behalf of guest users is not possible.

Monday, February 9, 2015

Pre-provision personalized page for new (first-time) users

A SharePoint customer is realizing the concept of Digital Workplace with SharePoint 2013 as platform foundation. Crucial element of this Digital Workplace is that the entry point is personalized, each employee can personalize the entry point [Dashboard] to own interests and work responsibilities. The user can select Apps from the company AppStore to place on the personalized home page.
To familiarize the employees with this new concept, at Go-Live the homepage will already be pre-provisioned for each individual user with a default set of Apps from the company AppStore.
The following aspects make this task somewhat challenging:
  1. Personalization of the home page requires to pre-provision the page impersonated as the individual users, and access the personalized page scope.
  2. The Digital Workplace is a new webapplication; the employees have not yet visited this webapplication. (SharePoint) Consequence is that the employee accounts are not yet administrated in the SharePoint user list, and thus no usertoken can be determined.
  3. It is not allowed to deploy a custom-build application in the SharePoint farm. This precludes the usage of a .Net console application for pre-provision execution.

Solution design

Construct a Powershell script that is feed with an user list, for each user access the home page impersonated as that user, and add the collection of Apps as ClientWebPart to the personalized webpartmanager scope.

Structure

Specialties

The users have not yet visited the webapplication, and thus are not administrated yet in the SharePoint sitecollection user list. Solution: use ‘ensureUser’.

Powershell script

$snapin = Get-PSSnapin | Where-Object {$_.Name -eq 'Microsoft.SharePoint.Powershell'} if ($snapin -eq $null) { Add-PSSnapin "Microsoft.SharePoint.Powershell" } # # -- Initialization -- # # Environment specific settings # $siteUrl = "<site-url>" $ADDomain = "<AD-domain>" # Environment-transparant settings # $homePagePath = "/Pages/Home.aspx" $homePageUrl = "$siteUrl$homePagePath" $errorImportingWbMsg = "Error Importing WebPart" # Pre-Provision configuration value # $loc = Get-Location $Users = Get-Content "$loc\Config\Users.txt" $WebPartDefinitions = Get-Content "$loc\Config\WebPartDefinitions.txt" $webPartCatalogRoot = "$loc\WebPartFiles\" # Access the web as administrator # $web = Get-SPWeb -identity $siteUrl # # -- Functions -- # function AddWebPartToPage($limitedWebPartManager, $WebPartDefinition, $User) { $TitleInAppCatalog = $WebPartDefinition.Split("|")[0] $WebPartFile = $WebPartDefinition.Split("|")[1] $WebPartZoneId = $WebPartDefinition.Split("|")[2] # Initialize the XmlReaderSettings Object which is required for the # XmlReader Object $xmlReaderSettings = New-Object System.Xml.XmlReaderSettings # Create the XmlReader Object by using the WebPart Definition file # and the ReaderSettings Object $WebPartUrl = "$webPartCatalogRoot$WebPartFile" $xmlReader = [System.Xml.XmlReader]::Create($WebPartUrl, $xmlReaderSettings); # Get the WebPart Definition Object based on the webpart definition xml file $oWebPartDefinition = $limitedWebPartManager.ImportWebPart( $xmlReader,[ref]$errorImportingWbMsg); # Add the WebPart to the WebPartManager by specifing the Zone and the Index. $limitedWebPartManager.AddWebPart($oWebPartDefinition,$WebPartZoneId,1); $limitedWebPartManager.SaveChanges($oWebPartDefinition) } function PreProvisionForUser($User) { $impSite = New-Object Microsoft.SharePoint.SPSite( $web.url, $web.EnsureUser("$ADDomain\$User").userToken) $impWeb = $impSite.OpenWeb(); $page = $impWeb.GetFile($homePagePath) if ($page.Exists) { # Make the Web as AllowUnSafeUpdates as true $impWeb.AllowUnsafeUpdates = $true # Get the LimitedWebPartManager $limitedWebPartManager = $page.GetLimitedWebPartManager( [System.Web.UI.WebControls.WebParts.PersonalizationScope]::User) # Reset any personalization state (e.g. in case of repeated pre-provision) $limitedWebPartManager.ResetAllPersonalizationState() # Pre-provision the page by adding all the configured webparts foreach($WebPartDefinition in $WebPartDefinitions) { AddWebPartToPage $limitedWebPartManager $WebPartDefinition $User } # Revert the AllowUnsafeUpdates to False once we are done. $impWeb.AllowUnsafeUpdates = $false Write-Host "Pre-Provision done for user:" $User } $impWeb.Dispose(); $impSite.Dispose(); } #### EXECUTE ### foreach ($User in $Users) { PreProvisionForUser($User) }

Friday, September 5, 2014

PowerShell to repeatedly provision SharePoint master and config data

The ease of creating and maintaining SharePoint lists also makes them a good option for application configuration. As example, in one application I applied SharePoint list as the configuration storage for task types specification, in another as the configuration storage of the specifications for BCS invocations via a generic BDC model. However, as easy it is to create a list; so it is also to delete one including the configuration contents. In particular in the development phase, with repeated solution deployment and feature de- and reactivation, typically the configuration list and thus its contents get deleted.
To cope with this, I utilize PowerShell to refill the list with the desired configurations. And the same PowerShell scripts are also of use for transition of the application from development landscape to QA, and ultimate to production.
Example:
# script Clear # Fill Configuration $webApp = “<url>" $ApplicationConfigList="/Lists/ApplicationConfiguration"   # functions   function X-CheckItemNotExists($list, $itemTitle) {    $camlQuery =       "<Where>          <Eq>             <FieldRef Name='Title' />             <Value Type='Text'>" + $itemTitle + "</Value>          </Eq>        </Where>"    $spQuery = new-object Microsoft.SharePoint.SPQuery    $spQuery.Query = $camlQuery    $spQuery.RowLimit = 1    $destItemCollection = $list.GetItems($spQuery)    return $destItemCollection.Count -eq 0 }   # Initialization   $spWeb = Get-SPWeb $webApp   # (Re)fill the Application Config Settings   $ApplicationConfigSettings = @{    "0" = @("<config-item title>", "AovpGetPerson ", "{0,8:D8}{1,30: 30}", "<SAP-system-ID>");    "1" = @("< config-item title>", "CrmGetCustomer", "{0,8:D8}{1,8:D8}", "<Oracle-system-ID>") }                                $spApplicationConfigList = $spWeb.GetList($ApplicationConfigList) foreach ($array in $ ApplicationConfigSettings.values) {    if (X-CheckItemNotExists -list $spApplicationConfigList -itemTitle $array[0].ToString())    {       $spApplicationConfigItem = $spApplicationConfigList.AddItem()       $spApplicationConfigItem["JsonMethodName"] = $array[0].ToString()       $spApplicationConfigItem["GenericModuleMethodName"] = $array[1].ToString()       $spApplicationConfigItem["CallInputParamsFormat"] = $array[2].ToString()        $spApplicationConfigItem["ExternalSystemAlias"] = $array[3].ToString()          $spApplicationConfigItem.Update()    } }    # Release   $spWeb.Dispose()

Thursday, August 22, 2013

Powershell to list all site collections in farm with feature activated from ‘Farm’ definition scope

In SharePoint 2010, a Feature can be installed from a farm solution and from a sandbox solution. In case of farm solution, the Feature is installed on farm-level, and dependent on the feature scope, visible (unless hidden) for all webapplications, site-collections or webs to activate (by GUI, Powershell and yes, even stsadm). In case of sandboxed solution, only possible for scope = Site or Web, the feature is only visible within the site-collection, and can also only be activated in the site-collections to which added and activated.
Today we encoutered a situation in which a sandboxed solution was per accident also deployed on the farm. The result was that features within the solution were installed and visible twice in each site collection to which it also was added as sandbox. The remedy is to retract the per-accident farm deployment. But we must take into account that features from the SharePoint solution may have been activated from the farm-deployed version.
Powershell enables us to easily determine in which site collections in the farm the feature is activated from the farm-deployed solution:
$snapin = Get-PSSnapin | Where-Object {$_.Name -eq 'Microsoft.SharePoint.Powershell'}
if ($snapin -eq $null) {
    Add-PSSnapin "Microsoft.SharePoint.PowerShell"
}

$featureId = ".......-....-....-....-............";
$contentWebAppServices = (Get-SPFarm).services | ? {$_.typename -eq "Microsoft SharePoint Foundation Web Application"}
foreach($webApp in $contentWebAppServices.WebApplications) {
  foreach($site in $webapp.Sites) {
    Get-SPFeature -Site $site| where{$_.Id -eq $featureId}|%{
    if ($($site.QueryFeatures($featureId)).FeatureDefinitionScope -match "Farm"); {
        Write-Host $_.DisplayName " is Activated from Farm deployment on site : " $site.url
      }
    }
  }
}