Wednesday, October 7, 2020

 

Analyze Microsoft 365 User Profile Photos using Azure Computer Vision API and CLI for Microsoft 365


With 2020 and the onset of a new decade, there has been a dramatic shift in the professional working paradigm. With more people working from home, video meetings, virtual discussion rooms and other AI tools becoming the norm, technology is connecting us across oceans and continents. Yet, with ‘great power comes great responsibility’ and with ‘the great power of digital connection comes increasing impersonality.’

As the adage goes ‘First impressions are the best impressions’, now is the perfect time to make a lasting and valuable first impression with your profile photos.

Here are a few areas where the profile picture can offer some much-needed help:

Less anonymity and more engagement

As humans, we’re visual beings - ‘A picture speaks louder than a thousand words’. So using profile photos eliminate anonymity and add the much needed humanistic touch to exchanges that could seem cold. With profile photos, you feel like you’re interacting with a real human.

And you tend to respond better and remember these interactions rather than those with a faceless nobody.

This is especially important right now as a large number of us are working remotely, and digital interactions are the sole means of communication.

Builds a sense of community

With profile photos, employees can get acquainted with peers outside of their immediate team, department, and office location. There’s no doubt that we’re all caught up in the events of our immediate colleagues and have no interaction with those beyond that.

In the digital workplace, profile photos contribute to employees better understanding and communicating with each other.

Acts as your digital brand

Inside your organization, your profile photo acts as your digital brand. If your aim is to get recognized within the company, a profile photo is a way to go.

Let alone an employee, even as a manager, there may be times where you need to applaud or celebrate an employee. Imagine not having access to a single photo of them or if their profile photo is that of their pets or them at a party?

As you search the depths of your file, you may come across a photo that aren’t sure you can even use. Why go through this hassle? With every employee having a professional profile photo, this worry no longer exists.

Despite these benefits, it’s shocking to see the number of people who don’t utilize this opportunity.

Results from Hyperfish study showed that in organizations with over 10,000 employees, about 97% don’t have a professional-looking profile picture.

Seems unbelievable, doesn’t it?

Little do people realize the significance of profile photos and their ability to contribute to a better and more interactive digital workspace environment. So to make it easier for organizations, Microsoft’s Computer Vision API offers state-of-the-art algorithms to process images and return information. For instance, you can use it to determine if an image contains mature content, whether it’s a group photo, an image of their pet or much more.

In this article, I have included a script that uses Azure Cognitive Service API and Microsoft 365 CLI to analyze user profile pictures and assess whether they meet the standards placed by the organization.

This script can be customized to ban content within an org channel or collaboration network where employees post pictures, memes, etc.

Prerequisites

Note: If you don’t already have an Azure Cognitive Services instance and key, create a cognitive service instance and get API key from there.

I would like to thank Hugo Bernier, for the sample SharePoint Framework WebPart.

PowerShell Script

$resultDir = "Output"
$azureVisionApiInstance = "azure-vision-api-instance-name"
$azureVisionApiKey = "azure-vision-api-key"
$photoRequirements = @{
    requirePortrait   = $false
    allowClipart      = $true
    allowLinedrawing  = $true
    allowAdult        = $false
    allowRacy         = $false
    allowGory         = $false
    forbiddenKeywords = @("cartoon", "animal", "nude")
}
$requiredProfileProperties = "id,displayName,mail"
$global:analysisOutcomes = @()

$executionDir = $PSScriptRoot
$outputDir = "$executionDir/$resultDir"
$outputFilePath = "$outputDir/$(get-date -f yyyyMMdd-HHmmss)-scan-profile-pictures-outcome.csv"

