Yamautomate.IAM.psm1
Function New-YcIAMAdUser { param ( [Parameter(Mandatory=$true)] [string]$firstname, [Parameter(Mandatory=$true)] [string]$lastname, [Parameter(Mandatory=$true)] [string]$location, [Parameter(Mandatory=$true)] [string]$department, [Parameter(Mandatory=$true)] [string]$team, [Parameter(Mandatory=$true)] [string]$phoneNumber, [Parameter(Mandatory=$true)] [string]$jobTitle, [Parameter(Mandatory=$true)] [string]$manager, [Parameter(Mandatory=$false)][string]$PathToConfig = "$env:USERPROFILE\.yc\YcIAMSampleConfig.json", [Parameter(Mandatory=$false)][string]$EventLogSource, [Parameter(Mandatory=$false)][bool]$LogEnabled = $true ) #Check for required modules #Requires -Modules @{ModuleName='Yamautomate.Core';ModuleVersion='1.0.6.4'} <# #Requires -Modules @{ModuleName='ActiveDirectory';ModuleVersion='1.0.1.0'} #> try { $requiredModules = @("ActiveDirectory") Get-YcRequiredModules -moduleNames $requiredModules -ErrorAction Stop } catch { throw ("Could not import needed modules. Aborting. Error Details: "+$_.Exception.Message) } #Import config and map values to variables try { $config = Get-Content -raw -Path $PathToConfig | ConvertFrom-Json -ErrorAction Stop $locationForLookup = "Location-"+$location $Street = $config.$locationForLookup.Street $City = $config.$locationForLookup.City $ZIPCode = $config.$locationForLookup.ZIPCode $Country = $config.$locationForLookup.Country $CountryPhone = $config.$locationForLookup.Phone $TopLevelDomain = $config.$locationForLookup.TopLevelDomain $OU = $config.ActiveDirectory.OU $rawDomainName = $config.ActiveDirectory.rawDomainName $SwapDomainsForEmailAlias = $config.ActiveDirectory.SecondarySMTPAlias $SetOfficeIpPhone = $config.ActiveDirectory.SetOfficeIpPhone $strUserEnabled = $config.ActiveDirectory.NewUserEnabled $strChangePasswordAtLogon = $config.ActiveDirectory.ChangePasswordAtLogon #Initialize Booleans with default values $PathToLogFile = "$env:USERPROFILE\.yc" [bool]$ChangePasswordAtLogon = $false [bool]$UserEnabled = $false [bool]$LogToEventLog = $false [bool]$LogToLogFile = $false [bool]$LogToOutput = $false [bool]$LogToHost = $true } catch { throw ("Could not grab contents of ConfigFile. Aborting. Error Details: "+$_.Exception.Message) } #Grab values from config if Log is enabled If ($LogEnabled) { $strLogToEventLog = $config.EventLogging.LogToEventlog $strLogToLogFile = $config.EventLogging.LogToLogFile $strLogToOutput = $config.EventLogging.LogToOutput $strLogToHost = $config.EventLogging.LogToHost $strPathToLogFile = $config.EventLogging.PathToLogFile if ($strPathToLogFile) { $PathToLogFile = $strPathToLogFile } else { $PathToLogFile = "$env:USERPROFILE\.yc" } If ($strLogToEventLog -eq "true") { [bool]$LogToEventLog = $true } if ($strLogToLogFile -eq "true") { [bool]$LogToLogFile = $true } if ($strLogToOutput -eq "true") { [bool]$LogToOutput = $true } if ($strLogToHost -eq "true") { [bool]$LogToHost = $true } } #Make sure there is a Event Source we can post among If (!($EventLogSource)) { $EventLogSource = $config.EventLogging.NameOfEventSource If (!($EventLogSource) -or $EventLogSource -eq " ") { $EventLogSource = "Yc.IAM-New-AdUser" } } # Construct the user’s full name and username with the location TLD $displayName = "$firstname $lastname" $samAccountName = "$firstname.$lastname" $primaryEmail = "$firstname.$lastname@$rawDomainname$TopLevelDomain" Write-YcLogMessage ("Primary E-Mail Address for new User is: "+$primaryEmail) -source $EventLogSource -ToEventLog $LogToEventLog -ToLogFile $LogToLogFile -ToOutput $LogToOutput -WriteHost $LogToHost -logDirectory $PathToLogFile # Create the Active Directory user try { #Create Random Password $InitialPw = New-YcRandomPassword -length 14 If ($StrUserEnabled -eq "true") { [bool]$UserEnabled = $true } If ($strChangePasswordAtLogon -eq "true") { [bool]$ChangePasswordAtLogon = $true } New-ADUser ` -GivenName $firstname ` -Surname $lastname ` -Name $displayName ` -SamAccountName $samAccountName ` -UserPrincipalName $primaryEmail ` -Path $OU ` -Division $team ` -OfficePhone $phoneNumber ` -Title $jobTitle ` -Department $department ` -DisplayName $displayName ` -StreetAddress $Street ` -City $City ` -PostalCode $ZIPCode ` -Country $Country ` -Enabled $UserEnabled ` -AccountPassword (ConvertTo-SecureString $InitialPw -AsPlainText -Force) ` -ChangePasswordAtLogon $ChangePasswordAtLogon ` -EmailAddress $primaryEmail Write-YcLogMessage ("Successfully created new AD User: "+$samAccountName) -source $EventLogSource -ToEventLog $LogToEventLog -ToLogFile $LogToLogFile -ToOutput $LogToOutput -WriteHost $LogToHost -logDirectory $PathToLogFile } catch { Write-YcLogMessage ("Could not create AD User. Aborting. Error Details: "+$_.Exception.Message) Error -source $EventLogSource -ToEventLog $LogToEventLog -ToLogFile $LogToLogFile -ToOutput $LogToOutput -WriteHost $LogToHost -logDirectory $PathToLogFile throw ("Could not create AD User. Aborting. Error Details: "+$_.Exception.Message) } # Add organization tab information try { Set-ADUser -Identity $samAccountName -Title $jobTitle -Department $department -Manager $manager Write-YcLogMessage ("Successfully set organizational info on new AD User: "+$samAccountName) -source $EventLogSource -ToEventLog $LogToEventLog -ToLogFile $LogToLogFile -ToOutput $LogToOutput -WriteHost $LogToHost -logDirectory $PathToLogFile } catch { Write-YcLogMessage ("Could not set organization tab info on AD User. Aborting. Error Details: "+$_.Exception.Message) -source $EventLogSource -ToEventLog $LogToEventLog -ToLogFile $LogToLogFile -ToOutput $LogToOutput -WriteHost $LogToHost -logDirectory $PathToLogFile throw ("Could not set organization tab info on AD User. Aborting. Error Details: "+$_.Exception.Message) } #Do we need to add second SMTP Alias? if ($SwapDomainsForEmailAlias -eq "true") { Write-YcLogMessage ("SecondarySMTPAlias is enabled.") -source $EventLogSource -ToEventLog $LogToEventLog -ToLogFile $LogToLogFile -ToOutput $LogToOutput -WriteHost $LogToHost -logDirectory $PathToLogFile $secondaryEmailTLD = $config.ActiveDirectory.MakeSecondary if ($secondaryEmailTLD -eq $TopLevelDomain) { $secondaryEmailTLD = $config.ActiveDirectory.SwapWith } $secondaryEmail = "$firstname.$lastname@$rawDomainname$secondaryEmailTLD" Write-YcLogMessage ("SecondarySMTPAlias is: "+$secondaryEmail) -source $EventLogSource -ToEventLog $LogToEventLog -ToLogFile $LogToLogFile -ToOutput $LogToOutput -WriteHost $LogToHost $proxyAddresses = @("SMTP:$primaryEmail", "smtp:$secondaryEmail") # Add proxy addresses to the user try { Set-ADUser -Identity $samAccountName -Add @{proxyAddresses=$proxyAddresses} Write-YcLogMessage ("Successfully set proxy addresses on new AD User: "+$samAccountName) -source $EventLogSource -ToEventLog $LogToEventLog -ToLogFile $LogToLogFile -ToOutput $LogToOutput -WriteHost $LogToHost -logDirectory $PathToLogFile } catch { Write-YcLogMessage ("Could not set proxy address on AD User. Aborting. Error Details: "+$_.Exception.Message) Error -source $EventLogSource -ToEventLog $LogToEventLog -ToLogFile $LogToLogFile -ToOutput $LogToOutput -WriteHost $LogToHost -logDirectory $PathToLogFile throw ("Could not set proxy address on AD User. Aborting. Error Details: "+$_.Exception.Message) } } #Wp we need to set country-specific main phone number if ($SetOfficeIpPhone -eq "true") { try { Set-ADUser -Identity $samAccountName -Replace @{ipPhone=$CountryPhone} Write-YcLogMessage ("Successfully set Ip Phone on: "+$samAccountName) -source $EventLogSource -ToEventLog $LogToEventLog -ToLogFile $LogToLogFile -ToOutput $LogToOutput -WriteHost $LogToHost -logDirectory $PathToLogFile } catch { Write-YcLogMessage ("Could not set country-specific main phone number on AD User. Aborting. Error Details: "+$_.Exception.Message) Error -source $EventLogSource -ToEventLog $LogToEventLog -ToLogFile $LogToLogFile -ToOutput $LogToOutput -WriteHost $LogToHost -logDirectory $PathToLogFile throw ("Could not set country-specific main phone number on AD User. Aborting. Error Details: "+$_.Exception.Message) } } Write-YcLogMessage ("---------------------------------------------------------------") -source $EventLogSource -ToEventLog $LogToEventLog -ToLogFile $LogToLogFile -ToOutput $LogToOutput -WriteHost $LogToHost -logDirectory $PathToLogFile return $InitialPw } Function New-YcIAMTeamsPhoneNumberAssignment { param ( [Parameter(Mandatory=$true)] [string]$phoneNumber, [Parameter(Mandatory=$true)] [string]$firstname, [Parameter(Mandatory=$true)] [string]$lastname, [Parameter(Mandatory=$true)] [string]$location, [Parameter(Mandatory=$false)][string]$PathToConfig = "$env:USERPROFILE\.yc\YcIAMSampleConfig.json", [Parameter(Mandatory=$false)][string]$EventLogSource, [Parameter(Mandatory=$false)][bool]$LogEnabled = $true ) #Check for required modules #Requires -Modules @{ModuleName='Yamautomate.Core';ModuleVersion='1.0.6.4'} <# #Requires -Modules @{ModuleName='MicrosoftTeams';ModuleVersion='6.5.0'} #> try { $requiredModules = @("MicrosoftTeams", "Yamautomate.Core") Get-YcRequiredModules -moduleNames $requiredModules -ErrorAction Stop } catch { throw ("Could not import needed modules. Aborting. Error Details: "+$_.Exception.Message) } #Import config and map values to variables try { $config = Get-Content -raw -Path $PathToConfig | ConvertFrom-Json -ErrorAction Stop $locationForLookup = "Location-"+$location $TopLevelDomain = $config.$locationForLookup.TopLevelDomain $CertificateThumbprint = $config.TeamsPhone.CertificateThumbprint $tenantId = $config.AzureGeneral.tenantId $appId = $config.TeamsPhone.AzureAppRegistrationClientId $rawDomainName = $config.ActiveDirectory.rawDomainName $policyname = $config.TeamsPhone.PolicyName $identity = $firstname+"."+$lastname+"@"+$rawDomainName+$TopLevelDomain } catch { throw ("Could not grab contents of ConfigFile. Aborting. Error Details: "+$_.Exception.Message) } #Initialize Logging vars [bool]$LogToEventLog = $false [bool]$LogToLogFile = $false [bool]$LogToOutput = $false [bool]$LogToHost = $true #Grab values from config if Log is enabled If ($LogEnabled) { $strLogToEventLog = $config.EventLogging.LogToEventlog $strLogToLogFile = $config.EventLogging.LogToLogFile $strLogToOutput = $config.EventLogging.LogToOutput $strLogToHost = $config.EventLogging.LogToHost $strPathToLogFile = $config.EventLogging.PathToLogFile if ($strPathToLogFile) { $PathToLogFile = $strPathToLogFile } else { $PathToLogFile = "$env:USERPROFILE\.yc" } If ($strLogToEventLog -eq "true") { [bool]$LogToEventLog = $true } if ($strLogToLogFile -eq "true") { [bool]$LogToLogFile = $true } if ($strLogToOutput -eq "true") { [bool]$LogToOutput = $true } if ($strLogToHost -eq "true") { [bool]$LogToHost = $true } } #Make sure there is a Event Source we can post among If (!($EventLogSource)) { $EventLogSource = $config.EventLogging.NameOfEventSource If (!($EventLogSource) -or $EventLogSource -eq " ") { $EventLogSource = "Yc.IAM-New-AdUser" } } try { Connect-MicrosoftTeams -TenantId $tenantId -Certificate $CertificateThumbprint -ApplicationId $appId Write-YcLogMessage ("Successfully connected to Teams Online using Certificate.") -source $EventLogSource -ToEventLog $LogToEventLog -ToLogFile $LogToLogFile -ToOutput $LogToOutput -WriteHost $LogToHost -logDirectory $PathToLogFile } catch { Write-YcLogMessage ("Could not connect to Teams. Aborting. Error Details: "+$_.Exception.Message) Error -source $EventLogSource -ToEventLog $LogToEventLog -ToLogFile $LogToLogFile -ToOutput $LogToOutput -WriteHost $false -logDirectory $PathToLogFile throw ("Could not connect to Teams. Aborting. Error Details: "+$_.Exception.Message) } try { Write-YcLogMessage ("Trying to assign policy: "+$policyname+" to user: "+$identity) -source $EventLogSource -ToEventLog $LogToEventLog -ToLogFile $LogToLogFile -ToOutput $LogToOutput -WriteHost $LogToHost -logDirectory $PathToLogFile Set-CsPhoneNumberAssignment -Identity $identity -PhoneNumber $phoneNumber -PhoneNumberType DirectRouting Grant-CsOnlineVoiceRoutingPolicy -Identity $identity -PolicyName $policyname Grant-CsTeamsUpgradePolicy -Identity $identity -PolicyName UpgradeToTeams Write-YcLogMessage ("Successfully assigned policy: "+$policyname+" to user: "+$identity) -source $EventLogSource -ToEventLog $LogToEventLog -ToLogFile $LogToLogFile -ToOutput $LogToOutput -WriteHost $LogToHost -logDirectory $PathToLogFile } catch { Write-YcLogMessage ("Could not connect to assign phoneNumber. Aborting. Error Details: "+$_.Exception.Message) Error -source $EventLogSource -ToEventLog $LogToEventLog -ToLogFile $LogToLogFile -ToOutput $LogToOutput -WriteHost $false -logDirectory $PathToLogFile throw ("Could not assign phoneNumber. Aborting. Error Details: "+$_.Exception.Message) } finally { Disconnect-MicrosoftTeams } Write-YcLogMessage ("---------------------------------------------------------------") -source $EventLogSource -ToEventLog $LogToEventLog -ToLogFile $LogToLogFile -ToOutput $LogToOutput -WriteHost $LogToHost -logDirectory $PathToLogFile } function New-YcIAMWelcomeLetterFromTemplate { param ( [Parameter(Mandatory=$true)] [string]$templatePath, [Parameter(Mandatory=$true)] [string]$FirstName, [Parameter(Mandatory=$true)] [string]$LastName, [Parameter(Mandatory=$true)] [string]$InitialPassword, [Parameter(Mandatory=$true)] [string]$location, [Parameter(Mandatory=$false)][string]$PathToConfig = "$env:USERPROFILE\.yc\YcIAMSampleConfig.json", [Parameter(Mandatory=$false)][hashtable]$CustomPlaceholders = @{} # Custom placeholders from config or parameters ) #Import config and map values to variables try { $config = Get-Content -raw -Path $PathToConfig | ConvertFrom-Json -ErrorAction Stop $locationForLookup = "Location-"+$location $TopLevelDomain = $config.$locationForLookup.TopLevelDomain $rawDomainName = $config.ActiveDirectory.rawDomainName $identity = ($firstname.ToLower())+"."+($lastname.ToLower())+"@"+$rawDomainName+$TopLevelDomain Write-YcLogMessage "Successfully read config." -WriteHost $true } catch { throw ("Could not grab contents of ConfigFile. Aborting. Error Details: "+$_.Exception.Message) } # Check if the templatePath is a SharePoint URL if ($templatePath -match "^https:\/\/.*\.sharepoint\.com") { try { Write-YcLogMessage "Detected SharePoint link. Trying to download file..." -WriteHost $true $siteurl = $config.WelcomeLetter.SharePointSiteURL Connect-PnPOnline -Url $siteUrl -Thumbprint ($config.WelcomeLetter.CertificateThumbprint) -ClientId ($config.WelcomeLetter.AzureAppRegistrationClientId) -Tenant ($config.AzureGeneral.TenantId) # Download the file from SharePoint $localTemplatePath = "$env:TEMP\" + (Split-Path -Leaf $templatePath) # Store in temp folder Get-PnPFile -Url $templatePath -Path $localTemplatePath -FileName (Split-Path -Leaf $templatePath) -AsFile Write-YcLogMessage "Successfully downloaded file from SharePoint." -WriteHost $true # Update $templatePath to use the downloaded local file $templatePath = $localTemplatePath } catch { throw ("Failed to download the file from SharePoint. Error: " + $_.Exception.Message) } } # Check if the file exists if (-not (Test-Path -Path $templatePath)) { throw ("Template does not exist at specified path.") } #Create working copy $templateBasePath = Split-Path $templatePath $workingcopyPath = $templateBasePath+"\WL_"+$FirstName+"_"+$LastName+".docx" Copy-Item -Path $templatePath -Destination $workingcopyPath $templatePath = $workingcopyPath Write-YcLogMessage ("Path to copied item from template: "+$workingcopyPath) -WriteHost $true try { # Build the path to the DLL relative to the module base directory $openXmlDll = Join-Path -Path $PSScriptRoot -ChildPath "lib\DocumentFormat.OpenXml.dll" $openXmlFramework = Join-Path -Path $PSScriptRoot -ChildPath "lib\DocumentFormat.OpenXml.Framework.dll" # Load the DLLs if not already loaded if (-not ([System.AppDomain]::CurrentDomain.GetAssemblies() | Where-Object { $_.Location -eq $openXmlDll })) { Add-Type -Path $openXmlDll } if (-not ([System.AppDomain]::CurrentDomain.GetAssemblies() | Where-Object { $_.Location -eq $openXmlFramework })) { Add-Type -Path $openXmlFramework } Write-YcLogMessage ("Successfully loaded required DLLs from path: "+$PSScriptRoot+"\lib\") -WriteHost $true } catch {throw ("Could not load required .DLLs. Aborting. Error Details: "+$_.Exception.Message)} # Create the search and replace text # Default placeholders $defaultPlaceholders = @{ 'EMAIL' = $identity 'INITPASS' = $InitialPassword 'FIRSTNAME' = $FirstName 'LASTNAME' = $LastName } # Merge custom placeholders (from config or parameters) with default placeholders $placeholders = $defaultPlaceholders + $CustomPlaceholders try { # Open the Word document for editing Write-YcLogMessage ("Trying to open .docx file using OpenXML .DLL...") -WriteHost $true [Reflection.Assembly]::LoadFrom($openXmlDll) | Out-Null $doc = [DocumentFormat.OpenXml.Packaging.WordprocessingDocument]::Open($templatePath, $true) Write-YcLogMessage ("Opened file. Trying to read text..") -WriteHost $true # Get the main document part $mainPart = $doc.MainDocumentPart $docText = '' # Read the document text $reader = [System.IO.StreamReader]::new($mainPart.GetStream()) try {$docText = $reader.ReadToEnd()} finally {$reader.Dispose()} Write-YcLogMessage ("Successfully read text from .docx") -WriteHost $true } catch {throw ("Could not read file content using System.IO.StreamReader. Aborting. Error Details: "+$_.Exception.Message)} try { # Iterate over each placeholder and replace it with the appropriate value foreach ($placeholder in $placeholders.GetEnumerator()) { Write-YcLogMessage ("Processing placeholder: "+$placeholder) -WriteHost $true $docText = $docText -replace $placeholder.Key, $placeholder.Value } # Write the updated text back to the document Write-YcLogMessage ("Writing updatet text back to working copy...") -WriteHost $true $writer = [System.IO.StreamWriter]::new($mainPart.GetStream([System.IO.FileMode]::Create)) try {$writer.Write($docText)} finally {$writer.Dispose()} } catch {throw ("Could not replace placeholder text using System.IO.StreamWriter. Aborting. Error Details: "+$_.Exception.Message)} finally {$doc.Dispose()} } function New-YcIAMSCIMRequest { [Parameter(Mandatory=$false)][string]$PathToConfig = "$env:USERPROFILE\.yc\YcIAMSampleConfig.json" #Requires -Modules @{ModuleName='Microsoft.Graph.Applications';ModuleVersion='2.23.0'} #Import config and map values to variables try { $config = Get-Content -raw -Path $PathToConfig | ConvertFrom-Json -ErrorAction Stop $PathToMappingFile = $config.EntraAPIprovisioning.PathToMappingFile $PathToCsv = $config.EntraAPIprovisioning.PathToImportCSV $APIProv_APIAppServicePrincipalId = $config.EntraAPIprovisioning.APIAppServicePinricipalId $APIProv_AzureAppRegistrationClientId = $config.EntraAPIprovisioning.AzureAppRegistrationClientId $APIProv_CertificateThumbprint = $config.EntraAPIprovisioning.CertificateThumbprint $tenantId = $config.AzureGeneral.tenantId #Initialize Booleans with default values $PathToLogFile = "$env:USERPROFILE\.yc" [bool]$LogToEventLog = $false [bool]$LogToLogFile = $false [bool]$LogToOutput = $false [bool]$LogToHost = $true } catch { throw ("Could not grab contents of ConfigFile. Aborting. Error Details: "+$_.Exception.Message) } #Grab values from config if Log is enabled If ($LogEnabled) { $strLogToEventLog = $config.EventLogging.LogToEventlog $strLogToLogFile = $config.EventLogging.LogToLogFile $strLogToOutput = $config.EventLogging.LogToOutput $strLogToHost = $config.EventLogging.LogToHost $strPathToLogFile = $config.EventLogging.PathToLogFile if ($strPathToLogFile) { $PathToLogFile = $strPathToLogFile } else { $PathToLogFile = "$env:USERPROFILE\.yc" } If ($strLogToEventLog -eq "true") { [bool]$LogToEventLog = $true } if ($strLogToLogFile -eq "true") { [bool]$LogToLogFile = $true } if ($strLogToOutput -eq "true") { [bool]$LogToOutput = $true } if ($strLogToHost -eq "true") { [bool]$LogToHost = $true } } #Make sure there is a Event Source we can post among If (!($EventLogSource)) { $EventLogSource = $config.EventLogging.NameOfEventSource If (!($EventLogSource) -or $EventLogSource -eq " ") { $EventLogSource = "Yc.IAM-EntraProv-NewSCIMRequest" } } try { $ClientCertificate = (Get-ChildItem Cert:\LocalMachine\My\$APIProv_CertificateThumbprint) } catch { throw ("Could not find/grab certificate needed for authentication. Aborting. Error Details: "+$_.Exception.Message) } try { $AttributeMapping = Import-PowerShellDataFile $PathToMappingFile } catch { throw ("Could not import data mapping file. Aborting. Error Details: "+$_.Exception.Message) } try { $script:ScimSchemas = @{ "urn:ietf:params:scim:schemas:core:2.0:User" = '{"id":"urn:ietf:params:scim:schemas:core:2.0:User","name":"User","description":"User Account","attributes":[{"name":"userName","type":"string","multiValued":false,"description":"Unique identifier for the User, typically used by the user to directly authenticate to the service provider. Each User MUST include a non-empty userName value. This identifier MUST be unique across the service provider''s entire set of Users. REQUIRED.","required":true,"caseExact":false,"mutability":"readWrite","returned":"default","uniqueness":"server"},{"name":"name","type":"complex","multiValued":false,"description":"The components of the user''s real name. Providers MAY return just the full name as a single string in the formatted sub-attribute, or they MAY return just the individual component attributes using the other sub-attributes, or they MAY return both. If both variants are returned, they SHOULD be describing the same name, with the formatted name indicating how the component attributes should be combined.","required":false,"subAttributes":[{"name":"formatted","type":"string","multiValued":false,"description":"The full name, including all middle names, titles, and suffixes as appropriate, formatted for display (e.g., ''Ms. Barbara J Jensen, III'').","required":false,"caseExact":false,"mutability":"readWrite","returned":"default","uniqueness":"none"},{"name":"familyName","type":"string","multiValued":false,"description":"The family name of the User, or last name in most Western languages (e.g., ''Jensen'' given the full name ''Ms. Barbara J Jensen, III'').","required":false,"caseExact":false,"mutability":"readWrite","returned":"default","uniqueness":"none"},{"name":"givenName","type":"string","multiValued":false,"description":"The given name of the User, or first name in most Western languages (e.g., ''Barbara'' given the full name ''Ms. Barbara J Jensen, III'').","required":false,"caseExact":false,"mutability":"readWrite","returned":"default","uniqueness":"none"},{"name":"middleName","type":"string","multiValued":false,"description":"The middle name(s) of the User (e.g., ''Jane'' given the full name ''Ms. Barbara J Jensen, III'').","required":false,"caseExact":false,"mutability":"readWrite","returned":"default","uniqueness":"none"},{"name":"honorificPrefix","type":"string","multiValued":false,"description":"The honorific prefix(es) of the User, or title in most Western languages (e.g., ''Ms.'' given the full name ''Ms. Barbara J Jensen, III'').","required":false,"caseExact":false,"mutability":"readWrite","returned":"default","uniqueness":"none"},{"name":"honorificSuffix","type":"string","multiValued":false,"description":"The honorific suffix(es) of the User, or suffix in most Western languages (e.g., ''III'' given the full name ''Ms. Barbara J Jensen, III'').","required":false,"caseExact":false,"mutability":"readWrite","returned":"default","uniqueness":"none"}],"mutability":"readWrite","returned":"default","uniqueness":"none"},{"name":"displayName","type":"string","multiValued":false,"description":"The name of the User, suitable for display to end-users. The name SHOULD be the full name of the User being described, if known.","required":false,"caseExact":false,"mutability":"readWrite","returned":"default","uniqueness":"none"},{"name":"hiringDate","type":"string","multiValued":false,"description":"The hiring date of the user.","required":false,"mutability":"readWrite","returned":"default","uniqueness":"none"},{"name":"nickName","type":"string","multiValued":false,"description":"The casual way to address the user in real life, e.g., ''Bob'' or ''Bobby'' instead of ''Robert''. This attribute SHOULD NOT be used to represent a User''s username (e.g., ''bjensen'' or ''mpepperidge'').","required":false,"caseExact":false,"mutability":"readWrite","returned":"default","uniqueness":"none"},{"name":"profileUrl","type":"reference","referenceTypes":["external"],"multiValued":false,"description":"A fully qualified URL pointing to a page representing the User''s online profile.","required":false,"caseExact":false,"mutability":"readWrite","returned":"default","uniqueness":"none"},{"name":"title","type":"string","multiValued":false,"description":"The user''s title, such as \"Vice President.\"","required":false,"caseExact":false,"mutability":"readWrite","returned":"default","uniqueness":"none"},{"name":"userType","type":"string","multiValued":false,"description":"Used to identify the relationship between the organization and the user. Typical values used might be ''Contractor'', ''Employee'', ''Intern'', ''Temp'', ''External'', and ''Unknown'', but any value may be used.","required":false,"caseExact":false,"mutability":"readWrite","returned":"default","uniqueness":"none"},{"name":"preferredLanguage","type":"string","multiValued":false,"description":"Indicates the User''s preferred written or spoken language. Generally used for selecting a localized user interface; e.g., ''en_US'' specifies the language English and country US.","required":false,"caseExact":false,"mutability":"readWrite","returned":"default","uniqueness":"none"},{"name":"locale","type":"string","multiValued":false,"description":"Used to indicate the User''s default location for purposes of localizing items such as currency, date time format, or numerical representations.","required":false,"caseExact":false,"mutability":"readWrite","returned":"default","uniqueness":"none"},{"name":"timezone","type":"string","multiValued":false,"description":"The User''s time zone in the ''Olson'' time zone database format, e.g., ''America/Los_Angeles''.","required":false,"caseExact":false,"mutability":"readWrite","returned":"default","uniqueness":"none"},{"name":"active","type":"boolean","multiValued":false,"description":"A Boolean value indicating the User''s administrative status.","required":false,"mutability":"readWrite","returned":"default"},{"name":"password","type":"string","multiValued":false,"description":"The User''s cleartext password. This attribute is intended to be used as a means to specify an initial password when creating a new User or to reset an existing User''s password.","required":false,"caseExact":false,"mutability":"writeOnly","returned":"never","uniqueness":"none"},{"name":"emails","type":"complex","multiValued":true,"description":"Email addresses for the user. The value SHOULD be canonicalized by the service provider, e.g., ''bjensen@example.com'' instead of ''bjensen@EXAMPLE.COM''. Canonical type values of ''work'', ''home'', and ''other''.","required":false,"subAttributes":[{"name":"value","type":"string","multiValued":false,"description":"Email addresses for the user. The value SHOULD be canonicalized by the service provider, e.g., ''bjensen@example.com'' instead of ''bjensen@EXAMPLE.COM''. Canonical type values of ''work'', ''home'', and ''other''.","required":false,"caseExact":false,"mutability":"readWrite","returned":"default","uniqueness":"none"},{"name":"display","type":"string","multiValued":false,"description":"A human-readable name, primarily used for display purposes. READ-ONLY.","required":false,"caseExact":false,"mutability":"readWrite","returned":"default","uniqueness":"none"},{"name":"type","type":"string","multiValued":false,"description":"A label indicating the attribute''s function, e.g., ''work'' or ''home''.","required":false,"caseExact":false,"canonicalValues":["work","home","other"],"mutability":"readWrite","returned":"default","uniqueness":"none"},{"name":"primary","type":"boolean","multiValued":false,"description":"A Boolean value indicating the ''primary'' or preferred attribute value for this attribute, e.g., the preferred mailing address or primary email address. The primary attribute value ''true'' MUST appear no more than once.","required":false,"mutability":"readWrite","returned":"default"}],"mutability":"readWrite","returned":"default","uniqueness":"none"},{"name":"phoneNumbers","type":"complex","multiValued":true,"description":"Phone numbers for the User. The value SHOULD be canonicalized by the service provider according to the format specified in RFC 3966, e.g., ''tel:+1-201-555-0123''. Canonical type values of ''work'', ''home'', ''mobile'', ''fax'', ''pager'', and ''other''.","required":false,"subAttributes":[{"name":"value","type":"string","multiValued":false,"description":"Phone number of the User.","required":false,"caseExact":false,"mutability":"readWrite","returned":"default","uniqueness":"none"},{"name":"display","type":"string","multiValued":false,"description":"A human-readable name, primarily used for display purposes. READ-ONLY.","required":false,"caseExact":false,"mutability":"readWrite","returned":"default","uniqueness":"none"},{"name":"type","type":"string","multiValued":false,"description":"A label indicating the attribute''s function, e.g., ''work'', ''home'', ''mobile''.","required":false,"caseExact":false,"canonicalValues":["work","home","mobile","fax","pager","other"],"mutability":"readWrite","returned":"default","uniqueness":"none"},{"name":"primary","type":"boolean","multiValued":false,"description":"A Boolean value indicating the ''primary'' or preferred attribute value for this attribute, e.g., the preferred phone number or primary phone number. The primary attribute value ''true'' MUST appear no more than once.","required":false,"mutability":"readWrite","returned":"default"}],"mutability":"readWrite","returned":"default"},{"name":"ims","type":"complex","multiValued":true,"description":"Instant messaging addresses for the User.","required":false,"subAttributes":[{"name":"value","type":"string","multiValued":false,"description":"Instant messaging address for the User.","required":false,"caseExact":false,"mutability":"readWrite","returned":"default","uniqueness":"none"},{"name":"display","type":"string","multiValued":false,"description":"A human-readable name, primarily used for display purposes. READ-ONLY.","required":false,"caseExact":false,"mutability":"readWrite","returned":"default","uniqueness":"none"},{"name":"type","type":"string","multiValued":false,"description":"A label indicating the attribute''s function, e.g., ''aim'', ''gtalk'', ''xmpp''.","required":false,"caseExact":false,"canonicalValues":["aim","gtalk","icq","xmpp","msn","skype","qq","yahoo"],"mutability":"readWrite","returned":"default","uniqueness":"none"},{"name":"primary","type":"boolean","multiValued":false,"description":"A Boolean value indicating the ''primary'' or preferred attribute value for this attribute, e.g., the preferred messenger or primary messenger. The primary attribute value ''true'' MUST appear no more than once.","required":false,"mutability":"readWrite","returned":"default"}],"mutability":"readWrite","returned":"default"},{"name":"photos","type":"complex","multiValued":true,"description":"URLs of photos of the User.","required":false,"subAttributes":[{"name":"value","type":"reference","referenceTypes":["external"],"multiValued":false,"description":"URL of a photo of the User.","required":false,"caseExact":false,"mutability":"readWrite","returned":"default","uniqueness":"none"},{"name":"display","type":"string","multiValued":false,"description":"A human-readable name, primarily used for display purposes. READ-ONLY.","required":false,"caseExact":false,"mutability":"readWrite","returned":"default","uniqueness":"none"},{"name":"type","type":"string","multiValued":false,"description":"A label indicating the attribute''s function, i.e., ''photo'' or ''thumbnail''.","required":false,"caseExact":false,"canonicalValues":["photo","thumbnail"],"mutability":"readWrite","returned":"default","uniqueness":"none"},{"name":"primary","type":"boolean","multiValued":false,"description":"A Boolean value indicating the ''primary'' or preferred attribute value for this attribute, e.g., the preferred photo or thumbnail. The primary attribute value ''true'' MUST appear no more than once.","required":false,"mutability":"readWrite","returned":"default"}],"mutability":"readWrite","returned":"default"},{"name":"addresses","type":"complex","multiValued":true,"description":"A physical mailing address for this User. Canonical type values of ''work'', ''home'', and ''other''. This attribute is a complex type with the following sub-attributes.","required":false,"subAttributes":[{"name":"formatted","type":"string","multiValued":false,"description":"The full mailing address, formatted for display or use with a mailing label. This attribute MAY contain newlines.","required":false,"caseExact":false,"mutability":"readWrite","returned":"default","uniqueness":"none"},{"name":"streetAddress","type":"string","multiValued":false,"description":"The full street address component, which may include house number, street name, P.O. box, and multi-line extended street address information. This attribute MAY contain newlines.","required":false,"caseExact":false,"mutability":"readWrite","returned":"default","uniqueness":"none"},{"name":"locality","type":"string","multiValued":false,"description":"The city or locality component.","required":false,"caseExact":false,"mutability":"readWrite","returned":"default","uniqueness":"none"},{"name":"region","type":"string","multiValued":false,"description":"The state or region component.","required":false,"caseExact":false,"mutability":"readWrite","returned":"default","uniqueness":"none"},{"name":"postalCode","type":"string","multiValued":false,"description":"The zip code or postal code component.","required":false,"caseExact":false,"mutability":"readWrite","returned":"default","uniqueness":"none"},{"name":"country","type":"string","multiValued":false,"description":"The country name component.","required":false,"caseExact":false,"mutability":"readWrite","returned":"default","uniqueness":"none"},{"name":"type","type":"string","multiValued":false,"description":"A label indicating the attribute''s function, e.g., ''work'' or ''home''.","required":false,"caseExact":false,"canonicalValues":["work","home","other"],"mutability":"readWrite","returned":"default","uniqueness":"none"}],"mutability":"readWrite","returned":"default","uniqueness":"none"},{"name":"groups","type":"complex","multiValued":true,"description":"A list of groups to which the user belongs, either through direct membership, through nested groups, or dynamically calculated.","required":false,"subAttributes":[{"name":"value","type":"string","multiValued":false,"description":"The identifier of the User''s group.","required":false,"caseExact":false,"mutability":"readOnly","returned":"default","uniqueness":"none"},{"name":"$ref","type":"reference","referenceTypes":["User","Group"],"multiValued":false,"description":"The URI of the corresponding ''Group'' resource to which the user belongs.","required":false,"caseExact":false,"mutability":"readOnly","returned":"default","uniqueness":"none"},{"name":"display","type":"string","multiValued":false,"description":"A human-readable name, primarily used for display purposes. READ-ONLY.","required":false,"caseExact":false,"mutability":"readOnly","returned":"default","uniqueness":"none"},{"name":"type","type":"string","multiValued":false,"description":"A label indicating the attribute''s function, e.g., ''direct'' or ''indirect''.","required":false,"caseExact":false,"canonicalValues":["direct","indirect"],"mutability":"readOnly","returned":"default","uniqueness":"none"}],"mutability":"readOnly","returned":"default"},{"name":"entitlements","type":"complex","multiValued":true,"description":"A list of entitlements for the User that represent a thing the User has.","required":false,"subAttributes":[{"name":"value","type":"string","multiValued":false,"description":"The value of an entitlement.","required":false,"caseExact":false,"mutability":"readWrite","returned":"default","uniqueness":"none"},{"name":"display","type":"string","multiValued":false,"description":"A human-readable name, primarily used for display purposes. READ-ONLY.","required":false,"caseExact":false,"mutability":"readWrite","returned":"default","uniqueness":"none"},{"name":"type","type":"string","multiValued":false,"description":"A label indicating the attribute''s function.","required":false,"caseExact":false,"mutability":"readWrite","returned":"default","uniqueness":"none"},{"name":"primary","type":"boolean","multiValued":false,"description":"A Boolean value indicating the ''primary'' or preferred attribute value for this attribute. The primary attribute value ''true'' MUST appear no more than once.","required":false,"mutability":"readWrite","returned":"default"}],"mutability":"readWrite","returned":"default"},{"name":"roles","type":"complex","multiValued":true,"description":"A list of roles for the User that collectively represent who the User is, e.g., ''Student'', ''Faculty''.","required":false,"subAttributes":[{"name":"value","type":"string","multiValued":false,"description":"The value of a role.","required":false,"caseExact":false,"mutability":"readWrite","returned":"default","uniqueness":"none"},{"name":"display","type":"string","multiValued":false,"description":"A human-readable name, primarily used for display purposes. READ-ONLY.","required":false,"caseExact":false,"mutability":"readWrite","returned":"default","uniqueness":"none"},{"name":"type","type":"string","multiValued":false,"description":"A label indicating the attribute''s function.","required":false,"caseExact":false,"canonicalValues":[],"mutability":"readWrite","returned":"default","uniqueness":"none"},{"name":"primary","type":"boolean","multiValued":false,"description":"A Boolean value indicating the ''primary'' or preferred attribute value for this attribute. The primary attribute value ''true'' MUST appear no more than once.","required":false,"mutability":"readWrite","returned":"default"}],"mutability":"readWrite","returned":"default"},{"name":"x509Certificates","type":"complex","multiValued":true,"description":"A list of certificates issued to the User.","required":false,"caseExact":false,"subAttributes":[{"name":"value","type":"binary","multiValued":false,"description":"The value of an X.509 certificate.","required":false,"caseExact":false,"mutability":"readWrite","returned":"default","uniqueness":"none"},{"name":"display","type":"string","multiValued":false,"description":"A human-readable name, primarily used for display purposes. READ-ONLY.","required":false,"caseExact":false,"mutability":"readWrite","returned":"default","uniqueness":"none"},{"name":"type","type":"string","multiValued":false,"description":"A label indicating the attribute''s function.","required":false,"caseExact":false,"canonicalValues":[],"mutability":"readWrite","returned":"default","uniqueness":"none"},{"name":"primary","type":"boolean","multiValued":false,"description":"A Boolean value indicating the ''primary'' or preferred attribute value for this attribute. The primary attribute value ''true'' MUST appear no more than once.","required":false,"mutability":"readWrite","returned":"default"}],"mutability":"readWrite","returned":"default"}],"meta":{"resourceType":"Schema","location":"/v2/Schemas/urn:ietf:params:scim:schemas:core:2.0:User"}}' | ConvertFrom-Json "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User" = '{"id":"urn:ietf:params:scim:schemas:extension:enterprise:2.0:User","name":"EnterpriseUser","description":"Enterprise User","attributes":[{"name":"employeeNumber","type":"string","multiValued":false,"description":"Numeric or alphanumeric identifier assigned to a person, typically based on order of hire or association with anorganization.","required":false,"caseExact":false,"mutability":"readWrite","returned":"default","uniqueness":"none"},{"name":"costCenter","type":"string","multiValued":false,"description":"Identifies the name of a cost center.","required":false,"caseExact":false,"mutability":"readWrite","returned":"default","uniqueness":"none"},{"name":"organization","type":"string","multiValued":false,"description":"Identifies the name of an organization.","required":false,"caseExact":false,"mutability":"readWrite","returned":"default","uniqueness":"none"},{"name":"division","type":"string","multiValued":false,"description":"Identifies the name of a division.","required":false,"caseExact":false,"mutability":"readWrite","returned":"default","uniqueness":"none"},{"name":"hiringDate","type":"string","multiValued":false,"description":"The hiring date of the user.","required":false,"mutability":"readWrite","returned":"default","uniqueness":"none"},{"name":"department","type":"string","multiValued":false,"description":"Identifies the name of a department.","required":false,"caseExact":false,"mutability":"readWrite","returned":"default","uniqueness":"none"},{"name":"manager","type":"complex","multiValued":false,"description":"The User''s manager. A complex type that optionally allows service providers to represent organizational hierarchy by referencing the ''id'' attribute of another User.","required":false,"subAttributes":[{"name":"value","type":"string","multiValued":false,"description":"The id of the SCIM resource representingthe User''s manager. REQUIRED.","required":false,"caseExact":false,"mutability":"readWrite","returned":"default","uniqueness":"none"},{"name":"$ref","type":"reference","referenceTypes":["User"],"multiValued":false,"description":"The URI of the SCIM resource representing the User''s manager. REQUIRED.","required":false,"caseExact":false,"mutability":"readWrite","returned":"default","uniqueness":"none"},{"name":"displayName","type":"string","multiValued":false,"description":"The displayName of the User''s manager. OPTIONAL and READ-ONLY.","required":false,"caseExact":false,"mutability":"readOnly","returned":"default","uniqueness":"none"}],"mutability":"readWrite","returned":"default"}],"meta":{"resourceType":"Schema","location":"/v2/Schemas/urn:ietf:params:scim:schemas:extension:enterprise:2.0:User"}}' | ConvertFrom-Json } Test-ScimAttributeMapping $AttributeMapping -ScimSchemaNamespace 'urn:ietf:params:scim:schemas:core:2.0:User' } catch { throw ("Could not verify SCIM Mapping. Aborting. Error Details: "+$_.Exception.Message) } try { $paramConnectMgGraph = @{} if ($TenantId) { $paramConnectMgGraph['TenantId'] = $TenantId } if ($ClientCertificate) { $paramConnectMgGraph['ClientId'] = $APIProv_AzureAppRegistrationClientId $paramConnectMgGraph['Certificate'] = $ClientCertificate } elseif ($ClientId) { $paramConnectMgGraph['ClientId'] = $APIProv_AzureAppRegistrationClientId $paramConnectMgGraph['Scopes'] = 'Application.ReadWrite.All', 'AuditLog.Read.All','SynchronizationData-User.Upload' } else { $paramConnectMgGraph['Scopes'] = 'Application.ReadWrite.All', 'AuditLog.Read.All','SynchronizationData-User.Upload' } Import-Module Microsoft.Graph.Applications -ErrorAction Stop Connect-MgGraph @paramConnectMgGraph -ErrorAction Stop | Out-Null $csv = Import-Csv -Path $PathToCsv foreach ($item in $csv) { Write-YcLogMessage -message ("CSV for SCIM Request: "+$item) -WriteHost $true Write-YcLogMessage -message ("-----------------------------------------------------------------------------------------------------------------------------------------------") -WriteHost $true } $body = ConvertTo-ScimBulkPayload -ScimSchemaNamespace $ScimSchemaNamespace -AttributeMapping $AttributeMapping -InputObject $csv Invoke-AzureADBulkScimRequest -ServicePrincipalId $APIProv_APIAppServicePrincipalId -Body $body -ErrorAction Stop } catch { throw ("Could not connect and or send the API request with SCIM payload. Aborting. Error Details: "+$_.Exception.Message) } finally { Disconnect-MgGraph } } function Test-ScimAttributeMapping { [CmdletBinding()] param ( # Map input properties to SCIM attributes [Parameter(Mandatory = $true)] [hashtable] $AttributeMapping, # SCIM schema namespace for attribute mapping [Parameter(Mandatory = $true)] [string] $ScimSchemaNamespace, # List of attribute names through sub-attribute names [Parameter(Mandatory = $false)] [string[]] $HierarchyPath ) ## Initialize $result = $true foreach ($_PropertyMapping in $AttributeMapping.GetEnumerator()) { if ($_PropertyMapping.Key -in 'id', 'externalId') { continue } [string[]] $NewHierarchyPath = $HierarchyPath + $_PropertyMapping.Key if ($_PropertyMapping.Key -is [string]) { if ($_PropertyMapping.Key.StartsWith('urn:')) { if ($ScimSchemas.ContainsKey($_PropertyMapping.Key)) { $nestedResult = Test-ScimAttributeMapping $_PropertyMapping.Value $_PropertyMapping.Key $result = $result -and $nestedResult } else { Write-Warning ('SCIM Schema Namespace [{0}] was not be validated because no schema representation has been defined.' -f $_PropertyMapping.Key) } } elseif ($ScimSchemas.ContainsKey($ScimSchemaNamespace)) { $ScimSchemaAttribute = $ScimSchemas[$ScimSchemaNamespace].attributes | Where-Object name -EQ $NewHierarchyPath[0] for ($i = 1; $i -lt $NewHierarchyPath.Count; $i++) { $ScimSchemaAttribute = $ScimSchemaAttribute.subAttributes | Where-Object name -EQ $NewHierarchyPath[$i] } if (!$ScimSchemaAttribute) { Write-Error ('Attribute [{0}] does not exist in SCIM Schema Namespace [{1}].' -f ($NewHierarchyPath -join '.'), $ScimSchemaNamespace) $result = $false } else { if ($ScimSchemaAttribute.multiValued -and $_PropertyMapping.Value -isnot [array]) { Write-Error ('Attribute [{0}] is multivalued in SCIM Schema Namespace [{1}] and must contain an array.' -f ($NewHierarchyPath -join '.'), $ScimSchemaNamespace) $result = $false } foreach ($_PropertyMappingValue in $_PropertyMapping.Value) { if ($ScimSchemaAttribute.type -eq 'Complex' -and $_PropertyMappingValue -is [string]) { Write-Error ('Attribute [{0}] of Type [{2}] in SCIM Schema Namespace [{1}] cannot have simple mapping.' -f ($NewHierarchyPath -join '.'), $ScimSchemaNamespace, $ScimSchemaAttribute.type) $result = $false } elseif ($ScimSchemaAttribute.type -ne 'Complex' -and ($_PropertyMappingValue -is [hashtable] -or $_PropertyMappingValue -is [System.Collections.Specialized.OrderedDictionary])) { Write-Error ('Attribute [{0}] of Type [{2}] in SCIM Schema Namespace [{1}] cannot have complex mapping.' -f ($NewHierarchyPath -join '.'), $ScimSchemaNamespace, $ScimSchemaAttribute.type) $result = $false } elseif ($_PropertyMappingValue -is [hashtable] -or $_PropertyMappingValue -is [System.Collections.Specialized.OrderedDictionary]) { $nestedResult = Test-ScimAttributeMapping $_PropertyMappingValue $ScimSchemaNamespace $NewHierarchyPath $result = $result -and $nestedResult } } } } } else { Write-Error ('Attribute Mapping Key [{0}] is invalid.' -f $_PropertyMapping.Key) $result = $false } } return $result } function ConvertTo-ScimBulkPayload { [CmdletBinding()] param ( # Resource Data [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [object[]] $InputObject, # Map all input properties to specified custom SCIM namespace [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)] [Alias('Namespace')] [string] $ScimSchemaNamespace, # Map input properties to SCIM attributes [Parameter(Mandatory = $false)] [hashtable] $AttributeMapping, # Operations per bulk request [Parameter(Mandatory = $false)] [int] $OperationsPerRequest = 50, # PassThru Object [Parameter(Mandatory = $false)] [switch] $PassThru ) begin { $ScimBulkObject = [PSCustomObject][ordered]@{ "schemas" = @("urn:ietf:params:scim:api:messages:2.0:BulkRequest") "Operations" = New-Object System.Collections.Generic.List[pscustomobject] "failOnErrors" = $null } $paramConvertToScimPayload = @{} if ($AttributeMapping) { $paramConvertToScimPayload['AttributeMapping'] = $AttributeMapping } $ScimBulkObjectInstance = $ScimBulkObject.psobject.Copy() } process { foreach ($obj in $InputObject) { $ScimOperationObject = [PSCustomObject][ordered]@{ "method" = "POST" "bulkId" = [string](New-Guid) "path" = "/Users" "data" = ConvertTo-ScimPayload $obj -ScimSchemaNamespace $ScimSchemaNamespace -PassThru @paramConvertToScimPayload } $ScimBulkObjectInstance.Operations.Add($ScimOperationObject) # Output object when max operations has been reached if ($OperationsPerRequest -gt 0 -and $ScimBulkObjectInstance.Operations.Count -ge $OperationsPerRequest) { if ($PassThru) { $ScimBulkObjectInstance } else { ConvertTo-Json $ScimBulkObjectInstance -Depth 10 } $ScimBulkObjectInstance = $ScimBulkObject.psobject.Copy() $ScimBulkObjectInstance.Operations = New-Object System.Collections.Generic.List[pscustomobject] } } } end { if ($ScimBulkObjectInstance.Operations.Count -gt 0) { ## Return Object with SCIM Data Structure if ($PassThru) { $ScimBulkObjectInstance } else { ConvertTo-Json $ScimBulkObjectInstance -Depth 10 } } } } function Invoke-AzureADBulkScimRequest { [CmdletBinding()] param ( # SCIM JSON Payload(s) [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [string[]] $Body, # Service Principal Id for the provisioning application [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] [string] $ServicePrincipalId ) begin { ## Import Mg Modules Import-Module Microsoft.Graph.Applications -ErrorAction Stop ## Lookup Service Principal $ServicePrincipalId = Get-MgServicePrincipal -Filter "id eq '$ServicePrincipalId' or appId eq '$ServicePrincipalId'" -Select id | Select-Object -ExpandProperty id #$ServicePrincipal = Get-MgServicePrincipal -ServicePrincipalId $ServicePrincipalId -ErrorAction Stop $SyncJob = Get-MgServicePrincipalSynchronizationJob -ServicePrincipalId $ServicePrincipalId -ErrorAction Stop if ($RestartService) { Suspend-MgServicePrincipalSynchronizationJob -ServicePrincipalId $ServicePrincipalId -SynchronizationJobId $SyncJob.Id } } process { foreach ($_body in $Body) { Invoke-MgGraphRequest -Method POST -Uri "https://graph.microsoft.com/beta/servicePrincipals/$ServicePrincipalId/synchronization/jobs/$($SyncJob.Id)/bulkUpload" -ContentType 'application/scim+json' -Body $_body } } end { if ($RestartService) { Start-MgServicePrincipalSynchronizationJob -ServicePrincipalId $ServicePrincipalId -SynchronizationJobId $SyncJob.Id } # if ($previousProfile.Name -ne (Get-MgProfile).Name) { # Select-MgProfile -Name $previousProfile.Name # } } } function ConvertTo-ScimPayload { [CmdletBinding()] param ( # Resource Data [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [object[]] $InputObject, # Map all input properties to specified custom SCIM namespace [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)] [Alias('Namespace')] [string] $ScimSchemaNamespace, # Map input properties to SCIM attributes [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)] [hashtable] $AttributeMapping = @{ "externalId" = "externalId" "userName" = "userName" "active" = "active" }, # PassThru Object [Parameter(Mandatory = $false)] [switch] $PassThru ) begin { function Resolve-ScimAttributeMapping { param ( # Resource Data [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [object] $InputObject, # Map input properties to SCIM attributes [Parameter(Mandatory = $true)] [hashtable] $AttributeMapping, # Add to existing hashtable or dictionary [Parameter(Mandatory = $false)] [object] $TargetObject = @{} ) foreach ($_AttributeMapping in $AttributeMapping.GetEnumerator()) { if ($_AttributeMapping.Key -is [string] -and $_AttributeMapping.Key.StartsWith('urn:')) { if (!$TargetObject['schemas'].Contains($_AttributeMapping.Key)) { $TargetObject['schemas'] += $_AttributeMapping.Key } } if ($_AttributeMapping.Value -is [array]) { ## Force array output $TargetObject[$_AttributeMapping.Key] = @(Resolve-PropertyMappingValue $InputObject $_AttributeMapping.Value) } else { $TargetObject[$_AttributeMapping.Key] = Resolve-PropertyMappingValue $InputObject $_AttributeMapping.Value } } return $TargetObject } function Resolve-PropertyMappingValue { param ( # Resource Data [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [object] $InputObject, # Property mapping value to output [Parameter(Mandatory = $true)] [object] $PropertyMappingValue ) foreach ($_PropertyMappingValue in $PropertyMappingValue) { if ($_PropertyMappingValue -is [scriptblock]) { Invoke-Transformation $InputObject $_PropertyMappingValue } elseif ($_PropertyMappingValue -is [hashtable] -or $_PropertyMappingValue -is [System.Collections.Specialized.OrderedDictionary]) { Resolve-ScimAttributeMapping $InputObject $_PropertyMappingValue } else { $InputObject.($_PropertyMappingValue) } } } # function Invoke-PropertyMapping { # param ( # # Resource Data # [Parameter(Mandatory = $true, ValueFromPipeline = $true)] # [object] $InputObject, # # Map input properties to another # [Parameter(Mandatory = $true)] # [object] $PropertyMapping # ) # foreach ($_PropertyMapping in $PropertyMapping) { # if ($_PropertyMapping -is [scriptblock]) { # Invoke-Transformation $InputObject $_PropertyMapping # } # elseif ($_PropertyMapping -is [hashtable] -or $_PropertyMapping -is [System.Collections.Specialized.OrderedDictionary]) { # $TargetObject = @{} # foreach ($_PropertyMapping2 in $_PropertyMapping.GetEnumerator()) { # $TargetObject[$_PropertyMapping2.Key] = Invoke-PropertyMapping2 $InputObject $_PropertyMapping2.Value # } # Write-Output $TargetObject # } # else { # $InputObject.($_PropertyMapping) # } # } # } function Invoke-Transformation { param ( # Resource Data [Parameter(Mandatory = $true, ValueFromPipeline = $true)] [object] $InputObject, # Transformation Script Block [Parameter(Mandatory = $true)] [scriptblock] $ScriptBlock ) process { ## Using Import-PowerShellDataFile to load a scriptblock wraps it in another scriptblock so handling that with loop $ScriptBlockResult = $ScriptBlock while ($ScriptBlockResult -is [scriptblock]) { $ScriptBlockResult = ForEach-Object -InputObject $InputObject -Process $ScriptBlockResult } return $ScriptBlockResult } } } process { foreach ($obj in $InputObject) { ## Generate Core SCIM Data Structure $ScimObject = [ordered]@{ schemas = [string[]]("urn:ietf:params:scim:schemas:core:2.0:User", "urn:ietf:params:scim:schemas:extension:enterprise:2.0:User") #id = [string](New-Guid) } ## Add Attributes to SCIM Data Structure $ScimObject = Resolve-ScimAttributeMapping $obj -AttributeMapping $AttributeMapping -TargetObject $ScimObject if ($ScimSchemaNamespace) { $ScimObject[$ScimSchemaNamespace] = $obj $ScimObject['schemas'] += $ScimSchemaNamespace } ## Return Object with SCIM Data Structure #$ScimObject = [PSCustomObject]$ScimObject if ($PassThru) { $ScimObject } else { ConvertTo-Json $ScimObject -Depth 5 } } } } function New-YcIAMSampleConfig { <# .SYNOPSIS The New-YcIAMSampleConfig function creates a sample configuration file at a specified path. .DESCRIPTION The New-YcIAMSampleConfig function takes an optional path parameter to specify where to create a sample configuration file. It defines a sample configuration as a PowerShell hashtable, converts it to a JSON string, and writes it to the specified path. The configuration includes sections for event logging, Azure Key Vault, Azure General, API settings, solution settings, and notifications. .PARAMETER ConfigPath The ConfigPath parameter is an optional string specifying the directory where the configuration file will be created. If not specified, it defaults to `$env:USERPROFILE\.yc\YcIAMSampleConfig.json`. .INPUTS The function does not accept any pipeline input. .OUTPUTS The function writes a sample configuration file to the specified path and logs a success message. .EXAMPLE The following example shows how to create a sample configuration file: PS> New-YcIAMSampleConfig -ConfigPath "C:\Configs" # Creates the file at C:\Configs\.yc\YcIAMSampleConfig.json #> param ( [Parameter(Mandatory=$false, Position=0)] [ValidateNotNullOrEmpty()] [string]$ConfigPath = "$env:USERPROFILE" ) # Define the full path where the config file will be created $FullPath = Join-Path -Path $ConfigPath -ChildPath ".yc\YcIAMSampleConfig.json" # Create the configuration template $configTemplate = [YcIAMConfigTemplate]::new() # Convert the hashtable to a JSON string $jsonConfig = $configTemplate.ToJson() # Check if the directory exists, and create it if not If (!(Test-Path (Split-Path $FullPath -Parent))) { New-Item -Path (Split-Path $FullPath -Parent) -ItemType Directory -Force } # Write the JSON string to the specified file path Set-Content -Path $FullPath -Value $jsonConfig -Force # Output a success message $OutputMessage = "New-YcIAMSampleConfig @ "+(Get-Date)+": Sample configuration created successfully at "+$FullPath Write-Host $OutputMessage -ForegroundColor Green } class YcIAMConfigTemplate { # Constructor YcIAMConfigTemplate() {} # Method to convert the class to a hashtable [hashtable] ToHashtable() { return [ordered]@{ "EventLogging" = @( @{ "LogToEventlog" = "true" "LogToLogFile" = "true" "LogToOutput" = "true" "LogToHost" = "true" "NameOfEventSource" = "YcIAM" "PathToLogFile" = "C:\Temp\" } ) "AzureGeneral" = @( @{ "tenantId" = "********-****-****-****-************" } ) "ActiveDirectory" = @( @{ "OU" = "" "rawDomainName" = "domainname" "SecondarySMTPAlias" = "true" "MakeSecondary" = ".com" "SwapWith" = ".ch" "SetOfficeIpPhone" = "true" "NewUserEnabled" = "true" "ChangePasswordAtLogon" = "true" } ) "TeamsPhone" = @( @{ "AzureAppRegistrationClientId" = "********-****-****-****-************" "CertificateThumbprint" = "***************************" "PolicyName" = "********" } ) "WelcomeLetter" = @( @{ "PathToTemplate" = "******" "SharePointSiteURL" = "*****" "AzureAppRegistrationClientId" = "********-****-****-****-************" "CertificateThumbprint" = "***************************" } ) "EntraAPIprovisioning" = @( @{ "PathToMappingFile" = "******" "PathToCsv" = "*****" "APIAppServicePinricipalId" = "********-****-****-****-************" "AzureAppRegistrationClientId" = "********-****-****-****-************" "CertificateThumbprint" = "***************************" } ) "Location-CH" = @( @{ "Street" = "******" "City" = "*****" "ZIPCode" = "*****" "Country" = "**" "Phone" = "+**********" "TopLevelDomain" = ".**" } ) "Location-DE" = @( @{ "Street" = "******" "City" = "*****" "ZIPCode" = "*****" "Country" = "**" "Phone" = "+**********" "TopLevelDomain" = ".**" } ) "Location-US" = @( @{ "Street" = "******" "City" = "*****" "ZIPCode" = "*****" "Country" = "**" "Phone" = "+**********" "TopLevelDomain" = ".**" } ) "Notifications" = @( @{ "SendReportEmailTo" = "*****@domain.com" "SendReportEmailFrom" = "*****@domain.com" "AzureAppRegistrationClientId" = "*******-****-****-****-************" "AzureAppRegistrationClientSecretCredentialName" = "********" } ) } } # Method to convert the class to a JSON string [string] ToJson() { $config = $this.ToHashtable() return $config | ConvertTo-Json -Depth 4 } } function New-YcIAMCsvFromMapping { param ( [string]$AttributeMappingFilePath, [string]$OutputCSVPath ) # Import the .psd1 attribute mapping file $attributeMapping = Import-PowerShellDataFile -Path $AttributeMappingFilePath # Initialize an array to store the headers for the CSV $csvHeaders = @() # Recursively extract all column names function Get-MappingColumns { param ($mapping, [ref]$headers) foreach ($key in $mapping.Keys) { $value = $mapping[$key] if ($value -is [string]) { # Simple key-value mapping $headers.Value += $value } elseif ($value -is [hashtable]) { # Nested hashtable (e.g. name, addresses) Get-MappingColumns -mapping $value -headers $headers } elseif ($value -is [array]) { foreach ($item in $value) { if ($item -is [hashtable]) { Get-MappingColumns -mapping $item -headers $headers } } } } } # Call the recursive function to extract column headers Get-MappingColumns -mapping $attributeMapping -headers ([ref]$csvHeaders) # Convert the headers to a unique set to avoid duplicates $csvHeaders = $csvHeaders | Sort-Object -Unique # Create an empty PSObject with the headers as properties $emptyRow = New-Object PSObject foreach ($header in $csvHeaders) { Add-Member -InputObject $emptyRow -MemberType NoteProperty -Name $header -Value $null } # Initialize CSV content with one empty row (or populate this with actual data) $csvContent = @() $csvContent += $emptyRow # Export the empty CSV with the headers $csvContent | Export-Csv -Path $OutputCSVPath -NoTypeInformation -Force -Delimiter ',' Write-Host "CSV with headers generated at: $OutputCSVPath" } function Add-YcIAMRowToImportCSV { param ( [string]$CSVPath, [string]$FirstName, [string]$LastName, [string]$EmployeeId, [string]$UserName, [string]$JobTitle, [string]$CountryCode, [string]$OnboardingDate, [string]$StreetAddress, [string]$City, [string]$ZipCode, [string]$OfficePhone, [string]$TelephoneNumber, [string]$Company, [string]$Team, [string]$Department, [string]$Manager ) # Create a new PSObject with the values provided $newRow = New-Object PSObject -Property @{ FirstName = $FirstName LastName = $LastName EmployeeId = $EmployeeId UserName = $UserName JobTitle = $JobTitle CountryCode = $CountryCode OnboardingDate = $OnboardingDate StreetAddress = $StreetAddress City = $City ZipCode = $ZipCode OfficePhone = $OfficePhone TelephoneNumber = $TelephoneNumber Company = $Company Team = $Team Department = $Department Manager = $Manager } # Add the new row to the existing CSV $newRow | Export-Csv -Path $CSVPath -Append -NoTypeInformation -Force -Delimiter ',' Write-Host "New row added to CSV: $CSVPath" } # Example usage: Add-RowToCSV -CSVPath $OutputCSVPath ` -FirstName "John" ` -LastName "Doe" ` -EmployeeId "1234" ` -UserName "john.doe" ` -JobTitle "Software Engineer" ` -CountryCode "US" ` -OnboardingDate "2024-09-01" ` -StreetAddress "123 Main St" ` -City "Springfield" ` -ZipCode "12345" ` -OfficePhone "123-456-7890" ` -TelephoneNumber "987-654-3210" ` -Company "Contoso" ` -Team "IT" ` -Department "Engineering" ` -Manager "Jane Doe" |