Uploading a Terms of Use PDF via Powershell to Azure using Graph - powershell

I've just started trying to use the MgGraph module to try and perform operations on an Office 365 tenant to perform some configuration automation and I'm having a hard time wrapping my head around some of these cmdlets - in particular, the one I'm currently messing around with, New-MgAgreement, in an attempt to automatically upload a terms of use file.
I guess, for starters, is this a viable path to achieving this? Is there some other method I should attempt to be using to try and accomplish this? And if not, I guess how do I go about actually doing this? In short, this is what I've attempted:
I'm utilizing a file picker via Windows Forms to prompt to capture the file I wish to upload
I've created a hash for the File parameter of the cmdlet to satisfy the properties needed (I thought...) as well as filling out the other parameters needed to provide information about the terms of use object being created on the tenant
When I try to run the command, I was initially told that "value cannot be null - parameter name source" which makes sense because I didn't see any parameters specifying source in the documentation for the physical file, so I tried simply adding a "source" parameter to the New-MgAgreement cmdlet thinking I just missed it in the documentation, but upon trying to do that, it tells me "a parameter cannot be found that matches parameter name 'source'.
Just to give an idea of my mindset when trying to achieve this, this is a code snippet hopefully illustrating the direction I'm trying to go about doing this:
Function Main {
Connect-Modules
Set-Tenant-Terms-Of-Use
Get-Tenant-Terms-Of-Use
}
Function Connect-Modules {
Connect-AzureAD
$tenantId = Get-AzureADTenantDetail | Select-Object ObjectId
C:\Windows\System32\cmd.exe /c start shell:AppsFolder\Microsoft.MicrosoftEdge_8wekyb3d8bbwe!MicrosoftEdge -private https://microsoft.com/devicelogin
Connect-MgGraph -Scopes "User.Read.All", "Group.ReadWrite.All", "Agreement.Read.All", "Agreement.ReadWrite.All" -TenantId $tenantId.ObjectId.ToString()
}
Function Set-Tenant-Terms-Of-Use {
$FileBrowser = New-Object System.Windows.Forms.OpenFileDialog -Property #{
InitialDirectory = [Environment]::GetFolderPath('Desktop')
Filter = 'PDF (*.pdf)|*.pdf'
}
$companyName = Read-Host "Please enter your company name: "
$null = $FileBrowser.ShowDialog()
$fileHash = #{
DisplayName = "All users terms of use";
FileName = $FileBrowser.SafeFileName
IsDefault = $true
IsMajorVersion = $true
Language = "English (Default)"
}
New-MgAgreement -DisplayName "$companyName Terms of Use" -File $fileHash -source $FileBrowser.FileName
}
Function Get-Tenant-Terms-Of-Use {
$termsOfUse = Get-MgAgreement | Select-Object Id, DisplayName
Write-Host $termsOfUse.Id.ToString()
Write-Host $termsOfUse.DisplayName.ToString()
}
Main
Disconnect-MgGraph
I'll admit... I'm kind of completely lost on this one - I haven't really worked with Graph previously and I'm trying to get the hang of it since it seems to have a lot of capabilities that hook into areas of an Azure/O365 tenant that the other modules I'm used to using do not. I appreciate any schooling I can get on the topic! Thank you!
EDIT - So I can get this to work the way I want via Graph Explorer... I realized I needed to convert my file to base64 and put the value under the data property under the FileData parameter from within the File Parameter... but when I try to mirror this in PowerShell using the same parameter values as what is included in Graph Explorer I just get a "New-MgAgreement: Value cannot be null. Parameter name: source"... so it's just that I don't know how to format this using the cmdlet... and I just can't wrap my head around how to use the complex parameters apparently given the documentation.

I was able to reproduce this issue. As an alternative, you can use Invoke-MgGraphRequest passing in the agreements endpoint and upload the agreement - this worked for me see below:
Agreement Resource Sample Data
$data
{
"displayName": "MSGraph Sample",
"isViewingBeforeAcceptanceRequired": true,
"files": [
{
"fileName": "TOU.pdf",
"language": "en",
"isDefault": true,
"fileData": {
"data": "SGVsbG8gd29ybGQ="
}
}
]
}
#Using IGR
Invoke-MgGraphRequest -Uri https://graph.microsoft.com/v1.0/agreements -Method POST -Body $data
Please let me know if this helps and if you have further questions.

Related

MS Graph API - Group & membership info

