├── .gitignore ├── .vscode ├── launch.json └── settings.json ├── ActionCard.ps1 ├── Applications.ps1 ├── Authentication.ps1 ├── Blank.xlsx ├── ChangeLog.md ├── Examples ├── CloudDriveExport-log-and-readback.ps1 ├── Data-XLSx-Drive-dlChart.ps1 ├── Data-XLSx-Drive.ps1 ├── Demo_1-Azure Logon.ps1 ├── Demo_2-CredLogon.ps1 ├── Demo_Main.ps1 ├── Link.png ├── New-BcAuthContext.ps1 ├── OneDrive.gif ├── PlannerImportExport │ ├── Create_Planner_Template.ps1 │ ├── Export-planner-to-xlsx.ps1 │ ├── Import-Planner-From-Xlsx.ps1 │ └── Planner-Export.xlsx ├── SQL Group roles to members.ps1 ├── Team.gif ├── Template_groups.csv ├── Template_membership.csv ├── Template_users.csv └── whatsinyaml.ps1 ├── Groups.ps1 ├── Identity.DirectoryManagement.ps1 ├── Identity.SignIns.ps1 ├── LICENSE ├── Microsoft.Graph.PlusPlus.format.ps1xml ├── Microsoft.Graph.PlusPlus.psd1 ├── Microsoft.Graph.PlusPlus.psm1 ├── Microsoft.Graph.PlusPlus.settings.ps1 ├── Microsoft.Graph.PlusPlus.types.ps1xml ├── Notes.ps1 ├── OneDrive.ps1 ├── PersonalContacts.ps1 ├── Planner.ps1 ├── README.md ├── Reports.ps1 ├── Sharepoint.ps1 ├── Users.Actions.ps1 ├── Users.Functions.ps1 ├── Users.ps1 ├── docs ├── AAD-Admin-Consent-At-Logon.png ├── AAD-Admin-Consent-Needed.png ├── AAD-AppOverview.png ├── AAD-AppSecrets.png ├── App Configuration.md ├── AppPermSummary.png ├── AuthSettings.png ├── DeviceLogon.png ├── InteractiveConsent.png ├── Logon options.docx ├── Logon options.pdf ├── Relationships.pdf ├── Relationships.vsdx ├── UsersGroupsDirObjects.jpg └── VaultPerms.png └── quick_publish.ps1 /.gitignore: -------------------------------------------------------------------------------- 1 | Old/* -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "PowerShell", 9 | "request": "launch", 10 | "name": "PowerShell Interactive Session", 11 | "cwd": "" 12 | }, 13 | { 14 | "type": "PowerShell", 15 | "request": "launch", 16 | "name": "PowerShell Launch Current File", 17 | "script": "${file}", 18 | "args": [], 19 | "cwd": "${file}" 20 | }, 21 | { 22 | "type": "PowerShell", 23 | "request": "launch", 24 | "name": "PowerShell Launch Current File in Temporary Console", 25 | "script": "${file}", 26 | "args": [], 27 | "cwd": "${file}", 28 | "createTemporaryIntegratedConsole": true 29 | }, 30 | { 31 | "type": "PowerShell", 32 | "request": "launch", 33 | "name": "PowerShell Launch Current File w/Args Prompt", 34 | "script": "${file}", 35 | "args": [ 36 | "${command:SpecifyScriptArgs}" 37 | ], 38 | "cwd": "${file}" 39 | }, 40 | { 41 | "type": "PowerShell", 42 | "request": "attach", 43 | "name": "PowerShell Attach to Host Process", 44 | "processId": "${command:PickPSHostProcess}", 45 | "runspaceId": 1 46 | }, 47 | { 48 | "name": ".NET Core Attach", 49 | "type": "coreclr", 50 | "request": "attach", 51 | "processId": "${command:pickProcess}" 52 | } 53 | ] 54 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "spellright.language": [ 3 | "en" 4 | ], 5 | "spellright.documentTypes": [ 6 | "latex" 7 | ] 8 | } -------------------------------------------------------------------------------- /ActionCard.ps1: -------------------------------------------------------------------------------- 1 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Scope='Function', Target='New*', Justification='New- Commands create definitions but do not change system state')] 2 | Param() 3 | Function Convert-ColorToHex { 4 | <# 5 | .Synopsis 6 | Turns a [System.Drawing.Color] into a hex string e.g. Red to ff0000 7 | #> 8 | [cmdletbinding()] 9 | [outputType([String])] 10 | Param ( 11 | [Alias("Colour")] 12 | [System.Drawing.Color]$Color 13 | ) 14 | #return 2 hex digits for red, green and blue components 15 | "{0:x2}{1:x2}{2:x2}" -f $Color.r, $Color.g, $Color.b 16 | } 17 | 18 | Function New-CardImage { 19 | [cmdletbinding()] 20 | [Alias('CardImage')] 21 | [OutPuttype([Hashtable])] 22 | Param ( 23 | [parameter(Position=0, Mandatory=$true)] 24 | [alias('URI')] 25 | [string]$ImageURI, 26 | [string]$Title = 'Image' 27 | ) 28 | @{image = 'uri' ; title='title'} 29 | } 30 | 31 | Function New-CardSection { 32 | [CmdletBinding()] 33 | [alias('cardsection')] 34 | [OutputType([System.Collections.Specialized.OrderedDictionary])] 35 | Param ( 36 | [String]$Title = '' , 37 | [String]$ActivityImage = '' , 38 | [String]$ActivityTitle = '' , 39 | [String]$ActivitySubtitle = '' , 40 | [String]$ActivityText = '' , 41 | [bool]$StartGroup = $false , 42 | $Images ,# = @( @{image = 'uri1' ; title='title1'}, @{image = 'uri' ; title='title'}) , 43 | $Facts ,# = @( @{name1='value'}, @{name2='value2'}) , 44 | $HeroimageURI = 'HeroImageuri' , 45 | $HeroimageTitle = 'HeroImagetitle' 46 | ) 47 | $section = [ordered]@{ 48 | title = $Title 49 | startGroup = $StartGroup 50 | activityImage = $ActivityImage 51 | activityTitle = $ActivityTitle 52 | activitySubtitle = $ActivitySubtitle 53 | activityText = $ActivityText 54 | text = $Text 55 | } 56 | if ($HeroimageURI) { 57 | $heroimage = New-CardImage -ImageURI $HeroimageURI -title $HeroimageTitle 58 | $section['heroImage'] = $heroimage 59 | } 60 | if ($Facts) { $section['facts'] = $Facts } 61 | if ($Images) { $section['images'] = $Images} 62 | return $section 63 | } 64 | 65 | Function New-CardInput { 66 | <# 67 | .synopsis 68 | Creates the UI imput controls for message cards 69 | .Example 70 | > New-CardInput -InputType MultiLineText -ID 'Feedback' -Title "Let us know what you think" 71 | #Creates as a multi-line text box with the the ID "feedback" and a title 72 | 73 | #> 74 | [CmdletBinding()] 75 | [alias('cardinput')] 76 | Param ( 77 | [Parameter(ParameterSetName='SingleChoice',Mandatory=$true,Position=0)] 78 | [ValidateSet('Text','MultiLineText','DateOnly','DateTime')] 79 | [Alias('Type')] 80 | [string]$InputType, 81 | [Parameter(ParameterSetName='SingleChoice')] 82 | [int]$MaxLength, 83 | [Parameter(ParameterSetName='MultiChoice',Mandatory=$true)] 84 | [string[]]$Choices, 85 | [Parameter(ParameterSetName='MultiChoice')] 86 | [switch]$MultiSelect, 87 | [Parameter(ParameterSetName='SingleChoice',Mandatory=$true,Position=1)] 88 | [Parameter(ParameterSetName='MultiChoice',Mandatory=$true,Position=0)] 89 | [string]$ID = 'feedback' , 90 | [string]$Title, 91 | [string]$DefaultValue, 92 | [switch]$IsRequired 93 | ) 94 | if ($InputType -in ('Text','MultilineText')) { 95 | $InputControl = [ordered]@{ 96 | '@type' = 'TextInput' 97 | 'id' = $ID 98 | 'isRequired' = [bool]$IsRequired 99 | 'title' = $Title 100 | } 101 | if ($InputType -eq 'MultilineText') { $InputControl['isMultiline'] = $true } 102 | if ($MaxLength) { $InputControl['maxLength'] = $MaxLength } 103 | } 104 | elseif ($InputType -in ('DateTime','Date') ) { 105 | $InputControl = [ordered]@{ 106 | '@type' = 'DateInput' 107 | 'id' = $ID 108 | 'isRequired' = [bool]$IsRequired 109 | 'title' = $Title 110 | } 111 | if ($InputType -eq 'DateTime') {InputControl['includeTime'] = $true} 112 | } 113 | elseif ($Choices) { 114 | $InputControl = [ordered]@{ 115 | '@type' = 'MultichoiceInput' 116 | 'id' = $ID 117 | 'isRequired' = [bool]$IsRequired 118 | 'title' = $Title 119 | 'choices' = @() 120 | } 121 | if ($MultiSelect) {$InputControl['isMultiSelect'] = $true} 122 | foreach ($c in $choices) { 123 | if ($c -is [string]) { 124 | $InputControl['choices'] += @{'display' = $c ; 'value' = $c} 125 | } 126 | elseif ($c.display -and $c.value) { 127 | $InputControl['choices'] += @{'display' = $c.display ; 'value' = $c.value} 128 | } 129 | else {throw 'Invalid value for a choice.'; return} 130 | } 131 | } 132 | if ($DefaultValue) { $InputControl['value'] = $DefaultValue } 133 | return $InputControl 134 | } 135 | 136 | Function New-CardActionHttpPost { 137 | <# 138 | .Synopsis 139 | Creates an Http Post action for a message card 140 | .Description 141 | See https://docs.microsoft.com/en-gb/outlook/actionable-messages/message-card-reference#httppost-action 142 | .Example 143 | New-CardActionHttpPost -Name 'Send Feedback' -Target 'http://feedback.contoso.com' -Primary 144 | Creates a button for a form which contains which will post to the URL. 145 | need more params to be useful! 146 | #> 147 | [cmdletbinding()] 148 | [Alias("HttpPostAction")] 149 | param( 150 | [Parameter(Mandatory=$true,Position=0)] 151 | $Name , 152 | [Parameter(Mandatory=$true,Position=1)] 153 | $Target , 154 | [string[]]$Headers , 155 | $Body , 156 | [ValidateSet('application/json', 'application/x-www-form-urlencoded')] 157 | $ContentType, 158 | [switch]$Primary 159 | ) 160 | $action = [ordered]@{ 161 | '@type' = 'HttpPOST' 162 | 'name' = $Name 163 | 'target' = $target 164 | } 165 | if ($Primary) {$action['isPrimary'] = $true} 166 | if ($Headers) {$action['headers'] = $Headers} 167 | if ($Body) {$action['body'] = $Body} 168 | if ($ContentType) {$action['bodyContentType'] = $ContentType} 169 | return $action 170 | } 171 | 172 | Function New-CardActionOpenUri { 173 | <# 174 | .Synopsis 175 | Creates a button on a message card to open a link 176 | .Example 177 | New-CardActionOpenUri -Name 'Learn more' -Targets 'https://docs.microsoft.com/outlook/actionable-messages' 178 | This creates an action which a appers as button on the card [Learn More] clicking it opens the link 179 | #> 180 | [cmdletbinding()] 181 | [Alias("OpenUriAction")] 182 | param( 183 | [Parameter(Mandatory=$true,Position=0)] 184 | $Name , 185 | [Parameter(Mandatory=$true,Position=1)] 186 | $Targets , 187 | [switch]$Primary 188 | ) 189 | $Action = [ordered]@{ 190 | '@type' = 'OpenUri' 191 | 'name' = $Name 192 | 'targets' = @() 193 | } 194 | if ($Primary) {$Action['isPrimary']= $true} 195 | foreach ($t in $Targets) { 196 | if ($t -is [string]) { 197 | $Action['targets'] += @{'os' = 'default' ; 'uri' = $t} 198 | } 199 | elseif ($t.os -and $t.uri) { 200 | $Action['targets'] += @{'os' = $t.os ; 'uri' = $t.uri} 201 | } 202 | else {throw 'Invalid value for a target.'; return} 203 | 204 | } 205 | return $action 206 | } 207 | 208 | Function New-CardActionCard { 209 | <# 210 | .Synopsis 211 | Creates an "Action card" action for a message card 212 | .Description 213 | Actions are presented on the card as buttons the user can click 214 | For an "action card" action this reveals a 'sub-card' with input controls 215 | and action buttons. The buttons must be Http posts or Open URI types, they 216 | can't be nest action cards. 217 | .Example 218 | > 219 | >$inputs = @() 220 | >$inputs += New-CardInput ... 221 | >$actions = New-CardActionHttpPost -Name 'Send Feedback' -Target 'http://....' 222 | >$actioncard = New-CardActionCard -Name 'Send Feedback' -Inputs $inputs -Actions $actions 223 | This example starts by creating the input fields for the Action card 224 | It the definies a single action - the both these steps have been truncated for brevity 225 | The final step creates the action card. This will then be passed in the Actions parameter for New-Message card. 226 | The post action will usually need to send a some of the input in the body of the post 227 | see under 'Input value substitution' in https://docs.microsoft.com/en-gb/outlook/actionable-messages/message-card-reference 228 | #> 229 | [cmdletbinding()] 230 | [Alias("CardAction")] 231 | param( 232 | [Parameter(Mandatory=$true,Position=0)] 233 | $Name , 234 | $Inputs, 235 | $Actions 236 | ) 237 | [ordered]@{ 238 | '@type' = 'ActionCard' 239 | 'name' = $Name 240 | 'inputs' = @() + $Inputs 241 | 'actions' = @() + $Actions 242 | } 243 | } 244 | 245 | Function New-MessageCard { 246 | <# 247 | .Synopsis 248 | Creates a message card and either posts to a webhook or returns it for examination/tweaking. 249 | .Example 250 | > 251 | >new-messageCard -WebHookURI $hookUri -Title 'From powershell to teams using webhooks' -Text @' 252 | ![Logo](https://cdn.vsassets.io/content/notifications/teams-build-succeeded.png)**James** did a _crane job_ on the logo! 253 | '@ 254 | Creates a simple card with a title and text using mark down to display a logo. 255 | This card is posted to the Webhook in $webhooURI. 256 | .Description 257 | See https://docs.microsoft.com/en-gb/outlook/actionable-messages/message-card-reference 258 | #> 259 | [cmdletbinding()] 260 | [Alias("MessageCard")] 261 | param( 262 | #The title for the card. The font for this is fixed. 263 | [string]$Title = 'Visit the Outlook Dev Portal' , 264 | #The body text for the card. This supports markdown, which you can use to include images. 265 | [String]$Text = '', 266 | #Short version of the text. 267 | [string]$Summary = '', 268 | [system.drawing.color]$Themecolor, 269 | $ThemeColorHex ,# ='0072C6', 270 | $Actions , 271 | [switch]$AsHashTable, 272 | [String]$WebHookURI 273 | 274 | ) 275 | #Build the card as a HashTable. Make it ordered when we connvert to JSON the items don't look in a strange order 276 | $CardSettings = [ordered]@{ 277 | '@context' = 'https://schema.org/extensions' 278 | '@type' = 'MessageCard' 279 | 'title' = $Title 280 | 'text' = $Text 281 | 'summary' = $Summary 282 | } 283 | #Add optional items color is a hex string but we'll take and convert system colors. 284 | if ($ThemeColorHex) {$cardSettings['themeColor'] = $ThemeColorHex} 285 | elseif ($Themecolor) {$cardSettings['themeColor'] = Convert-ColorToHex -Color $Themecolor} 286 | if ($Actions) {$cardSettings['potentialAction'] = @() + $Actions} 287 | 288 | #And either post to a webhook or return the results. 289 | if ($AsHashTable) {return $cardSettings} 290 | elseif ($WebHookURI) { 291 | Invoke-RestMethod -Method Post -Uri $WebHookURI -ContentType "application/json" -Body (ConvertTo-Json $cardSettings -Depth 99) 292 | } 293 | else {ConvertTo-Json $cardSettings -Depth 99} 294 | } 295 | 296 | ############## Example #################### 297 | 298 | # Your Web hook here 299 | # $hookUri = 'https://outlook.office.com/webhook/<>@<>/IncomingWebhook/<>/<>' 300 | 301 | <# 302 | First we can use these with the commands being written out in max-verbose. and storing in variables 303 | $inputs = New-CardInput -InputType MultiLineText -ID 'Feedback' -Title "Let us know what you think" 304 | $actions = New-CardActionHttpPost -Name 'Send Feedback' -Target 'http://....' -Primary 305 | $actioncard = New-CardActionCard -Name 'Send Feedback' -Inputs $inputs -Actions $actions 306 | $link = New-CardActionOpenUri -Name 'Learn more' -Targets 'https://docs.microsoft.com/outlook/actionable-messages' 307 | 308 | $actions = @($link,$actionCard) 309 | $json = New-MessageCard -Actions $actions -Title 'Visit the Outlook Dev Portal' -Text 'Click **Learn More** to learn more about Actionable Messages!' 310 | 311 | #The same but using aliases and being terse with parameters. 312 | $inputs = Cardinput MultiLineText feedback -Title "Let us know what you think" 313 | $actions = HttpPostAction 'Send Feeback' 'http://....' -Primary 314 | $actioncard = CardAction 'Send Feedback' -Inputs $inputs -Actions $actions 315 | $link = OpenUriAction 'Learn more' 'https://docs.microsoft.com/outlook/actionable-messages' 316 | $json = MessageCard 'Visit the Outlook Dev Portal' 'Click **Learn More** to learn more about Actionable Messages!' -Actions $link,$actioncard 317 | 318 | #And the same again but with attitude of "We don't need no stinking variables" 319 | MessageCard -Title 'Visit the Outlook Dev Portal' -Text @' 320 | ![Logo](https://cdn.vsassets.io/content/notifications/teams-build-succeeded.png)**James** did a _crane job_ on the logo! 321 | '@ -Actions @( 322 | (CardAction "Feedback" -Inputs (Cardinput MultiLineText feedback -Title "Let us know, what do you think?") ` 323 | -Actions (HttpPostAction 'Send us feedback' 'http://....' -Primary) ) , 324 | (OpenUriAction 'Learn more' 'https://docs.microsoft.com/outlook/actionable-messages') 325 | ) | clip # paste into https://messagecardplayground.azurewebsites.net/ 326 | #> -------------------------------------------------------------------------------- /Applications.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | The Get-GraphServicePrincipal function reworks work on service principals which was published by Justin Grote at 3 | https://github.com/JustinGrote/JustinGrote.Microsoft.Graph.Extensions/blob/main/src/Public/Get-MgO365ServicePrincipal.ps1 4 | https://github.com/JustinGrote/JustinGrote.Microsoft.Graph.Extensions/blob/main/src/Public/Get-MgManagedIdentity.ps1 and 5 | https://github.com/JustinGrote/JustinGrote.Microsoft.Graph.Extensions/blob/main/src/Public/Get-MgAppRole.ps1 6 | 7 | and licensed by him under the same MIT terms which apply to this module (see the LICENSE file for details) 8 | 9 | Portions of this file are Copyright 2021 Justin Grote @justinwgrote 10 | 11 | The remainder is Copyright 2018-2021 James O'Neill 12 | #> 13 | using namespace Microsoft.Graph.PowerShell.Models 14 | function Get-GraphServicePrincipal { 15 | <# 16 | .Synopsis 17 | Returns information about Service Principals 18 | .Description 19 | A replacement for the SDK's Get-MgServicePrincipal 20 | That has orderby which doesn't work - it's in the Docs but the API errors if you try 21 | It doesn't have find by name, or select Application or Managed IDs 22 | .Example 23 | PS > Get-GraphServicePrincipal "Microsoft graph*" 24 | 25 | Id DisplayName AppId SignInAudience 26 | -- ----------- ----- -------------- 27 | 25b13fbf-2f44-457a-9e68-d3414fc97915 Microsoft Graph 00000003-0000-0000-c000-000000000000 AzureADMultipleOrgs 28 | 4e71d88a-0a46-4274-85b8-82ad86877010 Microsoft Graph Change Tracking 0bf30f3b-4a52-48df-9a82-234910c4a086 AzureADMultipleOrgs 29 | ... 30 | 31 | Run with a name the command returns service principals with matching names. 32 | .Example 33 | PS >Get-GraphServicePrincipal 25b13fbf-2f44-457a-9e68-d3414fc97915 -ExpandAppRoles 34 | 35 | Value DisplayName Enabled Id 36 | ----- ----------- ------- -- 37 | AccessReview.Read.All Read all access reviews True d07a8cc0-3d51-4b77-b3b0-32704d1f69fa 38 | AccessReview.ReadWrite.All Manage all access reviews True ef5f7d5c-338f-44b0-86c3-351f46c8bb5f 39 | ... 40 | In this example GUID for Microsoft Graph was used from the previous example, and the command has listed the roles available to applications 41 | #> 42 | 43 | [CmdletBinding(DefaultParameterSetName='List1')] 44 | [OutputType([Microsoft.Graph.PowerShell.Models.IMicrosoftGraphAppRole],ParameterSetName=('AllRoles','FilteredRoles'))] 45 | [OutputType([Microsoft.Graph.PowerShell.Models.IMicrosoftGraphPermissionScope],ParameterSetName=('AllScopes','FilteredScopes'))] 46 | [OutputType([Microsoft.Graph.PowerShell.Models.IMicrosoftGraphServicePrincipal],ParameterSetName=('Get2','List1','List2','List3','List4'))] 47 | param ( 48 | [Parameter(ParameterSetName='AllRoles', Mandatory=$true, Position=0, ValueFromPipeline=$true)] 49 | [Parameter(ParameterSetName='FilteredRoles', Mandatory=$true, Position=0, ValueFromPipeline=$true)] 50 | [Parameter(ParameterSetName='AllScopes', Mandatory=$true, Position=0, ValueFromPipeline=$true)] 51 | [Parameter(ParameterSetName='FilteredScopes', Mandatory=$true, Position=0, ValueFromPipeline=$true)] 52 | [Parameter(ParameterSetName='Get2', Mandatory=$true, Position=0, ValueFromPipeline=$true)] 53 | #The GUID(s) for ServicePrincipal(s). Or SP objects. If a name is given instead, the command will try to resolve matching Service principals 54 | $ServicePrincipalId, 55 | 56 | [Parameter(ParameterSetName='List5')] 57 | [string]$AppId, 58 | 59 | #Produces a list filtered to only managed identities 60 | [Parameter(ParameterSetName='List2')] 61 | [switch]$ManagedIdentity, 62 | 63 | #Produces a list filtered to only applications 64 | [Parameter(ParameterSetName='List3')] 65 | [switch]$Application, 66 | 67 | #Produces a convenience list of office 365 security principals 68 | [Parameter(ParameterSetName='List4')] 69 | [switch]$O365ServicePrincipals, 70 | 71 | #Select properties to be returned 72 | [Alias('Select')] 73 | [String[]]$Property, 74 | 75 | #Filters items by property values 76 | [Parameter(ParameterSetName='List1')] 77 | [String]$Filter, 78 | 79 | #Returns the list of application roles to those the role name, displayname or ID match the parameter value. Wildcards are supported 80 | [Parameter(ParameterSetName='AllRoles', Mandatory=$true)] 81 | [switch]$ExpandAppRoles, 82 | 83 | #Filters the list of application roles available within a SP 84 | [Parameter(ParameterSetName='FilteredRoles', Mandatory=$true)] 85 | [string]$AppRoleFilter, 86 | 87 | #Returns the list of (user) oauth scopes available within a SP 88 | [Parameter(ParameterSetName='AllScopes', Mandatory=$true)] 89 | [switch]$ExpandScopes, 90 | 91 | #Filters the list of oauth scopes to those where the scope name, displayname or ID match the parameter value. Wildcards are supported 92 | [Parameter(ParameterSetName='FilteredScopes', Mandatory=$true)] 93 | [string]$ScopeFilter 94 | ) 95 | begin { 96 | [String]$managedIdentityFilter = @( 97 | '00000001-0000-0000-c000-000000000000' #Azure ESTS Service 98 | '00000007-0000-0000-c000-000000000000' #Common Data Service 99 | '0000000c-0000-0000-c000-000000000000' #Microsoft App Access Panel' 100 | '00000007-0000-0ff1-ce00-000000000000' #Microsoft Exchange Online Protection 101 | '00000003-0000-0000-c000-000000000000' #Microsoft Graph 102 | '00000006-0000-0ff1-ce00-000000000000' #Microsoft Office 365 Portal 103 | '00000012-0000-0000-c000-000000000000' #Microsoft Rights Management Services 104 | '00000008-0000-0000-c000-000000000000' #Microsoft.Azure.DataMarket 105 | '00000002-0000-0ff1-ce00-000000000000' #Office 365 Exchange Online 106 | '00000003-0000-0ff1-ce00-000000000000' #Office 365 SharePoint Online 107 | '00000009-0000-0000-c000-000000000000' #Power BI Service 108 | '00000004-0000-0ff1-ce00-000000000000' #Skype for Business Online 109 | '00000002-0000-0000-c000-000000000000' #Windows Azure Active Directory 110 | ).foreach{"appId eq '$PSItem'"} -join ' or ' 111 | 112 | if ($ExpandScopes) {$ScopeFilter = '*'} 113 | if ($ExpandAppRoles) {$AppRoleFilter = '*'} 114 | 115 | $webparams = @{ 116 | AsType = ([MicrosoftGraphServicePrincipal]) 117 | ExcludeProperty = @('@odata.context','createdDateTime','disabledByMicrosoftStatus', 118 | 'keyCredentials','resourceSpecificApplicationPermissions','verifiedPublisher') 119 | Headers = @{'ConsistencyLevel'= 'Eventual'} 120 | } 121 | } 122 | process { 123 | if ( -not $ServicePrincipalId) { 124 | if ($ManagedIdentity) {$filter = "?`$filter=servicePrincipaltype eq 'ManagedIdentity'"} 125 | elseif ($Application) {$filter = "?`$filter=servicePrincipaltype eq 'Application'"} 126 | elseif ($AppId) {$filter = "?`$filter=appid eq '$AppId'" } 127 | elseif ($PSBoundParameters['Filter'] -and 128 | $O365ServicePrincipal ) {$filter = "?`$filter=( $($PSBoundParameters['Filter']) ) and $managedIdentityFilter"} 129 | elseif ($PSBoundParameters['Filter']) {$filter = "?`$filter=$($PSBoundParameters['Filter'])"} 130 | elseif ($O365ServicePrincipals) {$filter = "?`$filter=$managedIdentityFilter"} 131 | 132 | if ($Property -and $filter) {$filter += '&$select=' +($property -join ',')} 133 | elseif ($Property) {$filter = '?$select=' +($property -join ',')} 134 | Invoke-GraphRequest "$GraphUri/servicePrincipals$filter" -ValueOnly @webparams | Sort-Object displayname 135 | } 136 | else { 137 | foreach ($sp in $ServicePrincipalId) { 138 | $result = $null 139 | if ($sp -match $GUIDRegex) { 140 | $webparams['uri'] = "$GraphUri/servicePrincipals/$sp" 141 | if ($Property) {$webparams['uri'] += '?$select=' +($property -join ',')} 142 | try {$result = Invoke-GraphRequest @webparams} 143 | catch { 144 | if ($_.Exception.Response.StatusCode.value__ -eq 404) { 145 | Write-Warning "$sp was not found as a service principal ID. It may be an App ID."; continue 146 | } 147 | else {throw $_.Exception } 148 | } 149 | } 150 | else { 151 | $webparams['uri'] = "$GraphUri/servicePrincipals?`$filter=" + (FilterString $sp) 152 | if ($Property) {$webparams['uri'] += '&$select=' +($property -join ',')} 153 | $result = Invoke-GraphRequest -ValueOnly @webparams 154 | } 155 | if ($AppRoleFilter) { 156 | $(foreach ($r in $result) {$r.approles | 157 | Where-Object {$_.id -like $AppRoleFilter -or $_.DisplayName -like $AppRoleFilter -or $_.value -like $AppRoleFilter } | 158 | Add-Member -PassThru -NotePropertyName ServicePrincipal -NotePropertyValue $r.Id} ) | 159 | Sort-Object -Property Value 160 | } 161 | elseif ($ScopeFilter) { 162 | $(foreach ($r in $result) {$r.Oauth2PermissionScopes | 163 | Where-Object {$_.id -like $ScopeFilter -or $_.AdminConsentDisplayName -like $ScopeFilter -or $_.value -like $ScopeFilter } | 164 | Add-Member -PassThru -NotePropertyName ServicePrincipal -NotePropertyValue $r.Id} ) | 165 | Sort-Object -Property Value 166 | } 167 | else {$result | Sort-Object -Property displayname} 168 | } 169 | } 170 | } 171 | } 172 | 173 | function Get-GraphApplication { 174 | <# 175 | .Synopsis 176 | Returns information about Applications 177 | #> 178 | 179 | [CmdletBinding(DefaultParameterSetName='List1')] 180 | [OutputType([Microsoft.Graph.PowerShell.Models.MicrosoftGraphApplication])] 181 | param ( 182 | [Parameter(ParameterSetName='List3',Position=0)] 183 | [string]$Id, 184 | 185 | #The GUID(s) for Apps(s). Or App objects. If a name is given instead, the command will try to resolve matching App principals 186 | [Parameter(ParameterSetName='List2',ValueFromPipelineByPropertyName=$true)] 187 | [string]$AppId, 188 | 189 | #Select properties to be returned 190 | [Alias('Select')] 191 | [String[]]$Property, 192 | 193 | #Filters items by property values 194 | [Parameter(ParameterSetName='List1')] 195 | [String]$Filter 196 | ) 197 | process { 198 | $result = @() 199 | $webparams = @{ 200 | ExcludeProperty = @('verifiedPublisher', 'applicationTemplateId','addins','@odata.context') 201 | Headers = @{'ConsistencyLevel'= 'Eventual'} 202 | } 203 | if ( -not $Id) { 204 | if ($PSBoundParameters['Filter']) {$filter = "?`$filter=$($PSBoundParameters['Filter'])"} 205 | elseif ($AppId) {$filter = "?`$filter=AppID eq '$AppId'"} 206 | if ($Property -and $filter) {$filter += '&$select=' +($property -join ',')} 207 | elseif ($Property) {$filter = '?$select=' +($property -join ',')} 208 | 209 | $result += Invoke-GraphRequest "$GraphUri/Applications$filter" -ValueOnly @webparams 210 | } 211 | else { 212 | foreach ($app in $Id) { 213 | if ($app -match $GUIDRegex) { 214 | $webparams['uri'] = "$GraphUri/applications/$app" 215 | if ($Property) {$webparams['uri'] += '?$select=' +($property -join ',')} 216 | try { $result += Invoke-GraphRequest @webparams} 217 | catch { 218 | if ($_.Exception.Response.StatusCode.value__ -eq 404) { 219 | Write-Warning "$sp was not found as an ID It may be an APPID."; continue 220 | } 221 | else {throw $_.Exception } 222 | } 223 | } 224 | else { 225 | $filter = '?$filter=' + (FilterString $app) 226 | if ($Property) {$filter += '&$select=' +($property -join ',')} 227 | $result += Invoke-GraphRequest "$GraphUri/applications$filter" -ValueOnly @webparams 228 | } 229 | } 230 | } 231 | foreach ($r in $result) { 232 | foreach ($p in $r.passwordCredentials) { 233 | $p.customKeyIdentifier = [byte[]][char[]]$p.customKeyIdentifier 234 | } 235 | foreach ($k in $r.keyCredentials) { 236 | $k.customKeyIdentifier = [byte[]][char[]]$k.customKeyIdentifier 237 | } 238 | New-Object -TypeName MicrosoftGraphApplication -Property $r 239 | } 240 | } 241 | } -------------------------------------------------------------------------------- /Blank.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jhoneill/MsftGraph/22d09cb61ede5cf3f969e812f723f7d5398d8568/Blank.xlsx -------------------------------------------------------------------------------- /ChangeLog.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## 1.5.6 4 | 5 | * Modified Get-GraphUserList to send results as they are received and instead of buffering a lot of users. 6 | * Allow Get-GraphUserList to accept "none" for the -expand parameter. 7 | * Updated the validate set for user properties. 8 | * Moved hard coded parts of Export-Graph user list into parameters to allow custom exports. 9 | * Improved robustness of process that removes properties returned by API which are not properties of .NET objects. 10 | 11 | ## 1.5.5 12 | 13 | * Fixed a problem with Connect-Graph not passing dynamic parameters (like Certificate) to Connect-MgGraph 14 | * Fixed a bug in Get-GraphGroup which prevented owners of a group being returned (small typo) 15 | * Added control of user properties fetched for owners and members of groups in Get-GraphGroup 16 | * Fixed a bug which meant group membership was not being expanded correctly 17 | * Made a change to group name matching so that names which contain [ ] do not treat them as a wildcard set. 18 | 19 | ## 1.5.4 20 | 21 | * Added Support for -UseDeviceAuthentication 22 | * Fixed a bug logging on with personal accounts, and using them to access OneDrive. 23 | * Fixed a issue finding types in the .PSM1 file in some configurations 24 | * Reformatted readme to to keep markdown lint happy. 25 | 26 | ## 1.5.3 27 | 28 | * Version issues - tried to update the gallery correctly with 1.5.2 using version number 1.5.1 29 | 30 | ## 1.5.2 31 | 32 | * Fixed handling of two rest calls sending back extra properties (MicrosoftGraphChannel with TenantID) and Users Managers with authorizationInfo 33 | * Fixed DomainName completer not working 34 | * Fixed incorrect cmdlet name for getting Domain Name Reference List 35 | * Added support for Behavior Options when creating groups 36 | * Re-formatted this log to keep markdown lint happy. 37 | 38 | ## 1.5 39 | 40 | * In `Grant-GraphDirectoryRole`: allow members to tab complete and fix bug granting multiple roles. 41 | * Update photo support in `Get-GraphUser` 42 | * Fix errors from additional properties when logged on with Azure access token 43 | * Restore alias "New-RecurrencePattern" which had got lost. 44 | * Set maximum column widths when formatting group objects. 45 | * Bump version number. 46 | 47 | ## 1.4.3 48 | 49 | ### After Beta 1 50 | 51 | * Improved auto-load behaviour so `Get-GraphUser` / `Get-GraphGroup` will autoLoad the module, login with cached credentials and execute (previously needed to load the module and run `Connect-Graph`). 52 | * Additional type extensions (more to come). 53 | * Warning messages in *users* now display the UPN (were showing a type name). 54 | * Workaround `$expand=` REST parameter only expanding the first 20 directory objects. 55 | 56 | ### In Beta 1 57 | 58 | * Added this ChangeLog.md ! 59 | * Fixed breaking typo in `Import-GraphUser`. 60 | * Fixed a bug which prevented `Remove user` running. 61 | * Fixed parameter sets in `Add-GraphUser` so UPN, displayName, first name and last name can all be specified together. 62 | * Fixed bug searching for users by name. It was only searching in the `mail` field. 63 | * In `Add-GraphGroupMember` / `Remove-GraphGroupMember` handle user members already being in the desired state. 64 | * Cleaned up the import processes and example templates. 65 | * Moved Personal contact functions to their own file and load the `PersonalContacts` DLL (`Contact` is incorrectly defined in `Users` so we need the right DLL). 66 | * `Set-GraphTaskDetails` should be private - removed it from the export list in the psd1. 67 | * Changed validation of `-DefaultUsageLocation` in `Set-GraphOptions` to work round an error when reloading the module. 68 | * Added a helper function: `FilterString "bob*"` returns `startsWith(displayName,'bob')` 69 | and `FilterString bob mail` returns `displayName -eq 'bob' or mail -eq 'bob'`. 70 | * Used this helper to remove implicit wildCarding as part of a clean-up of `Get-GraphServicePrincipal`. Previously a search for 'Microsoft Graph' also returned 71 | 'Microsoft Graph Change Tracking', 'Microsoft Graph data connect Data Transfer' and 'Microsoft Graph PowerShell'. 72 | * Added `Get-GraphApplication`. 73 | * Removed implied wildcards generally. 74 | * Added `ToString()` overrides to the types.ps1xml file to show UPN for users and Display name for other things. (*implicit* `.ToString()` doesn't always work!). 75 | * Added `Set-GraphDefaultGroup`. 76 | * Added new `ChannelCompleter` class - which will work with default Group/team. So: 77 | `Set-GraphDefaultGroup Accounts` (where accounts is a team-provisioned-group) 78 | `Get-GraphChannel` \[TAB\] 79 | will autocomplete the channels for the the accounts team. 80 | * Added upn/group/team completion to parameters where the existing completers had not been added. 81 | * Added helpers `idFromGroup` and `idFromTeam` - previously the same code to say "is it a guid, an object-with-a-guid, or a name to resolve to get a guid" was repeated in many places. As part of this removed implicit wildcards from searching for groups/teams. The intention is that any of the group / team functions can accept objects with at least an ID property, strings containing the GUID, strings containing a name (with wild card support) either singly or as an array or via the pipeline and return the same result however the group was passed. 82 | * Added aliases for `Connect-Graph`: "New-GraphSession" and "GraphSession". 83 | 84 | ## 1.4.1 & 1.4.2 85 | 86 | No code changes. Fixing incorrect files bundled to the PowerShell Gallery. 87 | 88 | ## 1.4.0 (42a5e9d) 89 | 90 | The **First release targeting the [SDK](https://github.com/microsoftgraph/msgraph-sdk-powershell)** version numbers will now try to sync with the SDK version. 91 | The module has changed name to Microsoft.Graph.PlusPlus and some files have changed name accordingly. Functional changes are. 92 | 93 | * `Connect-MSGraph` is now `Connect-Graph` and is a wrapper for `Connect-MgGraph` from the Microsoft.Graph.Authentication Module (and replaces the `Connect-Graph` Alias in that module) 94 | * Settings - especially used by the extra logon modes are in a settings file - the location of which can be set by the `GraphSettingsPath` environment variable. The default settings file will also check the `GraphScopes` environment variable for scopes to request. 95 | * Where possible functions now return the objects defined in the SDK which are all in the `Microsoft.Graph.PowerShell.Models` name space. The models loads the Microsoft.Graph.xxx.private.DLL files needed to makes theses types available without enabling the all the functionality in the modules. 96 | * Functions no longer call `Invoke-RestMethod` / `Invoke-WebRequest` but call `Invoke-GraphRequest` which is a wrapper for `Invoke-MGGraphRequest` from the Microsoft.Graph.Authentication Module (and replaces the `Invoke-GraphRequest` Alias in that module). This function converts to the desired type, removing any unwanted properties and will handle getting output in multiple pages. 97 | * Functions have been moved around .ps1 files to suit the groupings used in the SDK. Some need a private DLL loaded and will skip loading if the module isn't present. 98 | 99 | ### Replaced functions 100 | 101 | * `Add-GraphChannelThread` has been replaced by `New-GraphChannelMessage` 102 | * `Connect-MSGraph` replaced by `Connect-Graph` 103 | * `Expand-GraphTask` an internal function which is now handled inside `Get-GraphTask` 104 | * `Get-GraphContactList` replaced by `Get-GraphContact` 105 | * `Get-GraphMailFolderList` replaced by `Get-GraphMailFolder` 106 | * `Get-GraphSKUList` functionality is now in `Get-GraphSKU` 107 | * `New-Recipient` has been replaced by `New-GraphRecipient` 108 | 109 | ### New functions 110 | 111 | #### Session management 112 | 113 | * `Set-GraphHomeDrive`, `Set-GraphOneNoteHome`, `Set-GraphOptions`, Test-GraphSession 114 | 115 | #### User / account management 116 | 117 | * `New-GraphUser`, `Remove-GraphUser`, `Import-GraphUser`, Export-GraphUser, Reset-GraphUserPassword 118 | * `New-GraphInvitation` (to invite external users) 119 | * `Get-GraphServicePrincipal` (More SP functions are planned) 120 | 121 | #### Group / Team management 122 | 123 | * `Import-GraphGroup`, `Import-GraphGroupMember`, Export-GraphGroupMember 124 | * `Set-GraphPlanDetails`, `Rename-GraphPlanBucket`, `Remove-GraphPlan` 125 | * `New-GraphChannelMessage`, `New-GraphChannelReply` 126 | * `Add-GraphSharePointTab` 127 | 128 | #### Directory and access management 129 | 130 | * `Get-GraphDeletedObject`, `Restore-GraphDeletedObject` 131 | * `Get-GraphDirectoryRole`, `Grant-GraphDirectoryRole`, `Revoke-GraphDirectoryRole`, `Get-GraphDirectoryRoleTemplate` 132 | * `Get-GraphLicense`, `Grant-GraphLicense`, `Revoke-GraphLicense` 133 | * `Get-GraphConditionalAccessPolicy`, `Get-GraphNamedLocation` 134 | 135 | #### App functions (Excel OneNote, Outlook, ToDo) 136 | 137 | * `Get-GraphWorkBook`, `New-GraphWorkBook`, Export-GraphWorkSheet, `Import-GraphWorkSheet` 138 | * `Copy-GraphOneNotePage` 139 | * `Move-GraphMailItem`, `Save-GraphMailAttachment`, `New-GraphMailAddress`, `New-GraphAttendee`, `New-GraphRecurrence`, `New-GraphPhysicalAddress` 140 | * `Get-GraphToDoList`, `New-GraphToDoList`, `Remove-GraphToDoList`, `New-GraphToDoTask`, `Remove-GraphToDoTask`, `Update-GraphToDoTask` 141 | 142 | ## 1.0 (172fbf2) 143 | 144 | Release as the MsftGraph module. 145 | -------------------------------------------------------------------------------- /Examples/CloudDriveExport-log-and-readback.ps1: -------------------------------------------------------------------------------- 1 | param ( 2 | $Outfile = "./clouddrive/auditrecords.json", # for cloudshell 3 | [switch]$Read, 4 | $Infile = "s:\auditrecords.json" 5 | ) 6 | ### 7 | if ($Outfile -and -not $Read) { 8 | if (-not (Get-Module -ListAvailable microsoft.graph.authentication)) {Install-Module microsoft.graph.authentication -Force } 9 | $logonResult = Connect-MgGraph -AccessToken (Get-AzAccessToken -ResourceUrl 'https://graph.microsoft.com').Token 10 | if ($logonresult -notmatch 'Welcome') {Write-Warning 'Failed to log on, bailing out.';return} 11 | $uri = 'v1.0/auditLogs/directoryAudits' # $top=100' 12 | (Invoke-MgGraphRequest -Uri $uri).value | convertto-json -depth 100 | out-file $Outfile 13 | } 14 | if ($Infile -and $Read) { 15 | $records = (ConvertFrom-Json (Get-Content s:\auditrecords.json -Raw) -AsHashtable) 16 | foreach ($r in $records) { 17 | New-Object -TypeName Microsoft.Graph.PowerShell.Models.MicrosoftGraphDirectoryAudit -Property $r | 18 | Add-Member -PassThru -MemberType ScriptProperty -Name User -Value {$this.initiatedBy.user.userPrincipalName} | 19 | Add-Member -PassThru -MemberType ScriptProperty -Name App -Value {$this.initiatedBy.App.DisplayName} 20 | } 21 | } -------------------------------------------------------------------------------- /Examples/Data-XLSx-Drive-dlChart.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .Description 3 | Creates a very simple Excel file with a chart, and uses the Graph API to upload it 4 | Then uses the Excel part of the Graph API to extract the chart as a picture, back to the local machine. 5 | A roundabout way of getting a chart as a picture from data in Powershell :-) 6 | #> 7 | 8 | Param ( 9 | $ExcelFile = 'C:\temp\tempchart.xlsx', 10 | $Destination = 'root:/Documents' 11 | 12 | ) 13 | #Remove old files 14 | Remove-Item C:\temp\graph.png, $ExcelFile -ea SilentlyContinue 15 | 16 | #Get some trivial data - the names and lengths of PowerShell files in this module. 17 | #Export it to Excel and chart it 18 | $excel = Get-ChildItem -path (Get-Module 'Microsoft.Graph.PlusPlus').ModuleBase -Recurse -Include *.ps1 | 19 | Select-Object name,length | 20 | Export-Excel -PassThru -ChartType BarClustered -AutoNameRange -Path $ExcelFile 21 | Add-ExcelChart -Worksheet $excel.Sheet1 -ChartType ColumnClustered -XRange "Name" -YRange "Length" -Column 4 -SeriesHeader "file size" 22 | Close-ExcelPackage $excel 23 | 24 | #Upload the file to current users' one drive. Open the file in Excel web app to see it 25 | $graphfile = Copy-ToGraphFolder -Path $ExcelFile -Destination $Destination 26 | Start-Process $graphfile.webUrl 27 | 28 | #We can add /workbook to the end of the URI which refers to a one drive object, so use this to find the first chart ... 29 | $chartsURI = "https://graph.microsoft.com/v1.0/me/drive/items/$($graphfile.id)/workbook/worksheets/sheet1/charts" 30 | $chartName = (Invoke-RestMethod -Uri $chartsURI -method Get -Headers @{Authorization = "Bearer $AccessToken"} ).value | 31 | Select-Object -First 1 -ExpandProperty Name 32 | 33 | #We can download the chart image - it will come down as base-64 encoded PNG, so convert it back and save it , then open the PNG 34 | $imagechars = (Invoke-RestMethod -Uri "$chartsURI/$chartName/image" -Method Get -Headers @{Authorization = "Bearer $AccessToken"} ).value -as [char[]] 35 | $imagebytes = [convert]::FromBase64CharArray($imagechars , 0 , $imagechars.Count) 36 | [System.IO.File]::WriteAllBytes("C:\temp\graph.png",$imagebytes) 37 | Start-Process C:\temp\graph.png 38 | 39 | -------------------------------------------------------------------------------- /Examples/Data-XLSx-Drive.ps1: -------------------------------------------------------------------------------- 1 | $excelPath = "$env:TEMP\Test.xlsx" 2 | Remove-Item $excelPath -ErrorAction SilentlyContinue 3 | 4 | Get-Process | Select-Object -first 50 -Property Name, CPU, PM, Handles, Company | 5 | Export-Excel $excelPath -WorkSheetname Processes ` 6 | -IncludePivotTable -PivotRows Company -PivotData PM -NoTotalsInPivot -PivotDataToColumn -Activate ` 7 | -IncludePivotChart -ChartType PieExploded3D -ShowCategory -ShowPercent -NoLegend 8 | 9 | $myTeam = Get-GraphUser -Teams | Select-Object -First 1 10 | $teamDrive = Get-GraphTeam -Team $myTeam -Drive 11 | $generalfolder = '/drives/' + $teamDrive.id + '/root:/general/' 12 | 13 | $file = Copy-ToGraphFolder -Path $excelPath -Destination $generalfolder -Verbose 14 | Start-Process $file.webUrl -------------------------------------------------------------------------------- /Examples/Demo_1-Azure Logon.ps1: -------------------------------------------------------------------------------- 1 | Import-Module Microsoft.Graph.PlusPlus 2 | Connect-Graph -fromazure <#This will fail#> 3 | Connect-AzAccount <#connect to azure and try again#> 4 | Connect-Graph -FromAzureSession 5 | GWhoAmI 6 | Get-Alias gwhoami 7 | Get-GraphUser 8 | Get-GraphUser | ft Organization # shows the organization property set 9 | Get-GraphGroup 'Accounts' # an existing teams-enabled group 10 | Get-GraphGroup 'Accounts' -Drive # #Some things don't work with an azure token. -------------------------------------------------------------------------------- /Examples/Demo_2-CredLogon.ps1: -------------------------------------------------------------------------------- 1 | Import-Module Microsoft.Graph.PlusPlus 2 | $cred = Import-Clixml ~\Documents\PowerShell\mycred.xml # created with $cred | Export-CliXML -path mycred.xml 3 | Connect-Graph -Credential $cred #this will fail 4 | 5 | edit .\Microsoft.Graph.PlusPlus.settings.ps1 #Show what's in the settings file 6 | 7 | . ..\msftgraph.safe\Microsoft.Graph.PlusPlus.settings.ps1 #use a settings file with my TenantID ClientID and ClientSecret in it. 8 | 9 | Connect-Graph -Credential $cred # Now it works 10 | GWhoAmI 11 | 12 | Get-GraphUser | Format-Table Organization 13 | 14 | Get-GraphGroup 'Consultants' -Drive # Example group which failed when we tried the azure logon 15 | Get-GraphUser -Teams | Format-Table displayname,description # Teams current users is in 16 | Get-GraphTeam 'Consultants' -site -ov myteamsite # OV so we can see the value and use it in the next command 17 | Get-GraphSite $myteamsite -Lists 18 | 19 | Get-Graphsite $myteamSite -Lists |Where-Object name -like "prob*" -ov problemslist # Example list on team site 20 | 21 | Get-GraphList $problemslist -ColumnList 22 | Get-GraphList $problemslist -Items -Property title,issuestatus,priority 23 | $teamplan = Get-GraphTeam 'Consultants' -Plans | Where-Object title -like "team*" 24 | Get-GraphPlan $teamplan -Buckets 25 | Add-GraphPlanTask -Plan $teamplan -Bucket "To Do" -Title "Submit comments on new spec" -DueDate ([datetime]::Now).AddDays(7) -Links $teamdrive.webUrl 26 | -------------------------------------------------------------------------------- /Examples/Demo_Main.ps1: -------------------------------------------------------------------------------- 1 | Get-Module | ForEach-Object {get-command -Module $_ -CommandType Cmdlet,Function} | Measure-Object | ForEach-Object count 2 | Import-Module Microsoft.Graph.PlusPlus 3 | Connect-Graph 4 | 5 | Get-Module | ForEach-Object {get-command -Module $_ -CommandType Cmdlet,Function} | Measure-Object | ForEach-Object count 6 | 7 | #Select some users and put them in a new team. For my demo I pre-set value of department => group membership 8 | $GroupName = 'Presenters' 9 | $newProjectName = "Vienna" 10 | Get-GraphUserList -Filter "Department eq '$GroupName'" -OutVariable users | Format-Table Organization 11 | New-GraphTeam -Name $GroupName -Description "The $GroupName Department" -Visibility public -Members $users -OutVariable newTeam 12 | 13 | Get-GraphTeam $newTeam -Drive -OutVariable teamdrive | Set-GraphHomeDrive ; $teamDrive 14 | #later we will add a tab in teams for this drive 15 | 16 | #special folder tab completes 17 | Get-GraphDrive -SpecialFolder Documents 18 | Get-GraphDrive / 19 | 20 | #Send a local file to onedrive, and open it -use it for exporting in a moment. 21 | Get-ChildItem $env:temp\Test*.xlsx -OutVariable files 22 | 23 | #Destination tab completes - use General for preference 24 | $files | Copy-ToGraphFolder -OutVariable item -Destination 'root:/General' 25 | 26 | Start-Process $item.webUrl 27 | 28 | #Leave the window open to see export happen - itempath tab completes - use the file from the previous - 29 | Get-GraphUserList -MembersOnly | Select-Object Organization | Export-GraphWorkSheet -SheetName sheet1 -ItemPath 'root:/General/test.xlsx' -Show 30 | 31 | #groups upgraded to teams have channels for the teams App 32 | Get-GraphTeam $newTeam -Channels -OutVariable teamFirstChannel 33 | 34 | $null = New-GraphChannelMessage -Channel $teamFirstChannel -Content "Please keep posts in 'General' to admin and questions about using the group. Use the wiki or OneNote for shared notes." 35 | 36 | #create a New channel - with its own notebook section and a planner with 3 buckets & an initial task. Make them tabs in teams. 37 | $newChannel = New-GraphChannel -Team $newTeam -Name $newProjectName -Description "For anything about project $newProjectName" 38 | #The next commnd will fail - want to make a point about that! 39 | $newTeamplan = New-GraphTeamPlan -Team $newTeam -PlanName $newProjectName 40 | #The point to make: when you create a team yoy aren't added as a member and that stops you creating the planner so add (current user is in globalVar) and go again 41 | Add-GraphTeamMember -Group $Newteam -Member $GraphUser 42 | $newTeamplan = New-GraphTeamPlan -Team $newTeam -PlanName $newProjectName 43 | Add-GraphPlanBucket -Plan $NewTeamplan -Name 'Backlog', 'To-Do','Not Doing' 44 | Add-GraphPlanTask -Plan $newTeamplan -Title "Project $newProjectName Objectives" -Bucket "To-Do" -DueDate ([datetime]::Today.AddDays(7)) -AssignTo $users[-1].Mail 45 | 46 | #Add Planner and one note to teams. 47 | Add-GraphPlannerTab -TabLabel 'Planner' -Channel $newChannel -Plan $NewTeamplan | Out-Null 48 | 49 | #Groups have a calendar - add a meeting and invite members 50 | $pattern = New-GraphRecurrence -Type weekly -DaysOfWeek wednesday -NumberOfOccurrences 52 51 | $attendees = ((Get-GraphTeam -Team $newTeam -Members) + (Get-GraphTeam -Team $newTeam -Owners ) )| New-GraphAttendee -AttendeeType optional 52 | Add-GraphEvent -Team $newTeam -Subject "Midweek team lunch" -Attendees $attendees -Start ([datetime]::Today.AddHours(12)) -End ([datetime]::Today.AddHours(12)) -Recurrence $Pattern 53 | 54 | Get-GraphTeam $newTeam -Notebooks -OutVariable teamnotebook 55 | New-GraphOneNoteSection -Notebook $teamNotebook -SectionName $newProjectName -OutVariable NewSection 56 | $NewSection | Set-GraphOneNoteHome 57 | Add-GraphOneNotePage -HTMLPage "Project $newProjectName

A default home for your notes.

" 58 | Add-GraphOneNoteTab -TabLabel 'Project Notebook' -Channel $newChannel -Notebook $Newsection 59 | 60 | $teamDrive | Add-GraphSharePointTab -TabLabel "Team Drive" -Channel $NewChannel 61 | 62 | Get-GraphTeam $newTeam -Site -OutVariable Site 63 | $cols = 'AssignedTo', 'IssueStatus', 'TaskDueDate', 'V3Comments' | ForEach-Object {Get-GraphSiteColumn -Raw -name $_} 64 | $cols += Get-GraphSiteColumn -Raw -Name 'priority' -ColumnGroup 'Core Task and Issue Columns' 65 | $newlist = New-GraphList -Name "$newProjectName Issue Tracking" -Columns $cols -Site $site -Template genericList 66 | 67 | Add-GraphListItem -List $newlist -Fields @{Title='Demo Item';IssueStatus='Active';Priority='(2) Normal';} 68 | 69 | $newlist | Add-GraphSharePointTab -Channel $NewChannel 70 | 71 | New-GraphChannelMessage -Channel $teamFirstChannel -Content "A new channel has been added for Project $newProjectName with its own planner, one note section and issues list on the team site. Take a look " 72 | 73 | Start-Process $newlist.webUrl -------------------------------------------------------------------------------- /Examples/Link.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jhoneill/MsftGraph/22d09cb61ede5cf3f969e812f723f7d5398d8568/Examples/Link.png -------------------------------------------------------------------------------- /Examples/New-BcAuthContext.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | ############ from BcContainerHelper 3 | I have added it to the examples because it shows different ways of getting an access token 4 | # -includeDeviceLogin will direct the user to a enter a code at the device login page, and it has 5 | # username + password ; refresh token, and client_id + client_secret as options. 6 | 7 | MIT License 8 | 9 | Copyright (c) Microsoft Corporation. All rights reserved. 10 | 11 | Permission is hereby granted, free of charge, to any person obtaining a copy 12 | of this software and associated documentation files (the "Software"), to deal 13 | in the Software without restriction, including without limitation the rights 14 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 15 | copies of the Software, and to permit persons to whom the Software is 16 | furnished to do so, subject to the following conditions: 17 | 18 | The above copyright notice and this permission notice shall be included in all 19 | copies or substantial portions of the Software. 20 | 21 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 22 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 24 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 26 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 27 | SOFTWARE 28 | #> 29 | 30 | function Parse-JWTtoken([string]$token) { 31 | if ($token.Contains(".") -and $token.StartsWith("eyJ")) { 32 | $tokenPayload = $token.Split(".")[1].Replace('-', '+').Replace('_', '/') 33 | while ($tokenPayload.Length % 4) { $tokenPayload += "=" } 34 | return [System.Text.Encoding]::ASCII.GetString([System.Convert]::FromBase64String($tokenPayload)) | ConvertFrom-Json 35 | } 36 | throw "Invalid token" 37 | } 38 | 39 | <# 40 | .Synopsis 41 | Function for creating a new Business Central Authorization Context 42 | .Description 43 | Function for creating a new Business Central Authorization Context 44 | The Authorization Context can be used to authenticate to a Business Central online tenant/environment in various function from ContainerHelper. 45 | The Authorization Context contains an AccessToken and you can renew the accesstoken (if necessary) by calling Renew-BcAuthContext 46 | Order of priority of OAuth2 flows: client_credentials, password, refresh_token, devicecode 47 | .Parameter clientID 48 | ClientID of AAD app to use for authentication. Default is a well known PowerShell AAD App ID (1950a258-227b-4e31-a9cf-717495945fc2) 49 | .Parameter Resource 50 | Resource used for OAuth2 flow. Default is https://api.businesscentral.dynamics.com/ 51 | .Parameter tenantID 52 | TenantID to use for OAuth2 flow. Default is Common 53 | .Parameter authority 54 | Authority to use for OAuth2 login. Default is https://login.microsoftonline.com/$TenantID 55 | .Parameter scopes 56 | Scopes to use for OAuth2 flow. Default is https://api.businesscentral.dynamics.com/.default 57 | .Parameter refreshToken 58 | If Refresh token is specified, the refresh_token flow will be included in the list of OAuth2 flows to try 59 | .Parameter clientSecret 60 | If ClientSecret is specified, the client_credentials flow will be included in the list of OAuth2 flows to try 61 | .Parameter credential 62 | If Credential is specified, the password flow will be included in the list of OAuth2 flows to try 63 | .Parameter includeDeviceLogin 64 | Include this switch if you want to include a device login prompt if no other way to authenticate succeeds 65 | .Parameter deviceLoginTimeout 66 | Timespan indicating the timeout while waiting for user to perform devicelogin. Default is 5 minutes. 67 | .Example 68 | $authContext = New-BcAuthContext -refreshToken $refreshTokenSecret.SecretValueText 69 | .Example 70 | $authContext = New-BcAuthContext -clientID $clientID -clientSecret $clientSecret 71 | .Example 72 | $authContext = New-BcAuthContext -includeDeviceLogin -deviceLoginTimeout ([TimeSpan]::FromHours(1)) 73 | #> 74 | function New-BcAuthContext { 75 | Param( 76 | [string] $clientID = "1950a258-227b-4e31-a9cf-717495945fc2", 77 | [string] $Resource = "https://api.businesscentral.dynamics.com/", 78 | [string] $tenantID = "Common", 79 | [string] $authority = "https://login.microsoftonline.com/$TenantID", 80 | [string] $refreshToken, 81 | [string] $scopes = "https://api.businesscentral.dynamics.com/.default", 82 | [SecureString] $clientSecret, 83 | [PSCredential] $credential, 84 | [switch] $includeDeviceLogin, 85 | [Timespan] $deviceLoginTimeout = [TimeSpan]::FromMinutes(5) 86 | ) 87 | 88 | $authContext = @{ 89 | "clientID" = $clientID 90 | "Resource" = $Resource 91 | "tenantID" = $tenantID 92 | "authority" = $authority 93 | "includeDeviceLogin" = $includeDeviceLogin 94 | "deviceLoginTimeout" = $deviceLoginTimeout 95 | } 96 | $subject = "$($Resource.TrimEnd('/'))/$tenantID" 97 | $accessToken = $null 98 | if ($clientSecret) { 99 | $TokenRequestParams = @{ 100 | Method = 'POST' 101 | Uri = "$($authority.TrimEnd('/'))/oauth2/v2.0/token" 102 | Body = @{ 103 | "grant_type" = "client_credentials" 104 | "scope" = $scopes 105 | "client_id" = $clientId 106 | "client_secret" = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto([System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($clientSecret)) 107 | } 108 | } 109 | try { 110 | Write-Host "Attempting authentication to $scopes using clientCredentials..." 111 | $TokenRequest = Invoke-RestMethod @TokenRequestParams -UseBasicParsing 112 | $global:TT = $TokenRequest 113 | $accessToken = $TokenRequest.access_token 114 | $jwtToken = Parse-JWTtoken -token $accessToken 115 | Write-Host -ForegroundColor Green "Authenticated as app $($jwtToken.appid)" 116 | 117 | try { 118 | $expiresOn = [Datetime]::new(1970,1,1).AddSeconds($jwtToken.exp) 119 | } 120 | catch { 121 | $expiresOn = [DateTime]::now.AddSeconds($TokenRequest) 122 | } 123 | 124 | $authContext += @{ 125 | "AccessToken" = $accessToken 126 | "UtcExpiresOn" = $expiresOn 127 | "RefreshToken" = $null 128 | "Credential" = $null 129 | "ClientSecret" = $clientSecret 130 | "scopes" = $scopes 131 | } 132 | if ($tenantID -eq "Common") { 133 | Write-Host "Authenticated to common, using tenant id $($jwtToken.tid)" 134 | $authContext.TenantId = $jwtToken.tid 135 | } 136 | 137 | } 138 | catch { 139 | $exception = $_.Exception 140 | Write-Host -ForegroundColor Red $exception.Message 141 | $accessToken = $null 142 | } 143 | } 144 | else { 145 | if ($credential) { 146 | $TokenRequestParams = @{ 147 | Method = 'POST' 148 | Uri = "$($authority.TrimEnd('/'))/oauth2/token" 149 | Body = @{ 150 | "grant_type" = "password" 151 | "client_id" = $ClientId 152 | "username" = $credential.UserName 153 | "password" = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto([System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($credential.Password)) 154 | "resource" = $Resource 155 | } 156 | } 157 | try { 158 | Write-Host "Attempting authentication to $subject using username/password..." 159 | $TokenRequest = Invoke-RestMethod @TokenRequestParams -UseBasicParsing 160 | $accessToken = $TokenRequest.access_token 161 | $jwtToken = Parse-JWTtoken -token $accessToken 162 | Write-Host -ForegroundColor Green "Authenticated from $($jwtToken.ipaddr) as user $($jwtToken.name) ($($jwtToken.upn))" 163 | 164 | $authContext += @{ 165 | "AccessToken" = $accessToken 166 | "UtcExpiresOn" = [Datetime]::new(1970,1,1).AddSeconds($TokenRequest.expires_on) 167 | "RefreshToken" = $TokenRequest.refresh_token 168 | "Credential" = $credential 169 | "ClientSecret" = $null 170 | "scopes" = "" 171 | } 172 | if ($tenantID -eq "Common") { 173 | Write-Host "Authenticated to common, using tenant id $($jwtToken.tid)" 174 | $authContext.TenantId = $jwtToken.tid 175 | } 176 | } 177 | catch { 178 | $exception = $_.Exception 179 | Write-Host -ForegroundColor Yellow $exception.Message.Replace('{EmailHidden}',$credential.UserName) 180 | $accessToken = $null 181 | } 182 | } 183 | if (!$accessToken -and $refreshToken) { 184 | $TokenRequestParams = @{ 185 | Method = 'POST' 186 | Uri = "$($authority.TrimEnd('/'))/oauth2/token" 187 | Body = @{ 188 | "grant_type" = "refresh_token" 189 | "client_id" = $ClientId 190 | "refresh_token" = $refreshToken 191 | } 192 | } 193 | try 194 | { 195 | Write-Host "Attempting authentication to $subject using refresh token..." 196 | $TokenRequest = Invoke-RestMethod @TokenRequestParams -UseBasicParsing 197 | $accessToken = $TokenRequest.access_token 198 | try { 199 | $jwtToken = Parse-JWTtoken -token $accessToken 200 | Write-Host -ForegroundColor Green "Authenticated using refresh token as user $($jwtToken.name) ($($jwtToken.upn))" 201 | $authContext += @{ 202 | "AccessToken" = $accessToken 203 | "UtcExpiresOn" = [Datetime]::new(1970,1,1).AddSeconds($TokenRequest.expires_on) 204 | "RefreshToken" = $TokenRequest.refresh_token 205 | "Credential" = $null 206 | "ClientSecret" = $null 207 | "scopes" = "" 208 | } 209 | if ($tenantID -eq "Common") { 210 | Write-Host "Authenticated to common, using tenant id $($jwtToken.tid)" 211 | $authContext.TenantId = $jwtToken.tid 212 | } 213 | } 214 | catch { 215 | $accessToken = $null 216 | throw "Invalid Access token" 217 | } 218 | } 219 | catch { 220 | Write-Host -ForegroundColor Yellow "Refresh token not valid" 221 | } 222 | } 223 | if (!$accessToken -and $includeDeviceLogin) { 224 | 225 | $deviceCodeRequest = $null 226 | $deviceLoginStart = [DateTime]::Now 227 | $accessToken = "" 228 | $cnt = 0 229 | 230 | while ($accessToken -eq "" -and ([DateTime]::Now.Subtract($deviceLoginStart) -lt $deviceLoginTimeout)) { 231 | if (!($deviceCodeRequest)) { 232 | $DeviceCodeRequestParams = @{ 233 | Method = 'POST' 234 | Uri = "$($authority.TrimEnd('/'))/oauth2/devicecode" 235 | Body = @{ 236 | "client_id" = $ClientId 237 | "resource" = $Resource 238 | "Scope" = $scopes #<== JO'N Added 239 | } 240 | } 241 | 242 | $deviceLoginStart = [DateTime]::Now 243 | Write-Host "Attempting authentication to $subject using device login..." 244 | $DeviceCodeRequest = Invoke-RestMethod @DeviceCodeRequestParams -UseBasicParsing 245 | Write-Host $DeviceCodeRequest.message -ForegroundColor Yellow 246 | Write-Host -NoNewline "Waiting for authentication" 247 | 248 | $TokenRequestParams = @{ 249 | Method = 'POST' 250 | Uri = "$($authority.TrimEnd('/'))/oauth2/token" 251 | Body = @{ 252 | "grant_type" = "urn:ietf:params:oauth:grant-type:device_code" 253 | "code" = $DeviceCodeRequest.device_code 254 | "client_id" = $ClientId 255 | } 256 | } 257 | } 258 | 259 | Start-Sleep -Seconds 1 260 | try { 261 | $TokenRequest = Invoke-RestMethod @TokenRequestParams -UseBasicParsing 262 | $accessToken = $TokenRequest.access_token 263 | } 264 | catch { 265 | $tokenRequest = $null 266 | $exception = $_ 267 | try { 268 | $err = ($exception.ErrorDetails.Message | ConvertFrom-Json).error 269 | if ($err -eq "code_expired") { 270 | Write-Host 271 | Write-Host -ForegroundColor Red "Authentication request expired." 272 | $deviceCodeRequest = $null 273 | } 274 | elseif ($err -eq "expired_token") { 275 | Write-Host 276 | Write-Host -ForegroundColor Red "Authentication token expired." 277 | throw $exception 278 | } 279 | elseif ($err -eq "authorization_declined") { 280 | Write-Host 281 | Write-Host -ForegroundColor Red "Authentication request declined." 282 | throw $exception 283 | } 284 | elseif ($err -eq "authorization_pending") { 285 | if ($cnt++ % 5 -eq 0) { 286 | Write-Host -NoNewline "." 287 | } 288 | } 289 | else { 290 | Write-Host 291 | throw $exception 292 | } 293 | } 294 | catch { 295 | Write-Host 296 | throw $exception 297 | } 298 | } 299 | if ($accessToken) { 300 | try { 301 | $jwtToken = Parse-JWTtoken -token $accessToken 302 | Write-Host 303 | Write-Host -ForegroundColor Green "Authenticated from $($jwtToken.ipaddr) as user $($jwtToken.name) ($($jwtToken.upn))" 304 | $authContext += @{ 305 | "AccessToken" = $accessToken 306 | "UtcExpiresOn" = [Datetime]::new(1970,1,1).AddSeconds($TokenRequest.expires_on) 307 | "RefreshToken" = $TokenRequest.refresh_token 308 | "Credential" = $null 309 | "ClientSecret" = $null 310 | "scopes" = "" 311 | } 312 | if ($tenantID -eq "Common") { 313 | Write-Host "Authenticated to common, using tenant id $($jwtToken.tid)" 314 | $authContext.TenantId = $jwtToken.tid 315 | } 316 | } 317 | catch { 318 | $accessToken = $null 319 | throw "Inva$lid Access token" 320 | } 321 | } 322 | } 323 | } 324 | } 325 | if (!$accessToken) { 326 | Write-Host 327 | Write-Host -ForegroundColor Yellow "Authentication failed" 328 | return $null 329 | } 330 | return $authContext 331 | } 332 | 333 | -------------------------------------------------------------------------------- /Examples/OneDrive.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jhoneill/MsftGraph/22d09cb61ede5cf3f969e812f723f7d5398d8568/Examples/OneDrive.gif -------------------------------------------------------------------------------- /Examples/PlannerImportExport/Create_Planner_Template.ps1: -------------------------------------------------------------------------------- 1 | #requires -modules Microsoft.Graph.PlusPlus, importExcel 2 | <# 3 | .synopsis 4 | Use an existing team's planner to create a .xlsx file as a template for tasks to import 5 | #> 6 | param ( 7 | #The team which owns the planner. The signed in user must be a member of the team. Being an owner but not a member will fail 8 | $TeamName = 'Consultants' , 9 | #The name of the plan to base the template on 10 | $PlanName = 'Team Planner' , 11 | #Path for the Excel file to create as the template. 12 | $ExcelPath = '.\Planner-Template.xlsx' 13 | ) 14 | 15 | $myteam = Get-GraphUser -Teams | Where-Object -Property title -eq $teamName # assumes user is 'me' 16 | $teamplanner = Get-GraphTeam $myteam -Plans | Where-Object -Property title -eq $PlanName # my team's planner named above 17 | 18 | #region export Plan buckets and team members to a "Values" sheet in the workbook, and insert categories as column headings in a "Plan" sheet" but well to right; then put the other column headings in to the left of them 19 | $excelPackage = Get-GraphPlan -Plan $teamplanner -Buckets | 20 | Select-Object @{n="BucketName"; e={$_.name}},PlanTitle,ID | 21 | Export-Excel -Path $excelPath -WorksheetName Values -ClearSheet -BoldTopRow -AutoSize -PassThru 22 | 23 | $excelPackage = Get-GraphTeam $myteam -Members | 24 | Select-Object @{n='User';e={$_.displayName}},Jobtitle,mail,ID | 25 | Export-Excel -ExcelPackage $excelPackage -worksheetname Values -StartColumn 12 -BoldTopRow -AutoSize -PassThru 26 | 27 | #Hide IDs: we can spot new team members if they don't have an ID. and if a bucket is renamed in the spreadsheet, we can update it if we have the ID 28 | Set-Excelrange -Range $excelPackage.Workbook.Worksheets['Values'].Column(15) -Hidden 29 | Set-Excelrange -Range $excelPackage.Workbook.Worksheets['Values'].Column(3) -Hidden 30 | 31 | #Now export the catgegories - create a new worksheet named 'plan' and put them on the right in the top row. 32 | $excelPackage = Get-GraphPlan $teamplanner -Details | 33 | Select-Object -ExpandProperty categorydescriptions | 34 | Export-Excel -ExcelPackage $excelPackage -WorksheetName Plan -ClearSheet -StartColumn 10 -NoHeader -BoldTopRow -FreezeTopRowFirstColumn -Activate -PassThru 35 | 36 | #put the fixed column names in on the left of the top row in the 'plan' sheet 37 | $planSheet = $excelPackage.Workbook.Worksheets['Plan'] 38 | $col = 1 ; 39 | 'Task Title' , 'Bucket' , 'Start Date', 'Due Date', '% Complete', 'Assign To', 'Check list', 'Description' ,'Links' | ForEach-Object { 40 | $Address = [OfficeOpenXml.ExcelAddress]::new(1,$col,1,$col).address 41 | $PlanSheet.Cells[$address].Value = $_ 42 | $col ++ 43 | } 44 | #endregion 45 | 46 | #region set column widths, number formats and data validation rules on the "plan" sheet 47 | Set-ExcelRange -WorkSheet $PlanSheet -Range '1:1' -Bold 48 | $PlanSheet.Cells.AutoFitColumns() 49 | Set-ExcelRange -Range $planSheet.Cells['A:A'] -Width 35 #Title 50 | Set-ExcelRange -Range $planSheet.Cells['B:B'] -Width $excelPackage.Values.Column(1).width #Make Bucket column as wide as the bucket-name column on the values sheet 51 | Set-ExcelRange -Range $planSheet.Cells['C:D'] -Width 11 -NumberFormat 'Short Date' #Format Start-date and Due-date columns as dates 52 | Set-ExcelRange -Range $planSheet.Cells['F:F'] -Width $excelPackage.Values.Column(13).width #Make Assign-To column as wide as the email-address column on the values sheet 53 | Set-ExcelRange -Range $planSheet.Cells['G:H'] -Width 20 -WrapText #Check-list and Description columns 54 | Set-ExcelRange -Range $planSheet.Cells['I:I'] -Width 35 -WrapText #Links - tried setting a smaller font but excel applies its own hyperlink style when you add one. 55 | Set-ExcelRange -Range $planSheet.cells -VerticalAlignment Center 56 | $params = @{'ShowErrorMessage'=$true; 'ErrorStyle'='stop'; 'ErrorTitle'='Invalid Data'; 'worksheet'=$planSheet } 57 | Add-ExcelDataValidationRule @params -Range 'B2:B1001' -ValidationType List -Formula 'values!$a$2:$a$1000' -ErrorBody "You must select an item from the list.`r`nYou can add to the list on the values page" #Bucket 58 | Add-ExcelDataValidationRule @params -Range 'F2:F1001' -ValidationType List -Formula 'values!$M$2:$M$1000' -ErrorBody 'You must select an item from the list' # Assign to 59 | Add-ExcelDataValidationRule @params -Range 'J2:O1001' -ValidationType List -ValueSet @('yes','YES','Yes') -ErrorBody "Enter Yes or leave blank for no" # Categories 60 | Add-ExcelDataValidationRule @params -Range 'E2:E1001' -ValidationType Integer -Operator between -Value 0 -Value2 100 -ErrorBody 'Percentage must be a whole number between 0 and 100' # Pecent complete 61 | #endregion 62 | Close-ExcelPackage $excelPackage -------------------------------------------------------------------------------- /Examples/PlannerImportExport/Export-planner-to-xlsx.ps1: -------------------------------------------------------------------------------- 1 | #requires -modules Microsoft.Graph.PlusPlus, importExcel 2 | <# 3 | .synopsis 4 | Export a team planner 5 | #> 6 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '', Justification="False positives when initializing variable in begin block")] 7 | Param ( 8 | #The path of the Excel file to create 9 | $excelPath = '.\Planner-Export.xlsx', 10 | #The team which owns the planner. The signed in user must be a member of the team. Being an owner but not a member will fail 11 | $TeamName = 'Consultants', 12 | #The name of the plan to export 13 | $PlanName = 'Team Planner' 14 | ) 15 | 16 | #need to be a member of the team, not just an owner ! 17 | $teamplanner = Get-GraphTeam $TeamName -Plans | Where-Object title -eq $PlanName # my team's planner named 'team planner' 18 | $teamMembers = Get-GraphTeam $TeamName -Members 19 | $teamMembers | ForEach-Object -begin {$MemberHash = @{}} -Process {$memberhash[$_.id] = $_.mail} 20 | 21 | #region export Plan buckets and team members to "Values" workseet in the workbook. 22 | $excelPackage = Get-GraphPlan -Plan $teamplanner -buckets | 23 | Select-Object @{n="BucketName"; e={$_.name}},PlanTitle,ID | 24 | Export-excel -path $excelPath -worksheetname Values -ClearSheet -BoldTopRow -AutoSize -PassThru 25 | 26 | $excelPackage = $teamMembers | Select-Object @{n='User';e={$_.displayName}},Jobtitle,mail,ID | 27 | Export-Excel -ExcelPackage $excelPackage -worksheetname Values -StartColumn 12 -BoldTopRow -AutoSize -PassThru 28 | #Hide IDs: we can spot new team members if they don't have an ID. and if a bucket is renamed in the spreadsheet, we can update it if we have the ID 29 | Set-Excelrange -Range $excelPackage.Workbook.Worksheets['Values'].Column(15) -Hidden 30 | Set-Excelrange -Range $excelPackage.Workbook.Worksheets['Values'].Column(3) -Hidden 31 | #endregion 32 | 33 | #region export the plan to a "Plan" worksheet in the workbook 34 | #Export the tasks, for intitially name the category columns 'category1' to 'category6' 35 | $excelPackage = Get-GraphPlan -Plan $teamplanner -FullTasks | Sort-Object orderHint | 36 | Select-Object -Property @{n='Task Title' ; e={ $_.title }}, 37 | @{n='Bucket' ; e={ $_.BucketName }}, 38 | @{n='Start Date' ; e={ [datetime]$_.StartDateTime }}, 39 | @{n='Due Date' ; e={ [datetime]$_.dueDatetime }}, 40 | @{n='%Complate' ; e={ $_.percentComplete }}, 41 | @{n='AssignTo' ; e={ ($_.assignments.psobject.properties.name | ForEach-Object {$MemberHash[$_]} )-join "; " }}, 42 | @{n="Checklist" ; e={ ($_.checklist.psobject.Properties.value | Sort-Object orderHint | Select-Object -expand title) -join "; "} }, 43 | @{n='Description'; e={ $_.description }}, 44 | @{n="Links" ; e={ ($_.references.psobject.Properties.name -replace "%2E","." -replace "%3A",":" -replace "%25","%") -join "; "}}, 45 | @{n="Category1" ; e={if($_.appliedCategories.Category1) {'Yes'} else {$null} } }, 46 | @{n="Category2" ; e={if($_.appliedCategories.Category2) {'Yes'} else {$null} } }, 47 | @{n="Category3" ; e={if($_.appliedCategories.Category3) {'Yes'} else {$null} } }, 48 | @{n="Category4" ; e={if($_.appliedCategories.Category4) {'Yes'} else {$null} } }, 49 | @{n="Category5" ; e={if($_.appliedCategories.Category5) {'Yes'} else {$null} } } , 50 | @{n="Category6" ; e={if($_.appliedCategories.Category6) {'Yes'} else {$null} } } , ID | 51 | Export-Excel -ExcelPackage $excelPackage -AutoFilter -AutoSize -FreezeTopRowFirstColumn -BoldTopRow -WorksheetName Plan -ClearSheet -Activate -PassThru 52 | 53 | #Now give the category columns the right names by exporting catgegories to the right place in the plan sheet 54 | $excelPackage = Get-GraphPlan $teamplanner -Details | 55 | Select-Object -ExpandProperty categorydescriptions | 56 | Export-Excel -ExcelPackage $excelPackage -WorksheetName Plan -noheader -StartColumn 10 -BoldTopRow -AutoSize -PassThru #Categories in columns j to o 57 | 58 | $planSheet = $excelPackage.Workbook.Worksheets['Plan'] 59 | #Hide the IDs column: a new task won't have an ID and we can find tasks to update using the ID; format dates as short date 60 | Set-ExcelRange -Range $plansheet.Column(16) -Hidden 61 | Set-ExcelRange -Range $planSheet.cells['C:D'] -Width 11 -NumberFormat 'Short Date' 62 | Set-ExcelRange -Range $planSheet.cells -VerticalAlignment Center 63 | #if name, description and/or checklist are too wide, make them narrower 64 | if ($planSheet.Column(1).Width -gt 35) { 65 | Set-ExcelRange -Range $planSheet.cells['A:A'] -Width 35 -WrapText #Title 66 | } 67 | if ($planSheet.Column(7).Width -gt 20) { 68 | Set-ExcelRange -Range $planSheet.cells['G:G'] -Width 20 -WrapText #Check list 69 | } 70 | if ($planSheet.Column(8).Width -gt 20) { 71 | Set-ExcelRange -Range $planSheet.cells['H:H'] -Width 20 -WrapText #Description 72 | } 73 | if ($planSheet.Column(9).Width -gt 35) { 74 | $linksRange = "I2:I" + $PlanSheet.Dimension.end.row 75 | Set-ExcelRange -Range $planSheet.Cells[$linksRange] -FontSize 8 76 | Set-ExcelRange -Range $planSheet.cells['I:I'] -Width 35 -WrapText #Links 77 | } 78 | #Put a data bar on the percent complete; make sure it goes 0-100 not min value to max value 79 | $PercentRange = "E2:E" + $planSheet.Dimension.End.Row 80 | $databar = Add-ConditionalFormatting -WorkSheet $planSheet -Address $PercentRange -DataBarColor LightBlue -PassThru 81 | $databar.LowValue.type = [OfficeOpenXml.ConditionalFormatting.eExcelConditionalFormattingValueObjectType]::Num 82 | $databar.HighValue.type = [OfficeOpenXml.ConditionalFormatting.eExcelConditionalFormattingValueObjectType]::Num 83 | $databar.LowValue.Value = 0 84 | $databar.HighValue.Value = 100 85 | #endregion 86 | 87 | #Create Validation rules. Bucket Name and user must come from the values page, 6 Categories must be Yes or blank, Percentage is an integer from 0 to 100 88 | if (-not (Get-Command -Name Add-ExcelDataValidationRule -ErrorAction SilentlyContinue ) ) { 89 | Write-Warning -Message 'A newer version of the ImportExcel Module is needed to add validation rules' 90 | } 91 | else { 92 | $VParams = @{WorkSheet = $PlanSheet; ShowErrorMessage=$true; ErrorStyle='stop'; ErrorTitle='Invalid Data' } 93 | Add-ExcelDataValidationRule @VParams -Range 'B2:B1001' -ValidationType List -Formula 'values!$a$2:$a$1000' -ErrorBody "You must select an item from the list.`r`nYou can add to the list on the values page" #Bucket 94 | Add-ExcelDataValidationRule @VParams -Range 'F2:F1001' -ValidationType List -Formula 'values!$M$2:$M$1000' -ErrorBody 'You must select an item from the list' # Assign to 95 | Add-ExcelDataValidationRule @VParams -Range 'J2:O1001' -ValidationType List -ValueSet @('yes','YES','Yes') -ErrorBody "Select Yes or leave blank for no" # Categories 96 | Add-ExcelDataValidationRule @VParams -Range 'E2:E1001' -ValidationType Integer -Operator between -Value 0 -Value2 100 -ErrorBody 'Percentage must be a whole number between 0 and 100' # Percent complete 97 | } 98 | Close-ExcelPackage -ExcelPackage $excelPackage -Show 99 | -------------------------------------------------------------------------------- /Examples/PlannerImportExport/Import-Planner-From-Xlsx.ps1: -------------------------------------------------------------------------------- 1 | #requires -modules Microsoft.Graph.PlusPlus, importExcel 2 | <# 3 | .synopsis 4 | Import tasks from a .xlsx file into a teams plan. 5 | #> 6 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '', Justification="False positives when initializing variable in begin block")] 7 | param ( 8 | #File to import from 9 | $excelPath = '.\planner-Import.xlsx', 10 | #The team which owns the planner. The signed in user must be a member of the team. Being an owner but not a member will fail 11 | $TeamName = 'Consultants' , 12 | #The name of the plan to import to 13 | $PlanName = 'Team Planner' 14 | ) 15 | 16 | Write-Progress -Activity 'Importing plan' -Status 'Getting information about the plan, its team, and the team members' 17 | $teamplanner = Get-GraphTeam $TeamName -Plans | Where-Object title -eq $PlanName 18 | 19 | #region ensure team members in the sheet are really in the team 20 | #Get the members of the team and create two hash tables, one to get Mail from ID and one to get ID from mail 21 | $existingteamMembers = Get-GraphTeam $TeamName -Members | Where-Object {$_.mail} 22 | $existingteamMembers | ForEach-Object -Begin {$memberMailHash = @{}; $memberIDHash = @{} } -Process { 23 | $memberMailHash[$_.mail] = $_.id 24 | if ($_.id) {$memberIDHash[$_.id] = $_.mail } 25 | } 26 | 27 | $importedTeamMembers = Import-Excel -Path $excelPath -WorksheetName values -StartColumn 12 28 | #If any team members have no ID, and mail is not in the hash of existing users ... 29 | # ...Look them up (assume for this demo mail = upn) and add them to the team. 30 | $importedTeamMembers.Where({$mail -and -not $_.id -and -not $memberMailHash[$_.Mail]}) | ForEach-Object { 31 | Write-Progress -Activity 'Importing plan' -Status "Processing new team member '$($_.mail)'" 32 | $user = $null 33 | $user = Get-GraphUser -UserID $_.mail -ErrorAction SilentlyContinue 34 | if ($user) { 35 | $_.id = $user.id 36 | Add-GraphGroupMember -Group $TeamName -Member $user 37 | } 38 | else {Write-Warning "($_.mail) Doesn't Seem to be a valid user"} 39 | } 40 | #endregion 41 | 42 | #region ensure the plan's 6 category labels match the ones in the sheet 43 | #6 category lables are at I1:N1 in the Plan sheet; Import with no header so they will be P1..P6 as properties on an object. Make that into 6 objects with a name and value 44 | Write-Progress -Activity 'Importing plan' -Status 'Checking categories' 45 | $importedCategories = (Import-Excel -path $excelPath -WorksheetName 'Plan' -NoHeader -StartColumn 10 -EndRow 1 -EndColumn 14).psobject.Properties | Sort-Object name 46 | #Transform categories returned by the server into hash table of p1..P6 --> name; to compare with the imported ones 47 | $existingCategories = Get-GraphPlan $teamplanner -Details | Select-Object -ExpandProperty categorydescriptions 48 | $existingCategories.psobject.Properties | ForEach-Object -Begin {$catHash = @{} } -Process {$catHash[($_.name -replace 'category','p')] = $_.value} 49 | 50 | #for our 6 imported categories check them against the corresponding entry in catHash; if different pop in a hash table that can be splatted into Set-GraphplanDetails 51 | $importedCategories.where({$catHash[($_.name)] -ne $_.value}) | 52 | ForEach-Object -Begin {$newCategories= @{} } -Process { $newCategories[($_.name -replace 'p','Category')] = $_.value} 53 | if ($newCategories.Count -gt 0) { 54 | Write-Progress -Activity 'Importing plan' -Status 'Updating categories' 55 | Set-GraphPlanDetails $teamplanner @newCategories 56 | } 57 | #endregion 58 | 59 | #region ensure buckets in the the sheet are in the plan 60 | #Get buckets from the server and make a hash of name--> ID , and import the bucket list from the Values sheet 61 | Write-Progress -Activity 'Importing plan' -Status 'Checking Buckets' 62 | $existingBuckets = Get-GraphPlan $teamplanner -buckets 63 | $existingBuckets | foreach-object -Begin {$bucketHash = @{}} -Process {$bucketHash[$_.Name] = $_.id} 64 | $importedBuckets = Import-Excel -path $excelPath -WorksheetName values -StartColumn 1 -EndColumn 3 65 | 66 | #NewBuckets here is new or changed buckets. We don't cope with bucket A being renamed, and then bucket B being changed to "A" 67 | $newbuckets = $importedBuckets.where({-not $bucketHash[$_.bucketName]}) 68 | 69 | #Buckets with an ID but no match in the hash table must have been reanmed, and those without an ID are new... 70 | foreach ($bucket in $newbuckets.where({$_.id}) ) { 71 | Write-Progress -Activity 'Importing plan' -Status 'Renaming bucket to ' -CurrentOperation $bucket.bucketName 72 | Rename-GraphPlanBucket -Bucket $bucket.id -NewName $bucket.bucketName 73 | $bucketHash[$bucket.bucketName] = $bucket.id 74 | } 75 | foreach ($bucket in $newbuckets.where({-not $_.id}) ) { 76 | Write-Progress -Activity 'Importing plan' -Status 'Adding new bucket ' -CurrentOperation $bucket.bucketName 77 | $newbucket = New-GraphPlanBucket -Plan $teamPlanner -Name $bucket.bucketName 78 | $bucketHash[$newbucket.Name] = $newbucket.id 79 | } 80 | #endregion 81 | 82 | Write-Progress -Activity 'Importing plan' -Status 'Checking tasks' 83 | #region get existing tasks - fiddle the results so they look like what we export and we are going to import next. 84 | $existingTasks = Get-GraphPlan $teamplanner -FullTasks | 85 | Sort-Object -Property ID| 86 | Select-Object -Property @{n='Title' ; e={ $_.title }}, 87 | @{n='Bucket' ; e={ $_.BucketName }}, 88 | @{n='StartDate' ; e={ [datetime]$_.StartDateTime }}, 89 | @{n='DueDate' ; e={ [datetime]$_.dueDatetime }}, 90 | @{n='PercentComplete'; e={ $_.percentComplete }}, 91 | @{n='AssignTo' ; e={ ($_.assignments.psobject.properties.name) -join "; " }}, 92 | @{n="Checklist" ; e={ ($_.checklist.psobject.Properties.value | sort-object orderHint | Select-Object -expand title) -join "; "} }, 93 | @{n='Description' ; e={ $_.description }}, 94 | @{n="Links" ; e={ ($_.references.psobject.Properties.name -replace "%2E","." -replace "%3A",":" -replace "%25","%") -join "; "}}, 95 | @{n="Category1" ; e={if($_.appliedCategories.Category1) {'Yes'} else {$null} } }, 96 | @{n="Category2" ; e={if($_.appliedCategories.Category2) {'Yes'} else {$null} } }, 97 | @{n="Category3" ; e={if($_.appliedCategories.Category3) {'Yes'} else {$null} } }, 98 | @{n="Category4" ; e={if($_.appliedCategories.Category4) {'Yes'} else {$null} } }, 99 | @{n="Category5" ; e={if($_.appliedCategories.Category5) {'Yes'} else {$null} } } , 100 | @{n="Category6" ; e={if($_.appliedCategories.Category6) {'Yes'} else {$null} } } , 101 | @{n="Task" ; e={$_.id } } 102 | 103 | Write-Progress -Activity 'Importing plan' -Completed # it isn't from the progress message point of view it is. 104 | $existingTasks | foreach-object -Begin {$taskHash = @{}} -Process {$taskHash[$_.task] = $true } 105 | #endregion 106 | # 107 | Import the tasks from the sheet. The Category names will be customized so use our own header names, so make them usable, And pull other columns to match add-GraphPlanTask , Set-GraphPlanTask commands 108 | $importedTasks = Import-Excel -Path $excelPath -WorksheetName 'Plan' -HeaderName Title, Bucket, StartDate, DueDate, PercentComplete, AssigneeMail, Checklist, Description, Links, Category1, Category2, Category3, Category4, Category5, Category6,Task | 109 | ForEach-Object { 110 | if ($_.AssigneeMail) {Add-Member -InputObject $_ -MemberType NoteProperty -Name AssignTo -Value $memberMailHash[$_.AssigneeMail]} 111 | else {Add-Member -InputObject $_ -MemberType NoteProperty -Name AssignTo -Value ""} 112 | Add-Member -InputObject $_ -MemberType NoteProperty -Name CategoryNumbers -value $(foreach ($n in (1..6)) {if ($_."Category$n" -eq 'Yes') {$n} }) -PassThru 113 | } | Sort-Object -Property Task 114 | 115 | #If a task has no ID it is new, so add it. Check it has a title so if blank rows were imported, we don't try to process them 116 | $importedTasks.Where({$_.title -and -not $_.task}) | Add-GraphPlanTask -Plan $teamplanner 117 | 118 | #$existingTasks and Imported tasks can be compared - so compare them, show the results in Gridview to allow the user to see any which should not be modified 119 | $propsToCompare= @('Task', 'Title', 'Bucket', 'StartDate', 'DueDate', 'PercentComplete', 'assignto', 'Checklist', 'Description', 'Links', 'Category1', 'Category2', 'Category3', 'Category4', 'Category5', 'Category6') 120 | $comparison = Compare-Object $existingTasks $importedTasks.Where({$_.Task -and $taskhash[$_.Task]}) -Property $propsToCompare | Sort-Object Task,sideindicator 121 | $comparison | Select-Object @{n='Side';e={if ($_.sideindicator -eq "<=") {'Existing'} else {'Import'}}}, 'Title', 'Bucket', 'StartDate', 'DueDate', 122 | 'PercentComplete', @{n='Asignee';e={$memberIDHash[$_.assignto]}}, 'Checklist', 'Description', 'Links', 123 | 'Category1', 'Category2', 'Category3', 'Category4', 'Category5', 'Category6' | Out-GridView -Title 'Review the changes - import may overwrite newer data on the server' 124 | 125 | $Comparison.Where({$_.sideindicator -eq '=>'}) | Set-GraphPlanTask -Confirm 126 | 127 | -------------------------------------------------------------------------------- /Examples/PlannerImportExport/Planner-Export.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jhoneill/MsftGraph/22d09cb61ede5cf3f969e812f723f7d5398d8568/Examples/PlannerImportExport/Planner-Export.xlsx -------------------------------------------------------------------------------- /Examples/SQL Group roles to members.ps1: -------------------------------------------------------------------------------- 1 | Param ( 2 | $server = "tcp:amsqlprod.database.windows.net,1433", 3 | $databases = @("sqldb-amDev-moj","sqldb-amSIT-moj","sqldb-amUAT-moj"), 4 | $id = "amsqladmin", 5 | $pass 6 | ) 7 | $sql = @" 8 | SELECT DP1.name AS DatabaseRoleName, DP2.name AS DatabaseUserName ,Dp2.sid AS SID 9 | FROM sys.database_role_members AS DRM 10 | JOIN sys.database_principals AS DP1 ON DRM.role_principal_id = DP1.principal_id 11 | JOIN sys.database_principals AS DP2 ON DRM.member_principal_id = DP2.principal_id 12 | WHERE DP1.type = 'R' 13 | AND DP2.authentication_type_desc ='EXTERNAL' 14 | "@ 15 | $grouplist = Get-GraphGroup | Add-Member -MemberType ScriptProperty -Name SQLID -Value { ([guid]::Parse($this.id).ToByteArray() | ForEach-Object {$_.tostring("x2")}) -join ""} -PassThru 16 | $roles = foreach ($db in $databases) { 17 | $connectionString = "Server=$server;Initial Catalog=$db;Persist Security Info=False;User ID=$id;Password=$(Get-AzKeyVault | Get-AzKeyVaultSecret -Name saPwd -AsPlainText);MultipleActiveResultSets=False;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;" 18 | Get-SQL -ForceNew -MsSQLserver -Connection $connectionString -SQL $sql -Close | ForEach-Object { 19 | $sqlRole = $_.DatabaseRoleName 20 | $sqlId = ($_.sid | ForEach-Object {$_.tostring('x2') }) -join "" 21 | $grouplist | Where-Object sqlid -eq $sqlid | 22 | Get-GraphGroup -Members | 23 | Select-Object @{n="DatabaseName";e={$db}} , @{n="DatabaseRole";e={$sqlrole}}, GroupName,Displayname,UserPrincipalName,Mail 24 | } 25 | } 26 | 27 | $roles | Sort-Object -Property DatabaseName,DatabaseRole,GroupName, DisplayName | Format-Table #or what you will 28 | -------------------------------------------------------------------------------- /Examples/Team.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jhoneill/MsftGraph/22d09cb61ede5cf3f969e812f723f7d5398d8568/Examples/Team.gif -------------------------------------------------------------------------------- /Examples/Template_groups.csv: -------------------------------------------------------------------------------- 1 | Action,DisplayName,Description,Type,Visibility 2 | Remove,Test99,,, 3 | Add,Test111a,Test Group 111,,Private 4 | Add,Test222b,Testing,Team,Private 5 | Remove,Test333C,Another Test Group,Security,Private 6 | Add,Test222D,Further Testing,Team, 7 | Add,Test333E,Another Test Group,Security, 8 | -------------------------------------------------------------------------------- /Examples/Template_membership.csv: -------------------------------------------------------------------------------- 1 | Action,MemberOf,UserPrincipalName,DisplayName 2 | Add,Consultants,ruth@mobulaconsulting.com,Ruth Thatcher 3 | Remove,Consultants,robin@mobulaconsulting.com,Anything can appear in Display name 4 | Remove,Consultanting,bob@mobulaconsulting.com,Bob Cratchett 5 | -------------------------------------------------------------------------------- /Examples/Template_users.csv: -------------------------------------------------------------------------------- 1 | Action,UserPrincipalName,MailNickname,GivenName,Surname,DisplayName,UsageLocation,AccountDisabled,PasswordPolicies,Mail,MobilePhone,BusinessPhones,Manager,JobTitle,Department,Groups,Roles,Licenses,OfficeLocation,CompanyName,StreetAddress,City,State,Country,PostalCode 2 | Set,Jacob@mobulaconsulting.com,,,,,,TRUE,,,,,,,,,,,,,,,,, 3 | Add,Helena@mobulaconsulting.com,Helena,Helena,Handcart,Helena Handcart,GB,FALSE,,Helena@mobulaconsulting.com,,,Jacob@mobulaconsulting.com,,Accounts,,,,Oxford,,,,,, 4 | Add,Carol@mobulaconsulting.com,Carol,Carol,Singer,Carol Singer,GB,,,Carol@mobulaconsulting.com,07776 543 210,01865 234567,,Musicologist,Legal,Consultants;Accounts,Directory Readers,POWER_BI_STANDARD,,,High St,Oxford,OXON,UK,OX1 1AA 5 | Add,Ruth@mobulaconsulting.com,Ruth,Ruth,Thatcher,Ruth Thatcher,GB,,,Ruth@mobulaconsulting.com,,,,,Legal,,,,,Reed and Rite Roofs,,,,, 6 | Remove,Doug@mobulaconsulting.com,,Doug,Graves,,,,,,,,,,,,,,,,,,,, 7 | Add,Robin@mobulaconsulting.com,,Robin,Banks,,,,,,,,,Grounds keeper,Facilities,Consultants;Accounts,Directory Readers,POWER_BI_STANDARD,,,,,,, 8 | -------------------------------------------------------------------------------- /Examples/whatsinyaml.ps1: -------------------------------------------------------------------------------- 1 | push-location C:\Users\P10506111\downloads\msgraph-sdk-powershell-dev\openApiDocs\v1.0\ 2 | $lines = (Get-Content -raw *.yml | Measure-Object -line).Lines 3 | $files = (Get-ChildItem *.yml | Measure-Object).Count 4 | $paths = @{} 5 | $schemas = @{} 6 | $methods = @() 7 | Get-ChildItem *.yml | ForEach-Object { 8 | $y = ConvertFrom-Yaml (Get-Content $_ -Raw) 9 | foreach ($k in $y.paths.keys) {$paths[$k]= $true ; $methods += $y.paths[$k].Keys} 10 | foreach ($k in $y.components.schemas.keys) {$schemas[$k]= $true} 11 | } 12 | "$files files, of $lines lines defining $($schemas.keys.count) objects and $($paths.Keys.Count) RestAPI Paths" 13 | $methods | Group-Object -NoElement -------------------------------------------------------------------------------- /Identity.SignIns.ps1: -------------------------------------------------------------------------------- 1 | using namespace Microsoft.Graph.PowerShell.Models 2 | #MicrosoftGraphInvitation object is in Microsoft.Graph.Identity.SignIns.private.dll 3 | function New-GraphInvitation { 4 | <# 5 | .synopsis 6 | Invites an external user to become a guest in Azure AD 7 | #> 8 | [cmdletbinding(SupportsShouldProcess=$true)] 9 | param ( 10 | #The email address of the user being invited. 11 | #The characters #~ ! $ % ^ & * ( [ { < > } ] ) + = \ / | ; : " " ? , are not permitted 12 | #A . or - is permitted except at the beginning or end of the name. A _ is permitted anywhere. 13 | [Parameter(Position=0,ValueFromPipeline=$true)] 14 | [string]$EmailAddress, 15 | #The display name of the user being invited. 16 | [string]$DisplayName, 17 | #The userType of the user being invited. By default, this is Guest. You can invite as Member if you are a company administrator.' 18 | [string]$UserType, 19 | #The URL the user should be redirected to once the invitation is redeemed. Required. 20 | [string]$RedirectUrl = 'https://mysignins.microsoft.com/', 21 | #Indicates whether an email should be sent to the user being invited or not. 22 | [switch]$SendInvitationMessage 23 | ) 24 | 25 | ContextHas -WorkOrSchoolAccount -BreakIfNot 26 | $settings = @{ 27 | 'invitedUserEmailAddress' = $EmailAddress 28 | 'sendInvitationMessage' = $SendInvitationMessage -as [bool] 29 | 'inviteRedirectUrl' = $RedirectUrl 30 | } 31 | if ($DisplayName) {$settings['invitedUserDisplayName'] = $DisplayName} 32 | if ($UserType) {$settings['invitedUserType'] = $UserType} 33 | 34 | $webparams = @{ 35 | 'Method' = 'POST' 36 | 'Uri' = "$GraphUri/invitations" 37 | 'Contenttype' = 'application/json' 38 | 'Body' = (ConvertTo-Json $settings -Depth 5) 39 | 'AsType' = [MicrosoftGraphInvitation] 40 | 'ExcludeProperty' = '@odata.context' 41 | } 42 | Write-Debug $webparams.Body 43 | if ($force -or $pscmdlet.ShouldProcess($EmailAddress, 'Invite User')){ 44 | try { 45 | $u = Invoke-GraphRequest @webparams 46 | if ($Passthru ) {return $u } 47 | } 48 | catch { 49 | # xxxx Todo figure out what errors need to be handled (illegal name, duplicate user) 50 | $_ 51 | } 52 | } 53 | } 54 | 55 | function Get-GraphNamedLocation { 56 | <# 57 | .synopsis 58 | Returns named locations used in conditional access 59 | #> 60 | param ( 61 | #The ID or start of the display-name of a Location 62 | [Parameter(ValueFromPipeline=$true,Position=0)] 63 | $Location = "" 64 | ) 65 | process { 66 | $uri = "$GraphUri/identity/conditionalAccess/namedlocations" 67 | foreach ($l in $Location) { 68 | if ($l -match $GUIDRegex) { 69 | $response = Invoke-GraphRequest "$uri/$l" -PropertyNotMatch '@odata' 70 | } 71 | elseif ($l) { 72 | $response = Invoke-GraphRequest -ValueOnly ($uri + "?`$filter=startswith(toLower(displayName),'{0}')" -f $l.ToLower()) 73 | } 74 | else{ 75 | $response = Invoke-GraphRequest -ValueOnly $uri 76 | } 77 | foreach ($r in $response) { 78 | if ($r.isTrusted -is [bool]) { 79 | $trusted = $r.isTrusted 80 | $null = $r.Remove('isTrusted') 81 | } 82 | else {$trusted = $null} 83 | if ($r.ipRanges) { 84 | $ipranges = $(foreach ($ipRange in $r.ipRanges) {$ipRange.cidrAddress}) -join ";" 85 | $null = $r.Remove('ipranges') 86 | } 87 | else {$ipranges = $null} 88 | if ($r.countriesAndRegions) { 89 | $countries = $r.countriesAndRegions -join ";" 90 | $null = $r.Remove('countriesAndRegions') 91 | } 92 | else {$countries = $null} 93 | if ($r.includeUnknownCountriesAndRegions -is [bool]) { 94 | $includeUnknown = $r.includeUnknownCountriesAndRegions 95 | $null = $r.Remove('includeUnknownCountriesAndRegions') 96 | } 97 | else {$includeUnknown = $null } 98 | 99 | $null = $r.remove('@odata.type'), $r.remove('@odata.id') 100 | New-object -TypeName Microsoft.Graph.PowerShell.Models.MicrosoftGraphNamedLocation -Property $r | 101 | Add-Member -PassThru -NotePropertyName IsTrusted -NotePropertyValue $trusted | 102 | Add-Member -PassThru -NotePropertyName IpRanges -NotePropertyValue $ipranges | 103 | Add-Member -PassThru -NotePropertyName CountriesAndRegions -NotePropertyValue $countries | 104 | Add-Member -PassThru -NotePropertyName IncludeUnknownCountriesAndRegions -NotePropertyValue $includeUnknown 105 | } 106 | } 107 | } 108 | } 109 | 110 | function Get-GraphConditionalAccessPolicy { 111 | param ( 112 | #The ID or start of the display-name of a Policy 113 | [Parameter(ValueFromPipeline=$true,Position=0)] 114 | $Policy = "" 115 | ) 116 | process { 117 | #needs beta to work when conditions.devices is set (which is marked as preview in the GUI) 118 | $uri = "beta/identity/conditionalAccess/policies" 119 | foreach ($p in $Policy) { 120 | if ($p -match $GUIDRegex) { 121 | Invoke-GraphRequest "$uri/$p" -AsType ([MicrosoftGraphConditionalAccessPolicy]) -PropertyNotMatch '@odata' 122 | } 123 | elseif ($P) { 124 | $uri += ("?`$filter=startswith(toLower(displayName),'{0}')" -f $p.ToLower()) 125 | Invoke-GraphRequest -ValueOnly $uri -AsType ([MicrosoftGraphConditionalAccessPolicy]) 126 | } 127 | else{ Invoke-GraphRequest -ValueOnly $uri -AllValues -AsType ([MicrosoftGraphConditionalAccessPolicy]) } 128 | } 129 | } 130 | } 131 | 132 | function Expand-GraphConditionalAccessPolicy { 133 | param ( 134 | #The ID or start of the display-name of a Policy 135 | [Parameter(ValueFromPipeline=$true,Position=0)] 136 | $Policy = "" 137 | ) 138 | begin { 139 | $locations = @{} 140 | Invoke-GraphRequest "$GraphUri/identity/conditionalAccess/namedlocations" -ValueOnly -AllValues | 141 | ForEach-Object {$locations[$_.id] = $_.DisplayName} 142 | $dirRoleTemplates = @{} ; 143 | Invoke-GraphRequest -Uri "$GraphUri/directoryroletemplates/" -ValueOnly -AllValues | 144 | ForEach-Object {$dirRoleTemplates[$_.id] = $_.DisplayName} 145 | $servicePrincipals = @{} 146 | Invoke-GraphRequest -Uri "$GraphUri/servicePrincipals/" -ValueOnly -AllValues | 147 | ForEach-Object {$servicePrincipals[$_.id] = $_.Displayname} 148 | 149 | $dirobjs = @{} 150 | $result = @() 151 | function resolveDirObj { 152 | param ( 153 | [Parameter(ValueFromPipeline=$true)] $D 154 | ) 155 | process { 156 | if ($D -notmatch $GUIDRegex ) {return $d} 157 | elseif (-not $dirobjs.ContainsKey($D)) { 158 | $dirobjs[$d] = (Invoke-GraphRequest "$GraphUri/directoryObjects/$d").displayname 159 | } 160 | $dirobjs[$d] 161 | } 162 | } 163 | function tranlasteGUID { 164 | param ( 165 | [Parameter(ValueFromPipeline=$true)]$G, 166 | [Parameter(Position=0)]$hash 167 | ) 168 | process {if ($g -notmatch $GUIDRegex ) {return $g} else {$hash[$g]}} 169 | } 170 | } 171 | process { 172 | Get-GraphConditionalAccessPolicy @PSBoundParameters | 173 | Select-Object -Property DisplayName , Description, State, 174 | @{n='IncludeUsers'; e={($_.Conditions.Users.IncludeUsers | resolveDirObj ) -join '; '}}, 175 | @{n='ExcludeUsers'; e={($_.Conditions.Users.ExcludeUsers | resolveDirObj ) -join '; '}}, 176 | @{n='IncludeGroups'; e={($_.Conditions.Users.IncludeGroups | resolveDirObj ) -join '; '}}, 177 | @{n='ExcludeGroups'; e={($_.Conditions.Users.ExcludeGroups | resolveDirObj ) -join '; '}}, 178 | @{n='IncludeRoles'; e={($_.Conditions.Users.IncludeRoles | tranlasteGUID $dirRoleTemplates) -join '; '}}, 179 | @{n='ExcludeRoles'; e={($_.Conditions.Users.ExcludeRoles | tranlasteGUID $dirRoleTemplates) -join '; '}}, 180 | @{n='IncludeLocations'; e={($_.Conditions.Locations.IncludeLocations | translateGuid $locations) -join '; '}}, 181 | @{n='ExcludeLocations'; e={($_.Conditions.Locations.ExcludeLocations | translateGuid $locations) -join "; "}}, 182 | @{n='IncludeApplications'; e={($_.Conditions.Applications.IncludeApplications | tranlasteGUID $servicePrincipals) -join '; '}}, 183 | @{n='ExcludeApplications'; e={($_.Conditions.Applications.ExcludeApplications | tranlasteGUID $servicePrincipals) -join '; '}}, 184 | @{n='IncludeUserActions'; e={ $_.Conditions.Applications.IncludeUserActions -join '; '}}, 185 | @{n='IncludeDevices'; e={ $_.Conditions.Devices.IncludeDevices -join '; '}}, 186 | @{n='ExcludeDevices'; e={ $_.Conditions.Devices.ExcludeDevices -join '; '}}, 187 | @{n='ClientAppTypes'; e={ $_.Conditions.ClientAppTypes -join '; '}}, 188 | @{n='IncludePlatforms'; e={ $_.Conditions.Platforms.IncludePlatforms -join '; '}}, 189 | @{n='ExcludePlatforms'; e={ $_.Conditions.Platforms.EccludePlatforms -join '; '}}, 190 | @{n='AppEnforcedRestrictions'; e={ $_.SessionControls.ApplicationEnforcedRestrictions.IsEnabled}}, 191 | @{n='PersistentBrowser'; e={ 192 | if ($_.SessionControls.PersistentBrowser.isenabled) { 193 | $_.SessionControls.PersistentBrowser.Mode} 194 | else {$null}}}, 195 | @{n='CloudAppSecurity'; e={ 196 | if ($_.SessionControls.CloudAppSecurity.isenabled) { 197 | $_.SessionControls.CloudAppSecurity.CloudAppSecurityType} 198 | else {$null}}} , 199 | @{n='SignInFrequency'; e={ 200 | if ($_.SessionControls.SignInFrequency.isenabled) { 201 | $_.SessionControls.SignInFrequency.Value + " " + 202 | $_.SessionControls.SignInFrequency.Type } 203 | else {$null}}} 204 | } 205 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 James O'Neill 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Microsoft.Graph.PlusPlus.psd1: -------------------------------------------------------------------------------- 1 | @{ 2 | Copyright = '(c) 2021-3 James O''Neill. All rights reserved.' 3 | Author = 'James O''Neill' 4 | CompanyName = 'Mobula Consulting' 5 | ModuleVersion = '1.5.6' 6 | PrivateData = @{ 7 | PSData = @{ 8 | Tags = @('MicrosoftGraph', 'Microsoft', 'Office365', 'Graph', 'PowerShell', 'AzureAD', 'OneNote', 'OneDrive', 'Outlook', 'Sharepoint', 'Planner', 'MSGraph') 9 | Category = 'Functions' 10 | ProjectUri = 'https://github.com/jhoneill/MsftGraph' 11 | LicenseUri = 'https://github.com/jhoneill/MsftGraph/blob/master/LICENSE' 12 | } 13 | } 14 | Description = @' 15 | Module to work the Microsoft Graph API using both AzureAD 'work or school' accounts and 'personal' Microsoft accounts 16 | it contains over 100 functions to 17 | * Navigate, upload to and download from OneDrive 18 | * Navigate and manipulate Pages & Sections in OneNote Notebooks 19 | * Create and manage Groups and Teams, and post to Teams and their Channels 20 | * Create, read and write Plans for Teams. 21 | * Read and write Calendar-events and Contacts in Outlook, 22 | * Read and send Mail messages in Outlook 23 | * Work with Sharepoint Lists 24 | * Access ToDo Lists. 25 | '@ 26 | CompatiblePSEditions = @('Core', 'Desktop') 27 | PowerShellVersion = '5.1' 28 | GUID = 'f564c0f9-7d96-4452-a715-679dc47c20cc' 29 | RootModule = '.\Microsoft.Graph.PlusPlus.psm1' 30 | RequiredModules = @(@{ModuleName = 'Microsoft.Graph.Authentication'; ModuleVersion = '1.4.0'; }) 31 | FormatsToProcess = 'Microsoft.Graph.PlusPlus.format.ps1xml' 32 | TypesToProcess = @('Microsoft.Graph.PlusPlus.types.ps1xml') 33 | FunctionsToExport = @('Add-FileToGraphOneNote', 34 | 'Add-GraphEvent', 35 | 'Add-GraphGroupMember', 36 | 'Add-GraphGroupThread', 37 | 'Add-GraphListItem', 38 | 'Add-GraphOneNotePage', 39 | 'Add-GraphOneNoteTab', 40 | 'Add-GraphPlanBucket', 41 | 'Add-GraphPlannerTab', 42 | 'Add-GraphPlanTask', 43 | 'Add-GraphSharePointTab', 44 | 'Add-GraphWikiTab', 45 | 'Connect-Graph', 46 | 'ConvertTo-GraphDateTimeTimeZone', 47 | 'Copy-FromGraphFolder', 48 | 'Copy-ToGraphFolder', 49 | 'Copy-GraphOneNotePage', 50 | 'Expand-GraphConditionalAccessPolicy', 51 | 'Export-GraphGroupMember', 52 | 'Export-GraphUser', 53 | 'Export-GraphWorkSheet', 54 | 'Find-GraphPeople', 55 | 'Get-AccessToken', 56 | 'Get-GraphApplication', 57 | 'Get-GraphBucketTaskList', 58 | 'Get-GraphChannel', 59 | 'Get-GraphChannelReply', 60 | 'Get-GraphConditionalAccessPolicy', 61 | 'Get-GraphContact', 62 | 'Get-GraphDeletedObject', 63 | 'Get-GraphDirectoryLog', 64 | 'Get-GraphDirectoryRole', 65 | 'Get-GraphDirectoryRoleTemplate', 66 | 'Get-GraphDomain', 67 | 'Get-GraphDrive', 68 | 'Get-GraphEvent', 69 | 'Get-GraphGroup', 70 | 'Get-GraphGroupConversation', 71 | 'Get-GraphGroupList', 72 | 'Get-GraphGroupThread', 73 | 'Get-GraphLicense', 74 | 'Get-GraphList', 75 | 'Get-GraphMailFolder', 76 | 'Get-GraphMailItem', 77 | 'Get-GraphMailTips', 78 | 'Get-GraphNamedLocation', 79 | 'Get-GraphOneNoteBook', 80 | 'Get-GraphOneNotePage', 81 | 'Get-GraphOneNoteSection', 82 | 'Get-GraphOrganization', 83 | 'Get-GraphPlan', 84 | 'Get-GraphPlanTask', 85 | 'Get-GraphReminderView', 86 | 'Get-GraphReport', 87 | 'Get-GraphServicePrincipal', 88 | 'Get-GraphSignInLog', 89 | 'Get-GraphSite', 90 | 'Get-GraphSiteColumn', 91 | 'Get-GraphSiteUserList', 92 | 'Get-GraphSKU', 93 | 'Get-GraphTeamsApp', 94 | 'Get-GraphToDoList' 95 | 'Get-GraphUser', 96 | 'Get-GraphUserList', 97 | 'Get-GraphWorkBook', 98 | 'Grant-GraphDirectoryRole', 99 | 'Grant-GraphLicense', 100 | 'Import-GraphGroup', 101 | 'Import-GraphGroupMember', 102 | 'Import-GraphUser', 103 | 'Import-GraphWorkSheet', 104 | 'Invoke-GraphRequest', 105 | 'Move-GraphMailItem', 106 | 'New-ContactAddress', 107 | 'New-GraphAttendee', 108 | 'New-GraphChannel', 109 | 'New-GraphChannelMessage', 110 | 'New-GraphChannelReply' #All the different column types together! 111 | 'New-GraphColumn','New-GraphBooleanColumn', 'New-GraphCalculatedColumn', 'New-GraphChoiceColumn','New-GraphCurrencyColumn', 'New-GraphDateTimeColumn', 112 | 'New-GraphLookupColumn', 'New-GraphNumberColumn','New-GraphPersonOrGroupColumn','New-GraphTextColumn', 113 | # 'New-GraphContentType' , 114 | 'New-GraphContact' , 115 | 'New-GraphFolder', 116 | 'New-GraphGroup', 117 | 'New-GraphInvitation', 118 | 'New-GraphList', 119 | 'New-GraphMailAddress', 120 | 'New-GraphOneNoteSection', 121 | 'New-GraphPhysicalAddress', 122 | 'New-GraphRecipient', 123 | 'New-GraphRecurrence', 124 | 'New-GraphTeamPlan', 125 | 'New-GraphToDoList', 126 | 'New-GraphToDoTask', 127 | 'New-GraphUser', 128 | 'New-GraphWorkBook', 129 | 'Out-GraphOneNote', 130 | 'Remove-GraphChannel', 131 | 'Remove-GraphContact', 132 | 'Remove-GraphEvent', 133 | 'Remove-GraphGroup', 134 | 'Remove-GraphGroupMember', 135 | 'Remove-GraphGroupThread', 136 | 'Remove-GraphListItem', 137 | 'Remove-GraphOneNotePage', 138 | 'Remove-GraphPlan', 139 | 'Remove-GraphPlanbucket', 140 | 'Remove-GraphPlanTask', 141 | 'Remove-GraphToDoList', 142 | 'Remove-GraphToDoTask', 143 | 'Remove-GraphUser', 144 | 'Rename-GraphPlanBucket', 145 | 'Reset-GraphUserPassword', 146 | 'Restore-GraphDeletedObject', 147 | 'Revoke-GraphDirectoryRole', 148 | 'Revoke-GraphLicense', 149 | 'Save-GraphMailAttachment', 150 | 'Send-GraphGroupReply', 151 | 'Send-GraphMailForward', 152 | 'Send-GraphMailMessage', 153 | 'Send-GraphMailReply', 154 | 'Set-GraphContact', 155 | 'Set-GraphDefaultGroup', 156 | 'Set-GraphEvent', 157 | 'Set-GraphGroup', 158 | 'Set-GraphHomeDrive', 159 | 'Set-GraphOneNoteHome', 160 | 'Set-GraphOptions', 161 | 'Set-GraphListItem', 162 | 'Set-GraphPlanDetails', 163 | 'Set-GraphPlanTask', 164 | 'Set-GraphTeam', 165 | 'Set-GraphUser', 166 | 'Show-GraphFolder', 167 | 'Show-GraphSession', 168 | 'Test-GraphSession', 169 | 'Update-GraphOneNotePage', 170 | 'Update-GraphToDoTask' 171 | ) 172 | AliasesToExport = @( 173 | 'Add-FileToGraphNoteBook', 174 | 'Add-GraphNoteBookPage', 175 | 'Add-GraphTeamChannel', 176 | 'Add-GraphTeamMember', 177 | 'Copy-GraphNoteBookPage', 178 | 'Get-GraphContext', 179 | 'Get-GraphConversation', 180 | 'Get-GraphNoteBook', 181 | 'Get-GraphNoteBookPage', 182 | 'Get-GraphNoteBookSection', 183 | 'Get-GraphTeam', 184 | 'Get-GraphTeamChannel', 185 | 'Get-GraphTeamConversation', 186 | 'Get-GraphTeamThread', 187 | 'New-EventAttendee', 188 | 'New-GraphGroupPlan', 189 | 'New-GraphNoteBookSection', 190 | 'New-GraphSession', 191 | 'New-GraphTeam', 192 | 'New-RecurrencePattern', 193 | 'Out-GraphNoteBook', 194 | 'Remove-GraphNoteBookPage' 195 | 'Remove-GraphTeam', 196 | 'Remove-GraphTeamMember', 197 | 'Set-GraphDefaultTeam', 198 | 'Update-GraphNoteBookPage', 199 | 'BooleanColumn', 200 | 'CalculatedColumn', 201 | 'ChoiceColumn', 202 | 'CurrencyColumn', 203 | 'DateTimeColumn', 204 | 'ListColumn', 205 | 'LookupColumn', 206 | 'NumberColumn', 207 | 'PersonColumn', 208 | 'TextColumn', 209 | 'ggg', 210 | 'ggu', 211 | 'GWhoAmI', 212 | 'GraphSession', 213 | 'igr' 214 | ) 215 | FileList = @( 216 | '.\README.md' 217 | '.\LICENSE', 218 | '.\ChangeLog.md' 219 | '.\Microsoft.Graph.PlusPlus.psd1', 220 | '.\Microsoft.Graph.PlusPlus.psm1', 221 | '.\ActionCard.ps1', 222 | '.\Applications.ps1', 223 | '.\Authentication.ps1', 224 | '.\Groups.ps1', 225 | '.\Identity.DirectoryManagement.ps1', 226 | '.\Identity.SignIns.ps1', 227 | '.\Notes.ps1', 228 | '.\OneDrive.ps1', 229 | '.\PersonalContacts.ps1', 230 | '.\Planner.ps1', 231 | '.\Reports.ps1', 232 | '.\Sharepoint.ps1', 233 | '.\Users.Actions.ps1', 234 | '.\Users.Functions.ps1', 235 | '.\Users.ps1', 236 | '.\Microsoft.Graph.PlusPlus.settings.ps1', 237 | '.\Microsoft.Graph.PlusPlus.format.ps1xml', 238 | '.\Microsoft.Graph.PlusPlus.types.ps1xml', 239 | '.\Blank.xlsx', 240 | '.\docs\Relationships.pdf', 241 | '.\docs\Logon options.pdf', 242 | '.\Examples\PlannerImportExport', 243 | '.\Examples\Data-XLSx-Drive.ps1', 244 | '.\Examples\Data-XLSx-Drive-dlChart.ps1', 245 | '.\Examples\Demo_Main.ps1', 246 | '.\Examples\Demo_1-Azure Logon.ps1', 247 | '.\Examples\Demo_2-CredLogon.ps1', 248 | '.\Examples\Link.png', 249 | '.\Examples\New-BcAuthContext.ps1', 250 | '.\Examples\OneDrive.gif', 251 | '.\Examples\Team.gif', 252 | '.\Examples\Template_groups.csv', 253 | '.\Examples\Template_membership.csv', 254 | '.\Examples\Template_users.csv' 255 | '.\Examples\PlannerImportExport\Create_Planner_Template.ps1', 256 | '.\Examples\PlannerImportExport\Export-planner-to-xlsx.ps1', 257 | '.\Examples\PlannerImportExport\Import-Planner-From-Xlsx.ps1', 258 | '.\Examples\PlannerImportExport\Planner-Export.xlsx' 259 | ) 260 | 261 | # Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only. 262 | # CLRVersion = '' 263 | 264 | # HelpInfo URI of this module 265 | # HelpInfoURI = '' 266 | } -------------------------------------------------------------------------------- /Microsoft.Graph.PlusPlus.settings.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .description 3 | Executed when the module loads to set defaults for 4 | the scopes and information needed to support additional logon types. 5 | Environment Variable MGSettingsPath is set the information will be read from there. 6 | This file IS considered safe to edit. If it is deleted the module will 7 | user the default scopes provided by the PowerShell-graph-sdk. 8 | #> 9 | Set-GraphOptions -DefaultUsageLocation GB 10 | Set-GraphOptions -DefaultUserProperties @('businessPhones', 'displayName', 'givenName', 'id', 'jobTitle', 'mail', 'mobilePhone', 11 | 'officeLocation', 'preferredLanguage', 'surname', 'userPrincipalName', #The preceeding fields are the APIs defaults. 12 | 'assignedLicenses', 'department', 'usageLocation', 'userType') 13 | 14 | #The scopes requested. You can shorten this of you don't need all things provided in the module 15 | if ($env:GraphScopes) { 16 | Set-GraphOptions -DefaultScopes ( $env:GraphScopes -split ',\s*')} 17 | else { Set-GraphOptions -DefaultScopes @( 18 | 'AppCatalog.Read.All', 19 | 'AuditLog.Read.All', 20 | 'Directory.AccessAsUser.All', #Grant same rights to the directory as the user has 21 | 'Calendars.ReadWrite', 22 | 'Calendars.ReadWrite.Shared', 23 | 'ChannelMessage.Read.All', 24 | 'ChannelMessage.Delete', 25 | 'ChannelMessage.Edit', 26 | 'Contacts.ReadWrite', 27 | 'Contacts.ReadWrite.Shared', 28 | 'Files.ReadWrite.All', 29 | 'Group.ReadWrite.All',# or read fails when logging on as non-admin 30 | 'Mail.ReadWrite', 31 | 'Mail.Send', 32 | 'MailboxSettings.ReadWrite', 33 | 'Notes.ReadWrite', 34 | 'Notes.ReadWrite.All', 35 | 'Notes.Create', 36 | 'People.Read.All', 37 | 'Presence.Read.All', 38 | 'Reports.Read.All', 39 | 'Sites.ReadWrite.All', 40 | 'Sites.Manage.All', #Needed to create lists. 41 | 'Tasks.ReadWrite', #Needed for Todo access 42 | 'User.ReadWrite.all', # Read write users and groups may not be needed if Directory is granted ? 43 | 'openid', 44 | 'profile'#, 'offline_access' 45 | )} 46 | <# 47 | If you want to logon by providing a name and password or as the app you need to provide 48 | 1 your tenant ID (a GUID) 49 | 2 The ID of an App (other GUID) which has been granted the right to use some 50 | scopes - either on its own account or delegated for a user - in your tenant 51 | 3 A secret associated with the App. 52 | 53 | You can create an app in Azure AD or at https://apps.dev.microsoft.com/ 54 | I created mine as a native app, with a re-direct URI of https://login.microsoftonline.com/common/oauth2/nativeclient and 55 | gave it a set of Microsoft graph permissions in Azure AD 56 | 57 | If you ONLY Want to work with accounts in Azure AD you can set up your app with these instructions which I lifted from 58 | https://msunified.net/2018/12/12/post-at-microsoftteams-channel-chat-message-from-powershell-using-graph-api/ 59 | 1. Log on to https://portal.azure.com with a GA administrator 60 | 2. Navigate to Azure Active Directory 61 | 3 Go to App registrations 62 | 4. Click + New registration 63 | 5. Call it PowerShellMSGraphAPI (or another name of your choice) 64 | 6. Leave who can use this API on the default of single tennant and leave the Redirect URI blank 65 | 7. Click Register 66 | 8. This will bring up the details of the new APP. Under call APIS click View API permissions to grant the required group read and write permissions 67 | 9. Click + Add a permission 68 | 10. Choose Microsoft Graph, then Delegated permissions and choose Group.Read.All and ReadWrite.All (remember you need to expand Group) 69 | 12. I had to click the enterprise apps link and click "Grant admin Consent" from (this is where a GA admin is needed) 70 | 13. You now have admin consent granted for your tenant, so users can authenticate without a consent dialog. 71 | 14. Navigate back to Overview 72 | 15. Copy the Application (client) ID Paste it into this script as the value for ClientID in Set-GraphOptions; 73 | 16. Also copy the tenant ID paste it into this script as the value for TenantID in Set-GraphOptions 74 | 17. Click Certificates and Secrets, add a secret and chose never expires (unless you want to update the script later), click add 75 | 18. Copy the secret and EITHER (the dirty but portable way) paste into this script as the value for clientSecret in Set-GraphOptions 76 | OR (clean but not portable) Convert it to a securestring & export: ConvertTo-SecureString -Force -AsPlainText (Get-Clipboard) | Export-Clixml myclientSecret.xml 77 | Get the contents of the file as for setting clientsecret in Set-GraphOptions 78 | #> 79 | 80 | #YOUR tenant ID 81 | #Set-GraphOptions -TenantID "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" 82 | #A client APP ID known to your tenant 83 | Set-GraphOptions -ClientID "14d82eec-204b-4c2f-b7e8-296a70dab67e" #the Graph-Powershell SDK GUID. You can create your own. "1950a258-227b-4e31-a9cf-717495945fc2" is known client ID for PowerShell 84 | 85 | #Really this should be saved somewhere else as a secure string. 86 | #Set-GraphOptions -ClientSecret "xxxxxxxxxxxxxxxxxxxxxxxxxxxxx" 87 | #Set-GraphOptions -ClientSecret (Import-Clixml "$PSScriptRoot\myclientSecret.xml") 88 | 89 | 90 | #To update the Committed version remove the private info and run 91 | #git update-index --no-skip-worktree .\Microsoft.Graph.PlusPlus.settings.ps1 92 | ##Commit. and then run 93 | #git update-index --skip-worktree .\Microsoft.Graph.PlusPlus.settings.ps1 94 | #Before putting private info back 95 | -------------------------------------------------------------------------------- /Microsoft.Graph.PlusPlus.types.ps1xml: -------------------------------------------------------------------------------- 1 | 2 | Microsoft.Graph.PowerShell.Models.MicrosoftGraphUser 3 | Organization 4 | UserPrincipalName 5 | DisplayName 6 | JobTitle 7 | Department 8 | ManagerName 9 | Usagelocation 10 | Licensed 11 | 12 | Score 13 | $This.scoredEmailAddresses[0].relevanceScore 14 | 15 | emailaddresses 16 | scoredEmailAddresses 17 | 18 | ManagerName 19 | $This.Manager.displayName 20 | 21 | ManagerMail 22 | $This.Manager.Mail 23 | 24 | Licensed 25 | if ($This.AssignedLicenses) {$true} else {$null} 26 | 27 | ToString 30 | 31 | Microsoft.Graph.PowerShell.Models.MicrosoftGraphEvent 32 | When 33 | $s = [convert]::ToDateTime($this.Start.datetime) 34 | $e = [convert]::ToDateTime($this.end.datetime) 35 | if ($s.AddDays(1) -eq $e -and 36 | $s.hour -eq 0 -and $s.minute -eq 0 ) { 37 | $s.ToShortDateString() + ' All day' 38 | } 39 | else {$s.ToString("g") + ' to ' + $e.ToString("g") + $this.End.timezone} 40 | 41 | StartDateTime 42 | [convert]::ToDateTime($this.start.dateTime) 43 | 44 | EndDateTime 45 | [convert]::ToDateTime($this.end.dateTime) 46 | 47 | Where 48 | $this.location.displayname 49 | 50 | 51 | Microsoft.Graph.PowerShell.Models.MicrosoftGraphTeam 52 | ToString 55 | 56 | Microsoft.Graph.PowerShell.Models.MicrosoftGraphGroup 57 | ToString 60 | 61 | Microsoft.Graph.PowerShell.Models.MicrosoftGraphChannel 62 | ToString 65 | 66 | Microsoft.Graph.PowerShell.Models.MicrosoftGraphOnenotePage 67 | ToString 70 | Open 73 | 74 | Microsoft.Graph.PowerShell.Models.MicrosoftGraphOnenoteSection 75 | ToString 78 | Open 81 | 82 | Microsoft.Graph.PowerShell.Models.MicrosoftGraphNotebook 83 | ToString 86 | Open 89 | 90 | Microsoft.Graph.PowerShell.Models.MicrosoftGraphDrive 91 | Drive 92 | id 93 | 94 | Open 97 | 98 | Microsoft.Graph.PowerShell.Models.MicrosoftGraphDriveItem 99 | ItemID 100 | id 101 | 102 | Drive 103 | $this.ParentReference.DriveID 104 | 105 | Open 108 | 109 | Microsoft.Graph.PowerShell.Models.MicrosoftGraphSite 110 | Template 111 | $this.list.template 112 | 113 | Open 116 | List 120 | 121 | Microsoft.Graph.PowerShell.Models.MicrosoftGraphCalendar 122 | Calendar 123 | id 124 | 125 | 126 | Microsoft.Graph.PowerShell.Models.MicrosoftGraphMessage 127 | FromName 128 | $this.from.emailAddress.name 129 | 130 | FromAddress 131 | $this.from.emailAddress.address 132 | 133 | BodyText 134 | $this.body.content 135 | 136 | Move 140 | Open 143 | 144 | Microsoft.Graph.PowerShell.Models.MicrosoftGraphAttachment 145 | Move 149 | 150 | Microsoft.Graph.PowerShell.Models.MicrosoftGraphContact 151 | PSStandardMembers 152 | DefaultDisplayPropertySet 153 | displayname 154 | jobtitle 155 | companyname 156 | mail 157 | mobile 158 | business 159 | home 160 | 161 | 162 | mobile 163 | mobilephone 164 | 165 | business 166 | $this.businessPhones[0] 167 | 168 | home 169 | this.HomePhones[0] 170 | 171 | 172 | Microsoft.Graph.PowerShell.Models.MicrosoftGraphReminder 173 | Subject 174 | eventSubject 175 | 176 | Location 177 | eventLocation 178 | 179 | When 180 | if ( [System.Convert]::ToDateTime($this.eventStartTime.datetime).AddDays(1) -eq 181 | [System.Convert]::ToDateTime($this.eventEndTime.datetime )) { 182 | $this.eventStartTime.datetime -replace '(\d{2}:\d{2}):00$','$1' -replace '00:00$','All day' 183 | } 184 | else { ($this.eventStartTime.datetime -replace '(\d{2}:\d{2}):00$','$1') + ' to ' + 185 | ($this.eventEndTime.datetime -replace '(\d{2}:\d{2}):00$','$1') + $this.eventEndTime.timezone } 186 | 187 | Start 188 | [System.Convert]::ToDateTime($this.eventStartTime.datetime ) 189 | 190 | End 191 | [System.Convert]::ToDateTime($this.eventEndTime.datetime ) 192 | 193 | Reminder 194 | [System.Convert]::ToDateTime($this.reminderFireTime.datetime) 195 | 196 | 197 | Microsoft.Graph.PowerShell.Models.MicrosoftGraphMailTips 198 | Address 199 | $this.EmailAddress.Address 200 | 201 | MessageText 202 | $this.AutomaticReplies.Message 203 | 204 | MessageStart 205 | $this.AutomaticReplies.scheduledStartTime.DateTime 206 | 207 | MessageEnd 208 | $this.AutomaticReplies.scheduledEndTime.DateTime 209 | 210 | 211 | Microsoft.Graph.PowerShell.Models.MicrosoftGraphList 212 | Template 213 | $this.list.template 214 | 215 | Open 218 | 219 | 220 | Microsoft.Graph.PowerShell.Models.MicrosoftGraphChatMessage 221 | Team 222 | $this.ChannelIdentity.TeamID 223 | 224 | Channel 225 | $this.ChannelIdentity.ChannelId 226 | 227 | 228 | Microsoft.Graph.PowerShell.Models.MicrosoftGraphTeamsTab 229 | TeamsAppName 230 | $this.teamsApp.displayName 231 | 232 | 233 | Microsoft.Graph.PowerShell.Models.MicrosoftGraphSignIn 234 | City 235 | $this.location.City 236 | 237 | State 238 | $this.location.State 239 | 240 | Country 241 | $this.location.countryOrRegion 242 | 243 | Lat 244 | $this.location.geoCoordinates.latitude 245 | 246 | Long 247 | $this.location.geoCoordinates.longitude 248 | 249 | Browser 250 | $this.deviceDetail.browser 251 | 252 | Device 253 | $this.deviceDetail.displayName 254 | 255 | OperatingSystem 256 | $this.deviceDetail.OperatingSystem 257 | 258 | Date 259 | [datetime]$this.createdDateTime 260 | 261 | 262 | Microsoft.Graph.PowerShell.Models.MicrosoftGraphDirectoryAudit 263 | User 264 | $this.initiatedBy.user.userPrincipalName 265 | 266 | App 267 | $this.initiatedBy.App.DisplayName 268 | 269 | 270 | 271 | -------------------------------------------------------------------------------- /PersonalContacts.ps1: -------------------------------------------------------------------------------- 1 | 2 | function New-GraphPhysicalAddress { 3 | <# 4 | .synopsis 5 | Builds a street / postal / physical address to use in the contact commands 6 | .Example 7 | >$fabrikamAddress = New-GraphPhysicalAddress "123 Some Street" Seattle WA 98121 "United States" 8 | Creates an address - if the -Street, City, State, Postalcode country are not explictly 9 | specified they will be assigned in that order. Quotes are desireable but only necessary 10 | when a value contains spaces. 11 | It can then be used like this. Set-GraphContact $pavel -BusinessAddress $fabrikamAddress 12 | #> 13 | [cmdletbinding()] 14 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification='Does not change system state.')] 15 | param ( 16 | #Street address. This can contain carriage returns for a district, e.g. "101 London Road`r`nBotley" 17 | [String]$Street, 18 | #City, or town as people outside the US tend to call it 19 | [String]$City, 20 | #State, Province, County, the administrative level below country 21 | [String]$State, 22 | #Postal code. Even it parses as a number, as with US ZIP codes, it will be converted to a string 23 | [String]$PostalCode, 24 | #Usually a country but could be some other geographical entity 25 | [String]$CountryOrRegion 26 | ) 27 | $Address = @{} 28 | foreach ($P in $PSBoundParameters.Keys.Where({$_ -notin [cmdlet]::CommonParameters})) { 29 | $Address[$p] + $PSBoundParameters[$p] 30 | } 31 | $Address 32 | } 33 | 34 | function Get-GraphContact { 35 | <# 36 | .Synopsis 37 | Get the user's contacts 38 | .Example 39 | get-graphContact -name "o'neill" | ft displayname, mobilephone 40 | Gets contacts where the display name, given name, surname, file-as name, or email begins with 41 | O'Neill - note the function handles apostrophe, - a single one would normal cause an error with the query. 42 | The results are displayed as table with display name and mobile number 43 | #> 44 | [cmdletbinding(DefaultParameterSetName="None")] 45 | [outputtype([Microsoft.Graph.PowerShell.Models.MicrosoftGraphContact])] 46 | param ( 47 | #UserID as a guid or User Principal name. If not specified defaults to "me" 48 | [string]$User, 49 | #A custom set of contact properties to select 50 | [ValidateSet('assistantName', 'birthday', 'businessAddress', 'businessHomePage', 'businessPhones', 51 | 'categories', 'changeKey', 'children', 'companyName', 'createdDateTime', 'department', 52 | 'displayName', 'emailAddresses', 'fileAs', 'generation', 'givenName', 'homeAddress', 53 | 'homePhones', 'id', 'imAddresses', 'initials', 'jobTitle', 'lastModifiedDateTime', 54 | 'manager', 'middleName', 'mobilePhone', 'nickName', 'officeLocation', 'otherAddress', 55 | 'parentFolderId', 'personalNotes', 'profession', 'spouseName', 'surname', 'title', 56 | 'yomiCompanyName', 'yomiGivenName', 'yomiSurname')] 57 | [string[]]$Select, 58 | 59 | #If specified looks for contacts where the display name, file-as Name, given name or surname beging with ... 60 | [Parameter(Mandatory=$true, ParameterSetName='FilterByName')] 61 | [string]$Name, 62 | #A custom OData Filter String 63 | [Parameter(Mandatory=$true, ParameterSetName='FilterByString')] 64 | [string]$Filter 65 | ) 66 | 67 | #region build the URI - if we got a user ID, use it, add select, filter, orderby and/or top as needed 68 | if ($User.id) {$uri = "$GraphUri/users/$($User.id)/contacts?`$top=100"} 69 | elseif ($User) {$uri = "$GraphUri/users/$User/contacts?`$top=100" } 70 | else {$uri = "$GraphUri/me/contacts?`$top=100" } 71 | 72 | if ($Select) {$uri = $uri + '&$select=' + ($Select -join ',') } 73 | if ($Name) {$uri = $uri + '&$filter=' + (FilterString $Name -ExtraFields 'companyname','givenName','surname') } 74 | if ($Filter) {$uri = $uri + '&$Filter=' + $Filter } 75 | #endregion 76 | 77 | Invoke-GraphRequest -Uri $uri -ValueOnly -AllValues -AsType ([MicrosoftGraphContact]) -ExcludeProperty "@odata.etag" 78 | } 79 | 80 | function New-GraphContact { 81 | <# 82 | .Synopsis 83 | Adds an entry to the current users Outlook contacts 84 | .Description 85 | Almost all the paramters can be accepted form a piped object to make import easier. 86 | .Example 87 | >New-GraphContact -GivenName Pavel -Surname Bansky -Email pavelb@fabrikam.onmicrosoft.com -BusinessPhones "+1 732 555 0102" 88 | Creates a new contact; if no displayname is given, one will be decided using given name and suranme; 89 | .Example 90 | > 91 | >$PavelMail = New-GraphRecipient -DisplayName "Pavel Bansky [Fabikam]" -Mail pavelb@fabrikam.onmicrosoft.com 92 | >New-GraphContact -GivenName Pavel -Surname Bansky -Email $pavelmail -BusinessPhones "+1 732 555 0102" 93 | This creates the same contanct but sets up their email with a display name. 94 | New recipient creates a hash table 95 | @{'emailaddress' = @ { 96 | 'name' = 'Pavel Bansky [Fabikam]' 97 | 'address' = 'pavelb@fabrikam.onmicrosoft.com' 98 | } 99 | } 100 | #> 101 | [cmdletbinding(SupportsShouldProcess=$true)] 102 | [outputtype([Microsoft.Graph.PowerShell.Models.MicrosoftGraphContact])] 103 | param ( 104 | [Parameter(ValueFromPipelineByPropertyName)] 105 | $GivenName, 106 | [Parameter(ValueFromPipelineByPropertyName)] 107 | $MiddleName, 108 | [Parameter(ValueFromPipelineByPropertyName)] 109 | $Initials , 110 | [Parameter(ValueFromPipelineByPropertyName)] 111 | $Surname, 112 | [Parameter(ValueFromPipelineByPropertyName)] 113 | $NickName, 114 | [Parameter(ValueFromPipelineByPropertyName)] 115 | $FileAs, 116 | [Parameter(ValueFromPipelineByPropertyName)] 117 | $DisplayName, 118 | [Parameter(ValueFromPipelineByPropertyName)] 119 | $CompanyName, 120 | [Parameter(ValueFromPipelineByPropertyName)] 121 | $JobTitle, 122 | [Parameter(ValueFromPipelineByPropertyName)] 123 | $Department, 124 | [Parameter(ValueFromPipelineByPropertyName)] 125 | $Manager, 126 | #One or more mail addresses, as a single string with semi colons between addresses or as an array of strings or MailAddress objects created with New-GraphMailAddress 127 | [Parameter(ValueFromPipelineByPropertyName)] 128 | $Email, 129 | #One or more instant messaging addresses, as an array or as a single string with semi colons between addresses 130 | [Parameter(ValueFromPipelineByPropertyName)] 131 | $IM, 132 | #A single mobile phone number 133 | [Parameter(ValueFromPipelineByPropertyName)] 134 | $MobilePhone, 135 | #One or more Business phones either as an array or as single string with semi colons between numbers 136 | [Parameter(ValueFromPipelineByPropertyName)] 137 | $BusinessPhones, 138 | #One or more home phones either as an array or as single string with semi colons between numbers 139 | [Parameter(ValueFromPipelineByPropertyName)] 140 | $HomePhones, 141 | #An address object created with New-GraphPhysicalAddress 142 | [Parameter(ValueFromPipelineByPropertyName)] 143 | $Homeaddress, 144 | #An address object created with New-GraphPhysicalAddress 145 | [Parameter(ValueFromPipelineByPropertyName)] 146 | $BusinessAddress, 147 | #An address object created with New-GraphPhysicalAddress 148 | [Parameter(ValueFromPipelineByPropertyName)] 149 | $OtherAddress, 150 | #One or more categories either as an array or as single string with semi colons between them. 151 | [Parameter(ValueFromPipelineByPropertyName)] 152 | $Categories, 153 | #The contact's Birthday as a date 154 | [Parameter(ValueFromPipelineByPropertyName)] 155 | [dateTime]$Birthday , 156 | [Parameter(ValueFromPipelineByPropertyName)] 157 | $PersonalNotes, 158 | [Parameter(ValueFromPipelineByPropertyName)] 159 | $Profession, 160 | [Parameter(ValueFromPipelineByPropertyName)] 161 | $AssistantName, 162 | [Parameter(ValueFromPipelineByPropertyName)] 163 | $Children, 164 | [Parameter(ValueFromPipelineByPropertyName)] 165 | $SpouseName, 166 | #If sepcified the contact will be created without prompting for confirmation. This is the default state but can change with the setting of confirmPreference. 167 | [Switch]$Force 168 | ) 169 | 170 | process { 171 | Set-GraphContact @PSBoundParameters -IsNew 172 | } 173 | } 174 | 175 | function Set-GraphContact { 176 | <# 177 | .Synopsis 178 | Modifies or adds an entry in the current users Outlook contacts 179 | .Example 180 | > 181 | > $pavel = Get-GraphContact -Name pavel 182 | > Set-GraphContact $pavel -CompanyName "Fabrikam" -Birthday "1974-07-22" 183 | The first line gets the Contact which was added in the 'New-GraphContact" example 184 | and the second adds Birthday and Company-name attributes to the contact. 185 | .Example 186 | > 187 | > $fabrikamAddress = New-GraphPhysicalAddress "123 Some Street" Seattle WA 98121 "United States" 188 | > Set-GraphContact $pavel -BusinessAddress $fabrikamAddress 189 | This continues from the previous example, creating an address in the first line 190 | and adding it to the contact in the second. 191 | 192 | #> 193 | [cmdletbinding(SupportsShouldProcess=$true)] 194 | [outputtype([Microsoft.Graph.PowerShell.Models.MicrosoftGraphContact])] 195 | param ( 196 | #The contact to be updated either as an ID or as contact object containing an ID. 197 | [Parameter(ValueFromPipeline=$true,ParameterSetName='UpdateContact',Mandatory=$true, Position=0 )] 198 | $Contact, 199 | #If specified, instead of providing a contact, instructs the command to create a contact instead of updating one. 200 | [Parameter(ParameterSetName='NewContact',Mandatory=$true )] 201 | [switch]$IsNew, 202 | [Parameter(ValueFromPipelineByPropertyName)] 203 | $GivenName, 204 | [Parameter(ValueFromPipelineByPropertyName)] 205 | $MiddleName, 206 | [Parameter(ValueFromPipelineByPropertyName)] 207 | $Initials , 208 | [Parameter(ValueFromPipelineByPropertyName)] 209 | $Surname, 210 | [Parameter(ValueFromPipelineByPropertyName)] 211 | $NickName, 212 | [Parameter(ValueFromPipelineByPropertyName)] 213 | $FileAs, 214 | #If not specified a display name will be generated, so updates without the display name may result in overwriting an existing one 215 | [Parameter(ValueFromPipelineByPropertyName)] 216 | $DisplayName, 217 | [Parameter(ValueFromPipelineByPropertyName)] 218 | $CompanyName, 219 | [Parameter(ValueFromPipelineByPropertyName)] 220 | $JobTitle, 221 | [Parameter(ValueFromPipelineByPropertyName)] 222 | $Department, 223 | [Parameter(ValueFromPipelineByPropertyName)] 224 | $Manager, 225 | #One or more mail addresses, as a single string with semi colons between addresses or as an array of strings or MailAddress objects created with New-GraphMailAddress 226 | [Parameter(ValueFromPipelineByPropertyName)] 227 | $Email, 228 | #One or more instant messaging addresses, as an array or as a single string with semi colons between addresses 229 | [Parameter(ValueFromPipelineByPropertyName)] 230 | $IM, 231 | #A single mobile phone number 232 | [Parameter(ValueFromPipelineByPropertyName)] 233 | $MobilePhone, 234 | #One or more Business phones either as an array or as single string with semi colons between numbers 235 | [Parameter(ValueFromPipelineByPropertyName)] 236 | $BusinessPhones, 237 | #One or more home phones either as an array or as single string with semi colons between numbers 238 | [Parameter(ValueFromPipelineByPropertyName)] 239 | $HomePhones, 240 | #An address object created with New-GraphPhysicalAddress 241 | [Parameter(ValueFromPipelineByPropertyName)] 242 | $Homeaddress, 243 | #An address object created with New-GraphPhysicalAddress 244 | [Parameter(ValueFromPipelineByPropertyName)] 245 | $BusinessAddress, 246 | #An address object created with New-GraphPhysicalAddress 247 | [Parameter(ValueFromPipelineByPropertyName)] 248 | $OtherAddress, 249 | #One or more categories either as an array or as single string with semi colons between them. 250 | [Parameter(ValueFromPipelineByPropertyName)] 251 | $Categories, 252 | #The contact's Birthday as a date 253 | [Parameter(ValueFromPipelineByPropertyName)] 254 | [nullable[dateTime]]$Birthday , 255 | [Parameter(ValueFromPipelineByPropertyName)] 256 | $PersonalNotes, 257 | [Parameter(ValueFromPipelineByPropertyName)] 258 | $Profession, 259 | [Parameter(ValueFromPipelineByPropertyName)] 260 | $AssistantName, 261 | [Parameter(ValueFromPipelineByPropertyName)] 262 | $Children, 263 | [Parameter(ValueFromPipelineByPropertyName)] 264 | $SpouseName, 265 | #If sepcified the contact will be created without prompting for confirmation. This is the default state but can change with the setting of confirmPreference. 266 | [Switch]$Force 267 | ) 268 | begin { 269 | $webParams = @{ 270 | 'ContentType' = 'application/json' 271 | 'URI' = "$GraphUri/me/contacts" 272 | 'AsType' = ([Microsoft.Graph.PowerShell.Models.MicrosoftGraphContact]) 273 | 'ExcludeProperty' = @('@odata.etag', '@odata.context' ) 274 | } 275 | } 276 | process { 277 | $contactSettings = @{ } 278 | if ($Email) {$contactSettings['emailAddresses'] = @() } 279 | if ($Email -is [string]) {$Email = $Email -split '\s*;\s*'} 280 | foreach ($e in $Email) { 281 | if ($e.emailAddress) {$contactSettings.emailAddresses += $e.emailAddress } 282 | elseif ($e -is [string]) {$contactSettings.emailAddresses += @{'address' = $e} } 283 | else {$contactSettings.emailAddresses += $e } 284 | } 285 | if ($IM -is [string]) {$contactSettings['imAddresses'] = @() + $IM -split '\s*;\s*'} 286 | elseif ($IM ) {$contactSettings['imAddresses'] = $IM} 287 | if ($Categories -is [string]) {$contactSettings['categories'] = @() + $Categories -split '\s*;\s*'} 288 | elseif ($Categories ) {$contactSettings['categories'] = $Categories} 289 | if ($Children -is [string]) {$contactSettings['children'] = @() + $Children -split '\s*;\s*'} 290 | elseif ($Children ) {$contactSettings['children'] = $Children} 291 | if ($BusinessPhones -is [string]) {$contactSettings['businessPhones'] = @() + $BusinessPhones -split '\s*;\s*'} 292 | elseif ($BusinessPhones ) {$contactSettings['businessPhones'] = $BusinessPhones} 293 | if ($HomePhones -is [string]) {$contactSettings['homePhones'] = @() + $HomePhones -split '\s*;\s*'} 294 | elseif ($HomePhones ) {$contactSettings['homePhones'] = $HomePhones } 295 | if ($MobilePhone ) {$contactSettings['mobilePhone'] = $MobilePhone} 296 | if ($GivenName ) {$contactSettings['givenName'] = $GivenName} 297 | if ($MiddleName ) {$contactSettings['middleName'] = $MiddleName} 298 | if ($Initials ) {$contactSettings['initials'] = $Initials} 299 | if ($Surname ) {$contactSettings['surname'] = $Surname} 300 | if ($NickName ) {$contactSettings['nickName'] = $NickName} 301 | if ($FileAs ) {$contactSettings['fileAs'] = $FileAs} 302 | if ($DisplayName ) {$contactSettings['displayName'] = $DisplayName} 303 | if ($Manager ) {$contactSettings['manager'] = $Manager} 304 | if ($JobTitle ) {$contactSettings['jobTitle'] = $JobTitle} 305 | if ($Department ) {$contactSettings['department'] = $Department} 306 | if ($CompanyName ) {$contactSettings['companyName'] = $CompanyName} 307 | if ($PersonalNotes ) {$contactSettings['personalNotes'] = $PersonalNotes} 308 | if ($Profession ) {$contactSettings['profession'] = $Profession} 309 | if ($AssistantName ) {$contactSettings['assistantName'] = $AssistantName} 310 | if ($Children ) {$contactSettings['children'] = $Children} 311 | if ($SpouseName ) {$contactSettings['spouseName'] = $spouseName} 312 | if ($Homeaddress ) {$contactSettings['homeaddress'] = $Homeaddress} 313 | if ($BusinessAddress ) {$contactSettings['businessAddress'] = $BusinessAddress} 314 | if ($OtherAddress ) {$contactSettings['otherAddress'] = $OtherAddress} 315 | if ($Birthday ) {$contactSettings['birthday'] = $Birthday.tostring('yyyy-MM-dd')} #note this is a different date format to most things ! 316 | 317 | $webParams['body'] = ConvertTo-Json $contactSettings 318 | Write-Debug $webParams.body 319 | if ($IsNew) { 320 | if ($force -or $PSCmdlet.ShouldProcess($DisplayName,'Create Contact')) { 321 | Invoke-GraphRequest @webParams -method Post 322 | } 323 | } 324 | else {#if Contact Passed 325 | if ($force -or $PSCmdlet.ShouldProcess($Contact.DisplayName,'Update Contact')) { 326 | if ($Contact.id) {$webParams.uri += '/' + $Contact.ID} 327 | else {$webParams.uri += '/' + $Contact } 328 | Invoke-GraphRequest @webParams -method Patch 329 | } 330 | } 331 | } 332 | } 333 | 334 | function Remove-GraphContact { 335 | <# 336 | .synopsis 337 | Deletes a contact from the default user's contacts 338 | .Example 339 | > Get-GraphContact -Name pavel | Remove-GraphContact 340 | Finds and removes any user whose given name, surname, email or display name 341 | matches Pavel*. This might return unexpected users, fortunately there is a prompt 342 | before deleting - the prompt it can be supressed by using the -Force switch if you 343 | are confident you have the right contact selected. 344 | #> 345 | [cmdletbinding(SupportsShouldProcess=$true,ConfirmImpact='High')] 346 | param ( 347 | #The contact to remove, as an ID or as a contact object containing an ID 348 | [parameter(Position=0,ValueFromPipeline=$true,Mandatory=$true )] 349 | $Contact, 350 | #If specified the contact will be removed without prompting for confirmation 351 | $Force 352 | ) 353 | process { 354 | if ($force -or $pscmdlet.ShouldProcess($Contact.DisplayName, 'Delete contact')) { 355 | if ($Contact.id) {$Contact = $Contact.id} 356 | Invoke-GraphRequest -Method Delete -uri "$GraphUri/me/contacts/$Contact" 357 | } 358 | } 359 | } 360 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Microsoft.Graph.PlusPlus 2 | 3 | A group within Microsoft produces a [Graph SDK for PowerShell](https://github.com/microsoftgraph/msgraph-sdk-powershell), it is mostly auto-generated from the Open API definitions and is the starting point for other projects - like this one. 4 | 5 | [Multiple modules on the Powershell Gallery](https://www.powershellgallery.com/packages?q=microsoft.graph) are the raw output of that project, they cover a lot of the API, but do not try to be good and usable PowerShell, commands for example: 6 | 7 | * Pipeline support is missing. 8 | * Passing parameters by position is not supported (parameters must be named). 9 | * Common parameters (`-Verbose` / `-ErrorAction`) are not supported, in particular `-WhatIf` / `-confirm` are not implemented for dangerous operations. 10 | * In many cases the API requires a GUID as a parameter, and the auto-generated commands won't take the ID from an object (which is what would be passed via the pipeline) or translate human-readable names to IDs. This kind of intelligence would be embedded in better PowerShell commands. 11 | * Argument completion is weak - confined to some parameter sets. 12 | 13 | * The full set of modules adds over 2000 commands, making it difficult to navigate and resulting in load times of over a minute. 14 | 15 | **This module includes 'PlusPlus' in its name because it extends and improves the Microsoft.Graph modules.** 16 | Some examples: 17 | 18 | * **User creation:** `New-GraphUser -FirstName Bob -LastName Cratchett` will use rules to generate a User Principal Name / email address (First.Last@DefaultDomain) and a display name ('First Last') and will set the user's _usage location_ so they can be licensed immediately. 19 | * **Finding a user:** `Get-GraphUser bob` will find all users with names which start "bob", pressing \[Tab\] after `bob` will expand the matching User Principal Name(s). 20 | * **Finding things connected with a user or group:** `Get-GraphUser bob -MemberOf` returns the groups and directory roles that "bob" has been placed in; `Get-GraphGroup 'Accounts' --Notebooks` returns the OneNote notebook for a group. 21 | * **Easy OneDrive and OneNote access:** `Get-GraphUser -Drive | Set-GraphHomeDrive` removes the need to specify "current user's drive" in subsequent commands `Copy-FromGraphFolder` \[tab\] then completes the names of files and folders on that drive. 22 | 23 | To compare just one of these 24 | Here's a simple task with _PlusPlus_ using the alias `ggu` for `Get-GraphUser` 25 | 26 | ```powershell-interactive 27 | ggu bob | % Manager 28 | Display Name Job Title Office Location Mail 29 | ------------ --------- --------------- ---- 30 | Jacob Marley Office Manager Jacob@mobula_consulting.com 31 | ``` 32 | 33 | And with the SDK module 34 | 35 | ```powershell-interactive 36 | > $bob = Get-MgUser -Filter "startswith(userprincipalName,'Bob')" -ExpandProperty manager 37 | > Get-MgUser -UserId $bob.Manager.Id 38 | Id DisplayName Mail 39 | -- ----------- ---- 40 | 4f770fd0-6b51-4338-b66f-2b31d9048cd2 Jacob Marley Jacob@mobula_consulting.com 41 | ``` 42 | 43 | Pipeline support enables commands like 44 | 45 | ```PowerShell 46 | Get-GraphUser Jacob -DirectReports | Set-GraphUser -Manager ebenezer@mobula_consulting.com 47 | ``` 48 | 49 | With long UPNs, tab completion where a name or ID is expected becomes very welcome. 50 | 51 | ## Co-existence with Other Microsoft.Graph modules 52 | 53 | Microsoft.Graph.PlusPlus **requires** the `Microsoft.Graph.Authentication` module and, if available, will use the .private.dll files from following modules (without loading the full module). 54 | 55 | * **Users** 56 | * Users.Functions 57 | * Users.Actions 58 | * Reports 59 | * Identity.SignIns 60 | * Identity.DirectoryManagement 61 | * Applications 62 | 63 | Of these, it is **strongly recommended** that `users` is available, the others are optional. 64 | 65 | ### Private DLL or Module? 66 | 67 | Installing the all the `Microsoft.Graph.xxx` modules creates a lot of clutter, so _PlusPlus_ just loads their private.dll files. This allows their types to be used, so for example: 68 | `Get-GraphUser Bob` will return a `Microsoft.Graph.PowerShell.Models.MicrosoftGraphUser` object. 69 | _PlusPlus_ provides formatting for many of these types, and extends some of them, and only uses a small number of commands from the SDK modules. 70 | Instead of using `Install-Module microsoft.graph.reports` to support the _reporting_ commands, you can download the module with 71 | `Save-Module Microsoft.Graph.Reports -path \temp` and copy `Microsoft.Graph.Reports.private.dll` to the `Microsoft.Graph.PlusPlus` module Directory. 72 | **Whichever method you use, ensure all SDK modules are the same version**. Errors will result if one if one module tries to load a newer version of authentication than the others, for example. 73 | 74 | ### Replacement of Aliases 75 | 76 | The `Microsoft.Graph.Authentication` module defines aliases `Invoke-GraphRequest` and `Connect-Graph` 77 | for `Invoke-MGGraphRequest` and `Connect-MGGraph` respectively. 78 | In _PlusPlus_ these names are defined as functions which call `Invoke-MGGraphRequest` and `Connect-MGGraph`. 79 | 80 | ### Settings 81 | 82 | When the module loads it runs `Microsoft.Graph.PlusPlus.settings.ps1` in the module directory to set 83 | 84 | 1. The default usage location to configure for new users (so they can be granted licenses). 85 | 1. The default properties returned for users. 86 | 1. The **scopes** which will be requested when logging on. Some commands will fail if you do not request the right scopes, but you can request fewer scopes if you do not intend to use all the commands. 87 | 1. Tenant ID, Client (App) ID and Client Secret used for additional logon methods supported by `Connect-Graph` 88 | 89 | If you do not want to change the file provided with the module, edit a copy of it and set the environment variable `GraphSettingsPath` to point to the copy. These settings can also be set on demand with the `Set-GraphOptions` Command 90 | 91 | ## Expanded choice of logon options 92 | 93 | Depending on settings, `Connect-Graph` may offer parameters `-Credential` `-AsApp` `-FromAzureSession` and `-Refresh`. 94 | When none of these is specified it calls `Connect-MgGraph` which will see if a suitable token is cached in your .graph directory, and if not start the device logon process, with a message like this 95 | `To sign in, use a web browser to open the page https://microsoft.com/devicelogin and enter the code AB12DEFGH to authenticate.` 96 | 97 | If a **Tenant ID, Client ID and Secret** are set (see settings above) the `-AsApp` switch parameter is enabled to allow the session to sign in as an app registered with Azure AD. 98 | The `-Credential` parameter will be enabled if only the Tenant ID and Client ID are set; `Connect-Graph` treats the Client Secret as optional for allowing the login with credentials but the logon service will reject some Client IDs if no secret is provided. 99 | These two methods get an _Access Token_ which `Connect-MgGraph` is told to use, and an associated _Refresh Token_. These logon methods allow the module to be used non-interactively; however, they only get scopes which previously been granted to the client-app; `Show-GraphSession` will show the scopes which have been granted. `Show-GraphSession` will also allow you to get the Refresh Token to use in another session and `Set-GraphOptions` will allow you to bring such a token into the current session. If the session has a _Refresh_ Token, `Connect-Graph -Refresh` will update the _Access_ Token, and the module attempts to do this automatically when the _Access_ Token expires. 100 | 101 | If the `Az.accounts` module has been loaded the `-FromAzureSession` switch parameter is enabled: this will connect as the currently signed in account, but the available scopes are fixed by the Azure accounts module. 102 | 103 | ## Module contents 104 | 105 | ### Commands: Session management 106 | 107 | `Set-GraphOptions,Get-AccessToken, Connect-Graph, Show-GraphSession ,Test-GraphSession Invoke-GraphRequest` 108 | 109 | ### Commands for working with the Directory 110 | 111 | #### Users and service principals 112 | 113 | `Get-GraphUserList, Get-GraphUser, New-GraphUser, Set-GraphUser, Remove-GraphUser, Import-GraphUser Export-GraphUser, Reset-GraphUserPassword` 114 | `New-GraphInvitation` 115 | `Get-GraphServicePrincipal` 116 | 117 | #### Groups / Teams 118 | 119 | `Get-GraphGroupList` 120 | `Get-GraphGroup, New-GraphGroup, Remove-GraphGroup, Set-GraphGroup, Import-GraphGroup` 121 | `Add-GraphGroupMember, Remove-GraphGroupMember, Import-GraphGroupMember, Export-GraphGroupMember` 122 | `Set-GraphTeam` 123 | 124 | #### Licenses 125 | 126 | `Get-GraphSKU, Get-GraphLicense, Grant-GraphLicense, Revoke-GraphLicense` 127 | 128 | #### Roles 129 | 130 | `Get-GraphDirectoryRole, Grant-GraphDirectoryRole, Revoke-GraphDirectoryRole, Get-GraphDirectoryRoleTemplate` 131 | 132 | #### Misc 133 | 134 | `Get-GraphConditionalAccessPolicy,Expand-GraphConditionalAccessPolicy, Get-GraphNamedLocation` 135 | `Get-GraphDeletedObject, Restore-GraphDeletedObject` 136 | `Get-GraphDomain, Get-GraphOrganization, Find-GraphPeople` 137 | 138 | #### Reports and Logs 139 | 140 | `Get-GraphReport, Get-GraphDirectoryLog, Get-GraphSignInLog` 141 | 142 | ### Commands for working with Apps 143 | 144 | #### OneDrive & Excel files 145 | 146 | `Get-GraphDrive, Set-GraphHomeDrive, New-GraphFolder, Show-GraphFolder, Copy-FromGraphFolder, Copy-ToGraphFolder` 147 | `New-GraphWorkBook, Get-GraphWorkBook, Export-GraphWorkSheet, Import-GraphWorksheet` 148 | 149 | #### OneNote 150 | 151 | `Add-FileToGraphOneNote` 152 | `Get-GraphOneNoteBook, Set-GraphOneNoteHome, Get-GraphOneNoteSection, New-GraphOneNoteSection` 153 | `Get-GraphOneNotePage, Copy-GraphOneNotePage, Add-GraphOneNotePage, Update-GraphOneNotePage, Remove-GraphOneNotePage, Out-GraphOneNote` 154 | 155 | #### Outlook-Messages 156 | 157 | `Get-GraphMailTips, Get-GraphMailFolder , Save-GraphMailAttachment, Get-GraphMailItem , Move-GraphMailItem, Send-GraphMailMessage, Send-GraphMailForward , Send-GraphMailReply,` 158 | `New-GraphRecipient, New-GraphMailAddress` 159 | `Get-GraphGroupConversation, Send-GraphGroupReply, Get-GraphGroupThread, Add-GraphGroupThread, Remove-GraphGroupThread` 160 | 161 | #### Outlook-Calendar 162 | 163 | `Get-GraphReminderView, Get-GraphEvent, Add-GraphEvent, Remove-GraphEvent, Set-GraphEvent` 164 | `New-GraphAttendee, New-GraphRecurrence` 165 | 166 | #### Outlook-Contacts 167 | 168 | `Get-GraphContact, Set-GraphContact, New-GraphContact, Remove-GraphContact` 169 | `New-GraphPhysicalAddress` 170 | 171 | #### Planner 172 | 173 | `Get-GraphPlan, Remove-GraphPlan, Set-GraphPlanDetails, New-GraphTeamPlan` 174 | `Add-GraphPlanBucket, Remove-GraphPlanBucket, Rename-GraphPlanBucket, Get-GraphBucketTaskList` 175 | `Get-GraphPlanTask , Add-GraphPlanTask, Set-GraphPlanTask, Remove-GraphPlanTask, Set-GraphTaskDetails` 176 | 177 | #### Sharepoint 178 | 179 | `Get-GraphList , New-GraphList , Add-GraphListItem, Set-GraphListItem, Remove-GraphListItem` 180 | `New-GraphBooleanColumn, New-GraphCalculatedColumn, New-GraphChoiceColumn, New-GraphColumn, New-GraphCurrencyColumn, New-GraphDateTimeColumn, New-GraphLookupColumn, New-GraphNumberColumn, New-GraphPersonOrGroupColumn, New-GraphTextColumn` 181 | `Get-GraphSite, Get-GraphSiteColumn , Get-GraphSiteUserList` 182 | 183 | #### Teams 184 | 185 | `Get-GraphChannel , Remove-GraphChannel` 186 | `New-GraphChannelMessage, New-GraphChannelReply, Get-GraphChannelReply` 187 | `Add-GraphOneNoteTab, Add-GraphPlannerTab, Add-GraphSharePointTab, Add-GraphWikiTab` 188 | 189 | #### ToDo 190 | 191 | `Get-GraphToDoList, New-GraphToDoList, Remove-GraphToDoList, New-GraphToDoTask, Remove-GraphToDoTask, Update-GraphToDoTask` 192 | 193 | ### Formatting and types 194 | 195 | The module extends 16 types from the Microsoft.Graph SDK Modules `Attachment, Calendar, ChatMessage, Contact, DirectoryAudit, Drive, DriveItem, Event, List, MailTips, Reminder, SignIn, Site, Site, TeamsTab` and `User` 196 | 197 | It provides formats for 47 types. `AppRole, Calendar, Channel, ChatMessage, ColumnDefinition, Contact, Conversation, ConversationThread, Device, DirectoryAudit, DirectoryRole, Domain, Drive, DriveItem, Event, GraphExtendedTask, Group, LicenseDetails, List, MailFolder, MailTips, Message, MicrosoftGraphMailboxSettings, Notebook, OneNoteOperation, OneNotePage, OneNoteSection, Organization, PermissionScope, Person, PlannerBucket, PlannerPlan, PlannerTask, Post, Presence, Reminder, ServicePlanInfo, ServicePrincipal, SignIn, Site, SubscribedSku, Team, TeamsApp, TeamsAppDefinition, TeamsTab, TodoTask, TodoTaskList, User` and `VerifiedDomain` 198 | 199 | And it provides completers for `Domains`, `Group` names, `Mail folders`, `OneDrive folders` & `OneDrive Items`, `OneNote Section` names, `Roles` in Azure AD, `SKUs` (for licensing) & the `plans` within SKUs, and `User Principal Names`. 200 | -------------------------------------------------------------------------------- /Reports.ps1: -------------------------------------------------------------------------------- 1 | using namespace Microsoft.Graph.PowerShell.Models 2 | 3 | function Get-GraphReport { 4 | <# 5 | .Synopsis 6 | Gets reports from MS Graph 7 | .Example 8 | >Get-GraphReport -Report MailboxUsageDetail | ft "Display Name", "Storage Used (Byte)" 9 | Displays mailbox storage used by users - note that 10 | fields have 'friendly' names which need to be wrapped in quotes 11 | #> 12 | [cmdletbinding(DefaultParameterSetName="None")] 13 | param ( 14 | #The report to Fetch 15 | [ValidateSet( 16 | 'EmailActivityCounts', 'EmailActivityUserCounts', 'EmailActivityUserDetail', 17 | 'EmailAppUsageAppsUserCounts', 'EmailAppUsageUserCounts', 'EmailAppUsageUserDetail', 'EmailAppUsageVersionsUserCounts', 18 | 'MailboxUsageDetail', 'MailboxUsageMailboxCounts', 'MailboxUsageQuotaStatusMailboxCounts', 'MailboxUsageStorage', 19 | 'Office365ActivationCounts', 'Office365ActivationsUserCounts', 'Office365ActivationsUserDetail', 20 | 'Office365ActiveUserCounts', 'Office365ActiveUserDetail', 'Office365GroupsActivityCounts', 'Office365GroupsActivityDetail', 21 | 'Office365GroupsActivityFileCounts', 'Office365GroupsActivityGroupCounts', 'Office365GroupsActivityStorage', 22 | 'Office365ServicesUserCounts', 23 | 'OneDriveActivityFileCounts', 'OneDriveActivityUserCounts', 'OneDriveActivityUserDetail', 'OneDriveUsageAccountCounts', 24 | 'OneDriveUsageAccountDetail', 'OneDriveUsageFileCounts', 'OneDriveUsageStorage', 25 | 'SharePointActivityFileCounts', 'SharePointActivityPages', 'SharePointActivityUserCounts', 26 | 'SharePointActivityUserDetail', 'SharePointSiteUsageDetail', 'SharePointSiteUsageFileCounts', 27 | 'SharePointSiteUsagePages', 'SharePointSiteUsageSiteCounts', 'SharePointSiteUsageStorage', 28 | 'SkypeForBusinessActivityCounts', 'SkypeForBusinessActivityUserCounts', 'SkypeForBusinessActivityUserDetail', 29 | 'SkypeForBusinessDeviceUsageDistributionUserCounts','SkypeForBusinessDeviceUsageUserCounts', 'SkypeForBusinessDeviceUsageUserDetail', 30 | 'SkypeForBusinessOrganizerActivityCounts', 'SkypeForBusinessOrganizerActivityMinuteCounts', 31 | 'SkypeForBusinessOrganizerActivityUserCounts', 'SkypeForBusinessParticipantActivityCounts', 32 | 'SkypeForBusinessParticipantActivityMinuteCounts', 'SkypeForBusinessParticipantActivityUserCounts', 33 | 'SkypeForBusinessPeerToPeerActivityCounts', 'SkypeForBusinessPeerToPeerActivityMinuteCounts', 'SkypeForBusinessPeerToPeerActivityUserCounts', 34 | 'TeamsDeviceUsageDistributionUserCounts', 'TeamsDeviceUsageUserCounts', 'TeamsDeviceUsageUserDetail', 35 | 'TeamsUserActivityCounts', 'TeamsUserActivityUserCounts', 'TeamsUserActivityUserDetail', 36 | 'YammerActivityCounts', 'YammerActivityUserCounts', 'YammerActivityUserDetail', 'YammerDeviceUsageDistributionUserCounts', 37 | 'YammerDeviceUsageUserCounts', 'YammerDeviceUsageUserDetail', 'YammerGroupsActivityCounts', 38 | 'YammerGroupsActivityDetail', 'YammerGroupsActivityGroupCounts' 39 | )] 40 | [parameter(Mandatory=$true)] 41 | $Report, 42 | #Date for the report - this should be a date in the past 30 days. If specified, -Period is ignored. Reports ending in Count, Storage or pages don't support date filtering 43 | [DateTime]$Date, 44 | #The range of time for the report in the form "Dn" where n is the number of days. The default is D7, except for Office365Activation activation reports 45 | [ValidateSet("D7", "D30", "D90", "D180")] 46 | $Period, 47 | #If specified the data will be written in CSV format to the path provided, otherwise it will be output to the pipeline 48 | $Path 49 | ) 50 | if ($Date) { 51 | if ($report -match 'Counts$|Pages$|Storage$') {Write-Warning -Message 'Reports ending with Counts, Pages or Storage do not support date filtering' ; return } 52 | if ($report -match '^Office365Activation') {Write-Warning -Message 'Office365Activation Reports do not support any filtering.' ; return } 53 | if ($report -eq 'MailboxUsageDetail') {Write-Warning -Message 'MailboxUsageDetail does not support date filtering.' ; return} 54 | $uri = "$GraphUri/reports/microsoft.graph.Get{0}(date={1:yyyy-MM-dd})" -f $Report , $Date 55 | } 56 | elseif ($Period) { 57 | if ($report -match '^Office365Activation') {Write-Warning -Message 'Office365Activation Reports do not support any filtering.' ; return } 58 | $uri = "$GraphUri/reports/microsoft.graph.Get{0}(period='{1}')" -f $Report , $Period 59 | } 60 | else { 61 | if ($report -notmatch '^Office365Activation') { 62 | $uri = "$GraphUri/reports/microsoft.graph.Get{0}(period='d7')" -f $Report 63 | } 64 | else { 65 | $uri = "$GraphUri/reports/microsoft.graph.Get{0}" -f $Report 66 | } 67 | } 68 | if ($Path) { Invoke-GraphRequest -Method GET -uri $uri -OutputFilePath $Path} 69 | else { 70 | $Path = [System.IO.Path]::GetTempFileName() 71 | Invoke-GraphRequest -Method GET -uri $uri -OutputFilePath $Path 72 | Import-Csv $Path 73 | Remove-Item $Path 74 | } 75 | } 76 | 77 | function Get-GraphSignInLog { 78 | <# 79 | .synopsis 80 | Gets the audit log -requires a priviledged account 81 | .Description 82 | This command calls https://graph.microsoft.com/beta/auditLogs/signIns 83 | which requires consent to use the AuditLog.Read.All Scope this can only be granted to Azure AD apps. 84 | .Example 85 | > 86 | >Get-GraphSignInLog | 87 | > select Date,UserPrincipalName,appDisplayName,ipAddress,clientAppUsed,browser,device,city,lat,long | 88 | > Export-Excel -Path .\signin.xlsx -AutoSize -IncludePivotTable -PivotTableName Signins -PivotRows appdisplayName -PivotColumns browser -PivotData @{date='Count'} -show 89 | 90 | Gets the sign-in Log and exports it Excel, creating a PivotTable 91 | #> 92 | [cmdletbinding()] 93 | [outputtype([Microsoft.Graph.PowerShell.Models.MicrosoftGraphSignIn])] 94 | param ( 95 | $top = 200 96 | ) 97 | $i = 1 98 | Write-Progress -Activity 'Getting Sign-in Auditlog' 99 | 100 | $result = Invoke-GraphRequest -Method get -Uri "$GraphUri/auditLogs/signIns?`$top=$top" -SkipHttpErrorCheck -StatusCodeVariable status 101 | if ($result.error) { 102 | Write-Progress -Activity 'Getting Sign-in Auditlog' -Completed 103 | Write-Warning "An error was returned: '$($result.error.message)' code: $($result.error.code) " 104 | } 105 | if ($status -notmatch "2\d\d") {Write-Warning "Status code returned was $Status ($([System.Net.HttpStatusCode]$status)) which does not look like success."} 106 | 107 | $records = $result.value 108 | while ($result.'@odata.nextLink' -and $records.count -lt $top) { 109 | $i ++ 110 | Write-Progress -Activity 'Getting Sign-in Auditlog' -CurrentOperation "Page $i" 111 | $result = Invoke-GraphRequest -Method get -Uri $result.'@odata.nextLink' 112 | $records += $result.value 113 | } 114 | 115 | foreach ($r in $records) { 116 | $r.pstypenames.add('GraphSigninLog') 117 | $r['RiskEventTypesV2'] = $r['RiskEventTypes_V2'] ; 118 | $null = $r.Remove('RiskEventTypes_V2'), $r.remove( "@odata.etag") ; 119 | New-Object -TypeName MicrosoftGraphSignIn -Property $r 120 | } 121 | Write-Progress -Activity 'Getting Sign-in Auditlog'-Completed 122 | 123 | 124 | } 125 | 126 | function Get-GraphDirectoryLog { 127 | <# 128 | .synopsis 129 | Gets the Directory audit log -requires a priviledged account 130 | .Description 131 | This command calls https://graph.microsoft.com/beta/auditLogs/directoryAudits 132 | which requires consent to use the AuditLog.Read.All Scope this can only be granted to Azure AD apps. 133 | 134 | #> 135 | [cmdletbinding()] 136 | [outputType([Microsoft.Graph.PowerShell.Models.MicrosoftGraphDirectoryAudit])] 137 | param ( 138 | [switch]$all, 139 | $Top = 100 140 | ) 141 | $i = 1 142 | Write-Progress -Activity 'Getting Directory Audits log' 143 | $uri = "$GraphUri/auditLogs/directoryAudits" 144 | if (-not $all) {$uri += "?`$Top=$Top"} 145 | $result = Invoke-GraphRequest -Method get -Uri $uri -SkipHttpErrorCheck -StatusCodeVariable status 146 | if ($result.error) {Write-Warning "An error was returned: '$($result.error.message)' - code: $($result.error.code) "} 147 | 148 | $records = $result.value 149 | while ($result.'@odata.nextLink' -and $records.Count -lt $top) { 150 | $i ++ 151 | Write-Progress -Activity 'Getting Directory Audits log' -CurrentOperation "Page $i" 152 | $result = Invoke-GraphRequest -Method get -Uri $result.'@odata.nextLink' 153 | $records += $result.value 154 | } 155 | foreach ($r in $records) { 156 | New-Object -TypeName MicrosoftGraphDirectoryAudit -Property $r 157 | } 158 | Write-Progress -Activity 'Getting Directory Audits log' -Completed 159 | } 160 | -------------------------------------------------------------------------------- /Users.Actions.ps1: -------------------------------------------------------------------------------- 1 | using namespace Microsoft.Graph.PowerShell.Models 2 | #MicrosoftGraphMailTips object is isn Microsoft.Graph.Users.Actions.private.dll 3 | param() 4 | 5 | function Get-GraphMailTips { 6 | <# 7 | .synopsis 8 | Gets mail tips for one or more users (is their mailbox full, are auto-replies on etc) 9 | #> 10 | [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '', Justification="MailTip would be incorrect")] 11 | param ( 12 | #mail addresses 13 | [Parameter(Mandatory=$true)] 14 | [string[]]$Address 15 | ) 16 | $webparams = @{ 17 | 'Method' = 'post' 18 | 'Uri' = "$GraphUri/me/getMailTips" 19 | 'ContentType' = 'application/json' 20 | 'body' = Convertto-Json @{EmailAddresses= @() + $Address; 21 | MailTipsOptions= "automaticReplies, mailboxFullStatus, customMailTip, "+ 22 | "deliveryRestriction, externalMemberCount, maxMessageSize, " + 23 | "moderationStatus, recipientScope, recipientSuggestions, totalMemberCount" 24 | } 25 | } 26 | 27 | Invoke-GraphRequest @webparams -ValueOnly -AsType ([MicrosoftGraphMailTips]) 28 | } -------------------------------------------------------------------------------- /Users.Functions.ps1: -------------------------------------------------------------------------------- 1 | using NameSpace Microsoft.Graph.PowerShell.Models 2 | # MicrosoftGraphReminder is in Microsoft.Graph.Users.Functions.private.dll 3 | function Get-GraphReminderView { 4 | <# 5 | .Synopsis 6 | Returns a view of items with reminders set across all a users calendars. 7 | #> 8 | [cmdletbinding(DefaultParameterSetName="None")] 9 | [outputType([Microsoft.Graph.PowerShell.Models.MicrosoftGraphReminder])] 10 | param ( 11 | #UserID as a guid or User Principal name, whose calendar should be fetched If not specified defaults to "me" 12 | [ArgumentCompleter([UPNCompleter])] 13 | $User = $Global:GraphUser, 14 | #Time zone to rennder event times. By default the time zone of the local machine will me use 15 | $Timezone = $(tzutil.exe /g), 16 | #Number of days of calendar to fetch from today 17 | [int]$Days = 30 , 18 | #The number of events to fetch. Must be greater than zero, and capped at 1000 19 | [ValidateRange(1,1000)] 20 | [int]$Top 21 | ) 22 | begin { 23 | $webParams = @{ Method = 'Get' 24 | ValueOnly = $true 25 | AsType = ([Microsoft.Graph.PowerShell.Models.MicrosoftGraphReminder]) 26 | #MicrosoftGraphReminder uses strings where there should be dates. Types.ps1xml adds fields for true dates 27 | 28 | } 29 | if ($TimeZone) {$webParams['Headers'] = @{'Prefer' = "Outlook.timezone=""$TimeZone"""}} 30 | 31 | } 32 | process { 33 | foreach ($u in $User) { 34 | if ($u.ID) {$u=$u.ID} 35 | #users.functions.yml refers to /users/$u/microsoft.graph.reminderView(StartDateTime='{0:yyyy-MM-ddTHH:mm:ss}',EndDateTime='{1:yyyy-MM-ddTHH:mm:ss}') 36 | #https://docs.microsoft.com/en-us/graph/api/user-reminderview?view=graph-rest-1.0&tabs=http gives the syntax here i.e. without "microsoft.graph." 37 | $uri = "$GraphUri/users/$u/reminderView(startDateTime='{0:yyyy-MM-ddTHH:mm:ss}',endDateTime='{1:yyyy-MM-ddTHH:mm:ss}')" 38 | 39 | $webParams['uri'] = $uri -f [datetime]::Today, [datetime]::Today.AddDays($days) 40 | Invoke-GraphRequest @webParams 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /docs/AAD-Admin-Consent-At-Logon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jhoneill/MsftGraph/22d09cb61ede5cf3f969e812f723f7d5398d8568/docs/AAD-Admin-Consent-At-Logon.png -------------------------------------------------------------------------------- /docs/AAD-Admin-Consent-Needed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jhoneill/MsftGraph/22d09cb61ede5cf3f969e812f723f7d5398d8568/docs/AAD-Admin-Consent-Needed.png -------------------------------------------------------------------------------- /docs/AAD-AppOverview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jhoneill/MsftGraph/22d09cb61ede5cf3f969e812f723f7d5398d8568/docs/AAD-AppOverview.png -------------------------------------------------------------------------------- /docs/AAD-AppSecrets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jhoneill/MsftGraph/22d09cb61ede5cf3f969e812f723f7d5398d8568/docs/AAD-AppSecrets.png -------------------------------------------------------------------------------- /docs/App Configuration.md: -------------------------------------------------------------------------------- 1 | Microsoft.Graph.PlusPlus is a set of extensions for The Microsoft.Graph powershell modules. It makes the same calls to the Graph APIs but tries to provide more intelligent PowerShell commands. 2 | 3 | When anything calls the Graph APIs it needs an *access token* which says 4 | - This program 5 | - Is authorized to use some functionality 6 | - On behalf of some user 7 | 8 | The program might be one the user is running on their own computer, or a service running somewhere, possibly doing something the user agreed to long ago, but doesn't need to approve every time. Although we think of authentication being a two party process - we log on to a service - actually it is a three way process, where the service wants to know if we trust a program to do things for us. 9 | 10 | The Microsoft.Graph.Authentication module has an _alias_ `Connect-Graph` and calling this will call the `Connect-MgGraph` cmdlet 11 | Microsoft.Graph.PlusPlus replaces that alias with its own `Connect-Graph` _function_ with extra functionality, it still calls `Connect-MgGraph`and if no other parameters are provided `Connect-MgGraph` will log a user on using the **Device-Code method** . This method means the app tells the login service "I'm am the app with ID '14d82eec-204b-4c2f-b7e8-296a70dab67e' and **I want to use the these scopes** of functionality. If I send a user to you, can I collect a token from you after they logon?" The service responds "Tell the user to do these steps, which will identify our conversation." The app passes the instructions to the user, and polls the server until it gets a token or a failure message. 12 | 13 | We can **specify a set of scopes **with `Set-GraphOptions -DefaultScopes`. To avoid the need to do this every time, Microsoft.Graph.PlusPlus runs a settings script, which contains a list of scopes. The script will check for a comma seperated list of scopes in the environment variable `GraphScopes`; and the environment variable is not set the script has its own list. The default script is named `Microsoft.Graph.PlusPlus.settings.ps1` and is in the module directory - it sets other options, you can set scopes via environment variable or via script as you choose. If you don't like changing files in the module directory you can make a copy and point to it with the environment variable `GraphSettingsPath`. If the default is not set or passed as a parameter no scopes will be passed to `Connect-MgGraph`, which will assumes a basic set. 14 | 15 | When the user enters the device code the login service tells them *which app* is trying to get permission to act on their behalf. 16 | The application is identified to the user before they logon 17 | and before completing a logon the service can check the user is happy for application "X" to do A,B and C on for them. 18 | Interactive Consent dialog requesting access to user's OneNote 19 | In the picture above, a different app (named "MobulaPS") has told the login service the **scope** of actions it wants to carry out. Because this user has not given this app access to that scope before, the logon service displays a **consent** dialog - consent remains unless revoked later, so the user doesn't need to re-approve apps' use of scopes. If the user agress the app receives a time-limitted access token access token and may also get a refresh token - which `ConnectMg-Graph` encrypts and saves in your `.graph` directory: it can present the *referesh* token and get a new *access* token without needing another logon. 20 | 21 | For the first screen shot I used a few lines of powerShell to request a device logon with the App-ID of Microsoft Graph PowerShell (14d82eec-204b-4c2f-b7e8-296a70dab67e) - the dialog is not so much asking "Are you really using Microsoft Graph PowerShell" as telling me "if you logon now, whatever sent you here will be able to use scopes you have granted to this app". 22 | 23 | *Users* can consent to apps' use of many scopes, but some scopes require *administrator* consent before use. The screen shot below shows a third app which cannot procede because it is asking to use a scope which requires admin consent. 24 | Consent dialog reporting that only an admin can grant permission 25 | 26 | To simplify the process of Admin consent, the dialog shows Admins checkbox to consent to the use of scopes and this pre-approves them for all users of the tenant. 27 | Consent dialog reporting that only an admin can grant permission 28 | 29 | You can grant consents to a published app - like *Microsoft Graph PowerShell* - via Enterpise Apps in Azure Active Directory, or you can create your own apps from App registrations and use them as an alternative. 30 | 31 | `Connect-MgGraph` provides **"Bring your own token"** support so can we skip the Device-logon process. `Connect-Graph` uses this to provide **three additional ways to obtain a token, without a dialog between user and logon service**. You can set a custom App ID for a username and password logon by calling `Set-GraphOptions -ClientID` in the same script which sets the scopes, the default script sets the client ID to the app ID of *Microsoft Graph PowerShell*. Some app IDs also require an associated secret to the log the user on and some don't. Removing the dialog means the logon service can't ask for consent for an app to use a set of scopes, so consent must have been given to the app beforehand. 32 | 33 | As well as users being *security principals*, who consent to some scoped permission being *delegated* to apps, apps themselves can be security principals and receive consent to act in their own right. If you provide your Tenant ID, the ID of an app registered in the tenant, and the app's secret, you can run `Connect-Graph -AsApp` to logon as the app. This is the second of the three extra logon methods: the *Microsoft Graph PowerShell* app isn't a security principal in your tenant, and your tenant doesn't have a secret (password) for it. So it can be used for username/password logon (without a secret) - but `-AsApp` logon requires a *registered* app. 34 | 35 | You can set the tenant ID in the settings script with `Set-GraphOptions -TenantID` and the secret with "`Set-GraphOptions -TenantID` If TenantID and client ID are set `Connect-Graph` enables the `Credential` parameter (which takes a credential object ) and if the client secret is set it enables the `AsApp` switch parameter 36 | 37 | The tenant id appears in many places including on a registered app's *overview* page, where *Client ID* also appears: 38 | Overview page for an app in Azure AD showing the App and Tenant IDs 39 | If you want to logon as the app you will need a secret which you add the from _certificates and Secrets_. 40 | The app secrets page 41 | You only get one chance to copy the whole secret - you can see mine begins "a1c". These 3 pieces of information go into the settings file. 42 | The auth-settings file with the secret, the client/app ID and the tenant id 43 | 44 | In *API Permissions* for a registered app you can click `+` and select the scopes that can be used by a logon using this app ID - often the scopes will be Microsoft Graph functions but for the example below I have used the Azure keyvault 45 | Vault Permissions showing the target URL and the scopes that may be selected 46 | At the top of the *permissions page* is the URL that will be called for the rest API - this forms part of the token request, and 'https://graph.microsoft.com' is hardcoded where we are using Microsoft Graph. To use this App ID to work with Key Vault, a script would specify that it wants to token for 'https://Vault.azure.com' instead. Below that the pages shows a choice: are these *delegated permissions* that the app can excercise for a user, or are they *App permissions* which it can excercise in its own right ?. In the screen shot I'm adding the only scope - to have access to the vault as the user - and this doesn't need to be authorized by administrator. Back on the permissions summary page we can see one of the graph scopes DOES need admin consent before it can be used, and the Microsoft Graph scopes are already pre-authorized. Clicking "Grant Adminsitrative consent" will pre-authorize the newly added scope for all users. 47 | Permissions showing a new scope pending admin consent and options which do and do not allow user consent 48 | 49 | **So, we can consent to scopes for both modes: logging as the app and logging on as a user.** 50 | 51 | If `Connect-Graph` has the ID of an app with some consent Azure AD it can also be run as `Connect-Graph -Credential $someCred` it calls the login service and says "A user has given me their credentials, please can I have an access token to use my scopes as that user." Since it is handles user credentials the PowerShell code needs to be trusted. 52 | 53 | The final method for logging on is to piggy back on an existing Azure session current versions of the Az.Accounts module (v.2.2.6 at the time of writing, but not the V1 release), include a command `Get-AzAccessToken` - Justin Grote spotted that this could be used. It appears that Az Accounts logs in with a well-known accountID ( "1950a258-227b-4e31-a9cf-717495945fc2" - "Microsoft Azure PowerShell") Calling `Get-AzAccessToken` appears to say to the login service "I'm already logged authorised for this user and app, can I have a new token targetting a different service." `Connect-Graph -FromAzureSession` calls `Get-AzAccessToken` for you. 54 | 55 | All three of the Bring-your-own-token extensions try to track expiry of the access token and get a new one. There are a few cases where this not happen automatically. Running invoke-graphRequest "v1.0/me" should update the token and give you information about the logged on account. 56 | 57 | ## Things which go wrong. 58 | 59 | 1. You don't set any information (via the settings file or otherwise). The `-Credential` and `-AsApp` paramters do not appear. 60 | 1. You use the wrong tennantID, so you try to login as you@yourdomain using my tenant. The logon process will set you straight on that. 61 | 1. You use a client (APP) ID that isn't known to your tenant, again the logon process will fail. 62 | 1. You try to use -AsApp login with a secret which doesn't match the client ID (for example you don't change the client ID from 14d82eec-204b-4c2f-b7e8-296a70dab67e) 63 | 1. You don't **grant the right scopes**. Either the application doesn't request them when doing an intereactive login (specify them in the settings file), or the app hasn't been assigned them. If you use the -FromAzureSession option you don't get to choose your client ID, and you can't extend the scopes. 64 | -------------------------------------------------------------------------------- /docs/AppPermSummary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jhoneill/MsftGraph/22d09cb61ede5cf3f969e812f723f7d5398d8568/docs/AppPermSummary.png -------------------------------------------------------------------------------- /docs/AuthSettings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jhoneill/MsftGraph/22d09cb61ede5cf3f969e812f723f7d5398d8568/docs/AuthSettings.png -------------------------------------------------------------------------------- /docs/DeviceLogon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jhoneill/MsftGraph/22d09cb61ede5cf3f969e812f723f7d5398d8568/docs/DeviceLogon.png -------------------------------------------------------------------------------- /docs/InteractiveConsent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jhoneill/MsftGraph/22d09cb61ede5cf3f969e812f723f7d5398d8568/docs/InteractiveConsent.png -------------------------------------------------------------------------------- /docs/Logon options.docx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jhoneill/MsftGraph/22d09cb61ede5cf3f969e812f723f7d5398d8568/docs/Logon options.docx -------------------------------------------------------------------------------- /docs/Logon options.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jhoneill/MsftGraph/22d09cb61ede5cf3f969e812f723f7d5398d8568/docs/Logon options.pdf -------------------------------------------------------------------------------- /docs/Relationships.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jhoneill/MsftGraph/22d09cb61ede5cf3f969e812f723f7d5398d8568/docs/Relationships.pdf -------------------------------------------------------------------------------- /docs/Relationships.vsdx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jhoneill/MsftGraph/22d09cb61ede5cf3f969e812f723f7d5398d8568/docs/Relationships.vsdx -------------------------------------------------------------------------------- /docs/UsersGroupsDirObjects.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jhoneill/MsftGraph/22d09cb61ede5cf3f969e812f723f7d5398d8568/docs/UsersGroupsDirObjects.jpg -------------------------------------------------------------------------------- /docs/VaultPerms.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jhoneill/MsftGraph/22d09cb61ede5cf3f969e812f723f7d5398d8568/docs/VaultPerms.png -------------------------------------------------------------------------------- /quick_publish.ps1: -------------------------------------------------------------------------------- 1 | [cmdletbinding(SupportsShouldProcess=$true)] 2 | param ( 3 | [Parameter(Mandatory=$true,Position=0)] 4 | $key 5 | ) 6 | 7 | Get-item *.dll | Remove-Item -ErrorAction stop 8 | if (Get-Item *.dll) {throw 'Remove DLLs' ; break} 9 | if (Select-String -Path .\Microsoft.Graph.PlusPlus.settings.ps1 -Pattern 'clientsecret\s+["''](?!xxx)|TenantID\s+["''](?!xxx)') { 10 | throw "Settings contains secrets!"; break 11 | } 12 | #two chances to find things in settings 13 | git update-index --no-skip-worktree .\Microsoft.Graph.PlusPlus.settings.ps1 14 | if (-not (( git status ) -match "nothing to commit, working tree clean")) {throw "Unclean !" ; break} 15 | 16 | 17 | $files = @{} 18 | (Import-PowerShellDataFile .\Microsoft.Graph.PlusPlus.psd1 ).filelist | ForEach-Object { 19 | $i = Get-Item $_ -ErrorAction stop 20 | $files[$i.FullName] = $true 21 | } 22 | 23 | #we will use git to bring these back 24 | Remove-Item .\.vscode\* -Force -Recurse 25 | Remove-Item .\.gitignore 26 | Get-ChildItem -Recurse -File -Exclude $MyInvocation.MyCommand.name, .\.git | 27 | Where-Object {-not $files[$_.FullName] } | 28 | Remove-Item 29 | if ($PSCmdlet.ShouldProcess('Publish')) { 30 | Publish-Module -NuGetApiKey $key -Repository PSGallery -AllowPrerelease -Name Microsoft.Graph.PlusPlus 31 | } 32 | git reset HEAD --hard 33 | git update-index --skip-worktree .\Microsoft.Graph.PlusPlus.settings.ps1 34 | --------------------------------------------------------------------------------