if (-not (Test-Path -Path "$outputDir" -PathType Container)) {
    Write-Host "Creating $outputDir folder..."
    New-Item -ItemType Directory -Path "$outputDir"
}
function AddAnalysisOutcome {
    param (
        [Parameter(Mandatory = $false)] [string] $UserId,
        [Parameter(Mandatory = $false)] [string] $UserMail,
        [Parameter(Mandatory = $false)] [bool] $IsPortraitValid,
        [Parameter(Mandatory = $false)] [bool] $IsOnlyOnePersonValid,
        [Parameter(Mandatory = $false)] [bool] $IsClipartValid,
        [Parameter(Mandatory = $false)] [bool] $IsLineDrawingValid,
        [Parameter(Mandatory = $false)] [bool] $IsAdultValid,
        [Parameter(Mandatory = $false)] [bool] $IsRacyValid,
        [Parameter(Mandatory = $false)] [bool] $IsGoryValid,
        [Parameter(Mandatory = $false)] [bool] $IsCelebrity,
        [Parameter(Mandatory = $false)] [bool] $IsForbiddenKeywordExist,
        [Parameter(Mandatory = $false)] [bool] $IsValidProfilePhoto,
        [Parameter(Mandatory = $false)] [string] $Notes
    )

    $analysisOutcome = New-Object -TypeName PSObject

    $analysisOutcome | Add-Member -MemberType NoteProperty -Name "UserId" -Value $UserId
    $analysisOutcome | Add-Member -MemberType NoteProperty -Name "UserMail" -Value $UserMail
    $analysisOutcome | Add-Member -MemberType NoteProperty -Name "IsPortraitValid" -Value $IsPortraitValid
    $analysisOutcome | Add-Member -MemberType NoteProperty -Name "IsOnlyOnePersonValid" -Value $IsOnlyOnePersonValid
    $analysisOutcome | Add-Member -MemberType NoteProperty -Name "IsClipartValid" -Value $IsClipartValid
    $analysisOutcome | Add-Member -MemberType NoteProperty -Name "IsLineDrawingValid" -Value $IsLineDrawingValid
    $analysisOutcome | Add-Member -MemberType NoteProperty -Name "IsAdultValid" -Value $IsAdultValid
    $analysisOutcome | Add-Member -MemberType NoteProperty -Name "IsRacyValid" -Value $IsRacyValid
    $analysisOutcome | Add-Member -MemberType NoteProperty -Name "IsGoryValid" -Value $IsGoryValid
    $analysisOutcome | Add-Member -MemberType NoteProperty -Name "IsCelebrity" -Value $IsCelebrity
    $analysisOutcome | Add-Member -MemberType NoteProperty -Name "IsForbiddenKeywordExist" -Value $IsForbiddenKeywordExist
    $analysisOutcome | Add-Member -MemberType NoteProperty -Name "IsValidProfilePhoto" -Value $IsValidProfilePhoto
    $analysisOutcome | Add-Member -MemberType NoteProperty -Name "Notes" -Value $Notes

    $global:analysisOutcomes += $analysisOutcome
}

$users = m365 aad user list --properties $requiredProfileProperties -o json | ConvertFrom-Json -AsHashtable

foreach ($user in $users) {

    try {
        $userId = $user.id
        $userMail = $user.mail

        try {
            $token = m365 util accesstoken get --resource https://graph.microsoft.com --new

            try {
                $headers = New-Object "System.Collections.Generic.Dictionary[[String],[String]]"
                $headers.Add("Content-Type", "image/jpg")
                $headers.Add("Authorization", "Bearer $token")
                $userPhoto = (Invoke-RestMethod -Uri "https://graph.microsoft.com/v1.0/users/$userId/photo/`$value" -Headers $headers)

                If ($userPhoto) {

                    try {

                        $headers = New-Object "System.Collections.Generic.Dictionary[[String],[String]]"
                        $headers.Add("Content-Type", "application/json")
                        $headers.Add("Ocp-Apim-Subscription-Key", $azureVisionApiKey)

                        $analysis = (Invoke-RestMethod -Uri ("https://$azureVisionApiInstance.cognitiveservices.azure.com/vision/v3.1/analyze?visualFeatures=Categories,Adult,Tags,Description,Faces,Color,ImageType,Objects&details=Celebrities&language=en") `
                                -Headers $headers `
                                -Body ($userPhoto) `
                                -ContentType "application/octet-stream" `
                                -Method "Post");

                        if ($analysis) {
                            $analysisData = $analysis | ConvertFrom-Json -AsHashtable
                            $isPortrait = $analysisData.categories.Length -gt 0 ? ($analysisData.categories | Where-Object { $_.name -eq 'people_portrait' }).Length -gt 0  ? $true : $false : $false
                            $isPortraitValid = $photoRequirements.requirePortrait ? $isPortrait : $true
                            $isOnlyOnePersonValid = $analysisData.faces.Length -eq 1 ? $true : $false
                            $isClipartValid = $analysisData.imageType.clipArtType -eq 0 ? $true : $false
                            $isLineDrawingValid = $analysisData.imageType.lineDrawingType -eq 0 ? $true : $false
                            $isAdultValid = $photoRequirements.allowAdult ? $true : !$analysisData.adult.isAdultContent
                            $isRacyValid = $photoRequirements.allowRacy ? $true : !$analysisData.adult.isRacyContent
                            $isGoryValid = $photoRequirements.allowGory ? $true : !$analysisData.adult.isGoryContent
                            $isCelebrity = ($analysisData.categories | Where-Object { $_.detail.celebrities.Length -gt 0 }).Length -gt 0 ? $true : $false

                            $invalidKeywords = @()

                            foreach ($forbiddenKeyword in $photoRequirements.forbiddenKeywords) {
                                $isForbiddenKeywordExist = ($analysisData.tags | Where-Object { $_.name -eq $forbiddenKeyword }).Length -gt 0 ? $true : $false

                                if ($isForbiddenKeywordExist) {
                                    $invalidKeyword = New-Object -TypeName PSObject
                                    $invalidKeyword | Add-Member -MemberType NoteProperty -Name forbiddenKeyword -Value $forbiddenKeyword
                                    $invalidKeywords += $invalidKeyword
                                }
                            }

                            $isForbiddenKeywordExist = $invalidKeywords.Length -gt 0 ? $true : $false

                            $isValidProfilePhoto = $isPortraitValid `
                                -and $isOnlyOnePersonValid `
                                -and $isClipartValid  `
                                -and $isLineDrawingValid `
                                -and $isAdultValid `
                                -and $isRacyValid `
                                -and $isGoryValid `
                                -and !$isCelebrity `
                                -and !$isForbiddenKeywordExist;

                            AddAnalysisOutcome $userId `
                                $userMail `
                                $isPortraitValid `
                                $isOnlyOnePersonValid `
                                $isClipartValid `
                                $isLineDrawingValid `
                                $isAdultValid `
                                $isRacyValid `
                                $isGoryValid `
                                $isCelebrity `
                                $isForbiddenKeywordExist `
                                $isValidProfilePhoto `
                                "Profile photo available"
                        }
                    }
                    catch {
                        AddAnalysisOutcome $userId `
                            $userMail `
                            $false `
                            $false `
                            $false `
                            $false `
                            $false `
                            $false `
                            $false `
                            $false `
                            $false `
                            $false `
                            "Unable to analyze profile photo"
                    }
                }
            }
            catch {
                AddAnalysisOutcome $userId `
                    $userMail `
                    $false `
                    $false `
                    $false `
                    $false `
                    $false `
                    $false `
                    $false `
                    $false `
                    $false `
                    $false `
                    "Unable to get profile photo"
            }
        }
        catch {
            Write-Host "Unable to get new access token" -ForegroundColor Red
        }
    }
    catch {
        Write-Host "Unable to get profile details for this user" -ForegroundColor Red
    }
}