I'm trying to pull out a listing of all groups in our Azure Active Directory org along with all the associated members (be them users, groups, contacts, etc).
Since I was unable to locate a method to do this through the various Microsoft portals with a simple export button I began the process of obtaining access to the Microsoft Graph API/SDK via Powershell.
I'm by no means a PowerShell expert as it's not one of my go-to scripts; however, from what I can tell the ability to pull group info in this fashion is fairly limited.
The following is what I've been able to accomplish thus far:
Pull in a list of the groups using Get-MgGroup -All
Use Get-MgGroupMembers to pull back a list of Directory Objects.
This is where I get stuck. From what I've read it looks like a Directory Object by default only returns the ID and the Deleted Date. I'd like to get a display Name for these objects; I can obviously do this by running the appropriate 'Get' cmdlet for the type of directory object (i.e. Get-MgUser); From what I can tell the type of directory object can't be gleaned via PowerShell with out 'trial-and-error'... This seems highly inefficient to simply get a displayName.
Is there a more effective way to determine either the displayName of a Directory Object via a PowerShell cmdlet or at the very least a type so I can write a case statement to run the right cmdlet on the first try?
For the record this is going to be incorporated in to a Powershell Script, the current iteration of which looks like this and sorta works okay... assuming the Id passed in $member.Id belongs to a User type directory object.
Connect-MgGraph
$groups=Get-mgGroup -All
ForEach ($group in $groups){
$members = #{}
$members = Get-MgGroupMember -GroupId $group.Id -All
ForEach ($member in $members){
$user = Get-MgUser $member.Id
Write-Output $object.ODataType
Write-output $group.DisplayName "," $member.Id "," $user.UserType"," $user.DisplayName "," $user.UserPrincipalName "," $user.Mail >> C:scripts\Azure_Groups.txt
}
}
Would appreciate any direction/assistance on this. Thanks in advance!
Not sure why its not returning all the details on the PowerShell query:
This is working fine in MS Graph Explorer with the results showing all the details of the members:
For more details:https://learn.microsoft.com/en-us/graph/api/group-list-members?view=graph-rest-1.0&tabs=http#example-1-get-the-direct-membership-in-a-group

Use Connect-PnPOnline in a workflow

Good morning, everyone,
I have a problem in retrieving information from SharePoint groups. I do it in a workflow, first I get the list of all groups and then I use it in a foreach-parallel to list the members of these groups.
The problem is that the connection doesn't seem to be maintaining and my query doesn't recover all the groups.
Here is a piece of the code:
workflow GetGroup {
param(
[Parameter(Mandatory)]
[String]$SPOSite,
[System.Management.Automation.PSCredential]$SPOCreds
)
$KeepAlive = Connect-PnPOnline -Url $SPOSite -Credentials $SPOCreds -ReturnConnection
$GetGroups = Get-PnPGroup
ForEach -Parallel -ThrottleLimit 512 ($Group in $GetGroups)
{
$GroupName = $Group.LoginName
$Users = Get-PnPGroupMembers -Identity $groupName -Connection $workFlow:KeepAlive
}
}
GetGroup -SPOSite "https://xxx.sharepoint.com/sites/xx -SPOCreds (Get-Credential)
The expected result would be an array initialized at the beginning of the workflow with a PSCustomObject object that is added to our array. This table is made up of 3 things: The name of the group, the names of the people in that group, the emails of the people.
Unfortunately the table is only partially generated because a workflow here is the error I find nothing on the subject:
Impossible to link the "Connection" parameter. Impossible to convert the value "SharePointPnP.PowerShell.Commands.Base.SPOnlineConnection" of the type "SharePointPnP.PowerShell.Commands.
Deserialized.SharePointPnPnP.PowerShell.Commands.Base.SPOnlineConnection"
Thank you for your help.
I ran into the same error in a similar scenario, and this article helped me. Workflow is converting the Connection object ($KeepAlive in your case) to a deserialized format, and so the other cmdlet doesn't accept it. You will just have to wrap those cmdlets with InlineScript, or use a PowerShell script runbook instead of a PowerShell workflow runbook.

Setting a DateTime to $null/empty using PowerShell and the SCSM cmdlets

I'm currently trying to sync additional attributes from the AD (Active Directory) for user objects in SCSM (System Center Service Manager) using a PowerShell script.
The extension I wrote for this, includes an attribute for the expiration date of a AD user account (DateTime value, named DateTimeAttribute in the example) if the user account doesn't expire it should be empty/null.
Using Import-SCSMInstance, which should be similar to a CSV import, it kind of works by passing "null" for the field. The problem is that Import-SCSMInstance seems to be quite unreliable and it doesn't offer any kind of information of why it works or doesn't work. Using Update-SCSMClassInstance seems to work a lot better but I can't figure out a way to clear the field using this and even using [DateTime]::MinValue will result in an error, stating that it's an invalid value.
So would anyone have an idea on how to clear the value using Update-SCSMClassInstance or figure out why Import-SCSMInstance might or might not work?
A simple example for this could look like the following:
$server = "<ServerName>"
$extensionGuid = "<GUID>"
Import-Module 'C:\Program Files\System Center 2012 R2\Service Manager\Powershell\System.Center.Service.Manager.psd1'
New-SCManagementGroupConnection -ComputerName $server
$extensionClass = Get-SCSMClass -Id $extensionGuid
$scsmUserObject = Get-SCSMClassInstance -Class $extensionClass -Filter 'UserName -eq "<User>"'
# Error
$scsmUserObject.DateTimeAttribute = ""
# Works but fails on Update-SCSMClassInstance
$scsmUserObject.DateTimeAttribute = $null
$scsmUserObject.DateTimeAttribute = [DateTime]::MinValue
# Works
$scsmUserObject.DateTimeAttribute = "01-01-1970"
Update-SCSMClassInstance -Instance $scsmUserObject
It seems that you can't clear a date once it's filled. When you write $null, it sets the date to 1-1-0001 01:00:00, which is an invalid date causing the update-scsmclassinstance to fail.
What we have set as a user's AD property when we don't want something to expire, is 2999-12-31 (yyyy-MM-dd). Perhaps this could help, although it's not directly what you asked for...
Also, you can use the pipeline to update a property in SCSM:
Get-SCSMClassInstance -Class $extensionClass -Filter 'UserName -eq "<User>"' | % {$_.DateTimeAttribute = <date>; $_} | update-scsmclassinstance
It doesn't look like it's currently possible to clear custom date attributes using the PowerShell cmdlets.

HTMLDocumentClass and getElementsByClassName not working

Last year I had powershell (v3) script that parsed HTML of one festival page (and generate XML for my Windows Phone app).
I also was asking a question about it here and it worked like a charm.
But when I run the script this year, it is not working. To be specific - the method getElemntsByClassName is not returning anything. I tried that method also on other web pages with no luck.
Here is my code from last year, that is not working now:
$tmpFile_bandInfo = "C:\band.txt"
Write-Host "Stahuji kapelu $($kap.Nazev) ..." -NoNewline
Invoke-WebRequest http://www.colours.cz/ucinkujici/the-asteroids-galaxy-tour/ -OutFile $tmpFile_bandInfo
$content = gc $tmpFile_bandInfo -Encoding utf8 -raw
$ParsedHtml = New-Object -com "HTMLFILE"
$ParsedHtml.IHTMLDocument2_write($content)
$ParsedHtml.Close()
$bodyK = $ParsedHtml.body
$bodyK.getElementsByClassName("body four column page") # this returns NULL
$page = $page.item(0)
$aside = $page.getElementsByTagName("aside").item(0)
$img = $aside.getElementsByTagName("img").item(0)
$imgPath = $img.src
this is code I used to workaround this:
$sec = $bodyK.getElementsByTagName("section") | ? ClassName -eq "body four column page"
# but now I have no innerHTML, only the lonely tag SECTION
# so I am walking through siblings
$img = $sec.nextSibling.nextSibling.nextSibling.getElementsByTagName("img").item(0)
$imgPath = $img.src
This works, but this seems silly solution to me.
Anyone knows what I am doing wrong?
I actually solved this problem by abandoning Invoke-WebRequest cmdlet and by adopting HtmlAgilityPack.
I transformed my former sequential HTML parsing into few XPath queries (everything stayed in powershell script). This solution is much more elegant and HtmlAgilityPack is real badass ;) It is really honour to work with project like this!
The issue is not a bug but rather that the return where you're seeing NULL is because it's actually a reference to a proxy HTMLFile COM call to the DOM model.
You can force this to operate and return the underlying strings by boxing it into an array #() as such:
#($mybody.getElementsByClassName("body four column page")).textContent
If you do a Select-Object on it, that also automatically happens and it will unravel it via COM and return it as a string
$mybody.getElementsByClassName("body four column page") | Select-Object -Property TextContent

Powershell function parameters with multiple words/values

I'm new to powershell and my first module is for simply adding users to the local admin group on remote computers. It looks like:
function AddAdmin {
[CmdletBinding()]
Param(
[Parameter (Mandatory=$True,ValueFromPipeline=$True,Position=1) ]
[string[]]$Computer,
[Parameter (Mandatory=$True,ValueFromPipeline=$True,Position=2) ]
[string]$username
)
$Domain = "the domain"
$Group = [ADSI]"WinNT://$Computer/Administrators,group"
$Usertoadd = [ADSI]"WinNT://$Domain/$username,user"
$Group.Add($Usertoadd.Path)
}
so I can just type addadmin computername username and it gets added. I want to do the same for groups, the problem I'm having is figuring out how to set a parameter that has multiple values/words. For example let's say I want to add a group called Executive Team to local admins. addadmin computername executive team doesn't work - it only picks up executive as the value.
Googled quite a bit and can't seem to figure this out, I'm sure I'm missing something simple.
You just have to put the multiple words value into double quotes :
addadmin computername "executive team"
Positions start at 0, just FYI, and while JPBlanc's answer is correct (and honestly better from a technical standpoint) you should be able to add this to your Parameter list for the User Name to get the same results without having to put them in quotes.
ValueFromRemainingArguments = $true