$global:analysisOutcomes | Export-Csv -Path "$outputFilePath" -NoTypeInformation
Write-Host "Open $outputFilePath to review analysis outcomes report."

Script Outcome

m365 user photo scan analysis outcome

There is no better time than the present to get your profile photo game on point. So, get working on it.

I hope this script has been helpful, and I would love to know your thoughts. Please do post them in the comments section below.

Wednesday, August 5, 2020

 

Business users struggling to find information across multiple sites (SharePoint , confluence ,  WordPress , php etc..).Client wants to single place to search for any content using SharePoint.

Implemented Solution : Integrated SharePoint search with IBM Watson search.
SharePoint search contains SharePoint sites  .
Watson search contains all other non-SharePoint sites .
Single page created on SharePoint using search site template. 









Tuesday, June 9, 2020

 

Package and Deploy Provider Hosted Apps


This article provides the steps to package and deploy Provider Hosted App.

I assume you are aware of the following:

  • Provider Hosted Environment readiness
  • IIS site setup for remote web deployment
  • Provider Hosted App Project Setup using VS 2013
  • Registering an app in the App Registration Page
  • Setting up Publishing Profile (using high trust certificate)
  • Package and deploy the .app file in App Catalog from SharePoint Project

Steps to Package & Deploy

  • Right click on the Web Project and select Publish

1-provideapppackage

  • Click Next (Note: If required update the IIS Web Application Name, ClientId, ClientSigningCertificatePath, ClientSigningCertificatePassword and IssuerId in the publishing profile. In my case, I have configured all these during the project creation)
  • In the connection tab select the Publish method as “Web Deploy Package” and update the Package location & Site name as per your environment (refer the below example). Click Next

2-provideapppackage4

  • Select “Release” in the Configuration drop down and click Publish

3-provideapppackage1

  • Now your package should be ready at the package location you have configured in the previous step

4-provideapppackage2

  • Copy the app remote web package to the remote web server you wish to deploy
  • Open the Command Prompt and traverse to the directory containing the package files
  • Run the following command:   /y
    • Example: SiteProvisioningWeb.deploy.cmd /y
  • Now the deploy command should have deployed the files in the remote web server IIS website.
  • Access the app and make sure the app is working with the logic you have written.

Tuesday, March 10, 2020

 

pass parameters to Custom Connector Actions from PowerApps 


Custom Connector that connects to Microsoft Graph REST API to get the groups in an organization. This endpoint URL and query parameters such as orderby, filter, skiptoken, top are currently hardcoded but I would like to make it generic so that I can reuse by passing arguments to the connector from the PowerApps.

Solution

To create a custom connector, you must describe the API you want to connect to so that the connector understands the API’s operations and data structures. The custom connector wizard gives you a lot of options for defining how your connector functions, and how it is exposed in apps.

On the Definition page, The Request area displays information based on the HTTP request for the action. Choose Import from sample and configure sample as shown below:

screen-shot-2019-09-12-at-6.04.52-pm

screen-shot-2019-09-12-at-6.05.53-pm

At the top right of the wizard, choose Update connector. Now that we have configured the connector, test it to make sure it’s working properly.

On the Test page, create connection and return to the Test page:

Now, enter the values for the text fields, then choose Test operation.

screen-shot-2019-09-12-at-6.06.41-pm

The connector calls the API, and you can review the response.

Return to your PowerApps app and configure your expression as show below:

That is all. The Group Collection should have the REST API response data.

screen-shot-2019-09-12-at-6.07.30-pm

Important Note: Some requests return multiple pages of data so do not pass the $skiptoken for the first call. The $skiptoken parameter contains an opaque token that references the next page of results and is returned in the URL provided in the @odata.nextLink property in the response.

Tuesday, January 14, 2020

 

Restrict Smart user’s data entry on SharePoint List associated with PowerApps


ut of the box, SharePoint provides add/edit item rights to the list as long as the user has contribute or full permission. Access policies can be scoped at the site, list and item levels. Since the user has contribute access to a list, it is possible for the user to add items from UI even if we associate a custom form using tools like PowerApps. This will mess up the custom logic or formulas we used in our PowerApps forms before saving the data in the respective list columns.

Requirement:

Restrict the smart user’s data entry on the SharePoint list which is associated with PowerApps using the out of the box approaches such as quick edit.

Solution:

Create a permission level

A quick way to create a new permission level is to make a copy of an existing permission level. We might want to do this when the existing permission level has permissions similar to what the new permission level will have. After we make the copy, we can add or remove the permissions we need the new permission level to have.

  1. On the Permission Levels page, click the Contribute permission level
  2. On the Edit Permission Level page, choose Copy Permission Level, which is at the bottom of the page after the Personal Permissions section.
  3. On the Copy Permission Level page, type a name as “Restrict Smart User Data Entry“ and description as “This is a custom permission level to restrict manual data entry.” for the new permission level.
  4. Uncheck the View Application Pages permission under List Permissions
  5. After you made the changes, click Submit.

Create Group & Add Users

  1. Click on the Settings icon and choose Site Settings from the drop-down menu.
  2. Go to Site Permissions listed under the Users and Permissions header.
  3. Click on the Create Group icon in the Grant section.
  4. Enter the necessary details in the create group page.
  5. Select the Restrict Smart User Data Entry permission level we created in the previous section.
  6. Click Create
  7. Add the necessary users in this group who can add/edit items in the list that is associated with the PowerApps App.

Configure Permission

  1. Go to the list associated with the PowerApps App
  2. Choose Settings icon and then List settings.
  3. Click Stop Inheriting Permissions to break permissions inheritance from the parent
  4. Click Grant Permissions on the Permissions tab.
  5. In the Share… dialog box, select the group created in the previous section and click share.
  6. Select the out of the box members group and click on remove user permission.
  7. We are all set, now user should not be able to open the list from the browser but can add/edit items from PowerApps app.

Thursday, September 12, 2019

Executives (Directors ,VP , SVP) wants real time date with all the Technologies available in KP  .

Implemented Solution : Developed using REST API &high charts .
All charts are built based on SharePoint & ATLAS ( ORACLE tool to store information about Technology Life Cycles) data using highcharts API
POWER BI licenses are costly, and client wants without using any tools.
Schedules Task Scheduler jobs developed using SharePoint App model to run daily.





 

Wednesday, May 8, 2019

Client different Practices and Domains working on multiple documents. They don't have any portal to upload all the documents based on their team and level of Taxonomies. Client wants a single place to publish these documents with respective tags and search for respective content easily.

Solution : Uploading documents to specific folder with metadata is not intuitive with SharePoint default upload file in doc library. Solution  is developed by using jQuery, REST Api  and custom js files to provide a  SharePoint custom page for all users and managers to achieve below activities.

Users will Publish documents with metadata and tags using custom page .

Dynamic Global Navigation for Taxonomy Levels using List , JQuery , CSOM.

It triggers workflows to respective document owners and after approval documents will be available in Search.