Category Archives: Development

Add tenant licenses from csp to database.

This is a followup from previus post. In this post I will populate the database with what licenses a tenant has aquired.
This will add records of what aquired skus and ‘usage’. For me this is how many license are bought versus how many are assigned. This does not account for senarios other than CSP.
In this function I am only adding the overview of license. How many has been purchased(from us) and versus how many has been assigned. There is no information about what licens a specific user are assigened ( This is for a later article).

function update-tenantlicsinDB {
    <#
  .SYNOPSIS
  Update tenant subscriptions/licenses from Cegal CSP
  .DESCRIPTION
  Update licenses from tenant.
  Tenant refer to the display name of the tenant "Super Company 1"
  Tenant is a dynamic parameter, autocomplete list.

  DYNAMIC PARAMTERS
 
  PARAMETER tenant
  -tenant <string>
  tenant refers to the display name of the tenant "Super Company 1"
  If omitted all tenants are returned. Autocomplete list of all tenants in csp

  .PARAMETER force
  Will force update of skus in database. Normaly updated once a day.
 
  .EXAMPLE
  update-tenantlicsindb -tenant 'Tenant name' -force

  .EXAMPLE
  update-tenantlicsindb

 #>
    [CmdletBinding()]
    param(
        [switch]$force,
        [switch]$updateusers
    )
    DynamicParam {
        $ParameterName = "tenant"
        $RuntimeParameterDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary
        $AttributeCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute]

        # Create and set the parameters' attributes
        $ParameterAttribute = New-Object System.Management.Automation.ParameterAttribute
        $ParameterAttribute.Mandatory = $false
        $ParameterAttribute.Position = 1

        # Add the attributes to the attributes collection
        $AttributeCollection.Add($ParameterAttribute)

        # Generate and set the ValidateSet
 
        # Add the ValidateSet to the attributes collection  
        $SQLInstance = "localhost\SQLExpress"
        $SQLDatabase = "Microsoft365"
        $sqlqr = "select * from [Microsoft365].[dbo].[tenants]"
        $customers = invoke-sqlcmd -query $sqlqr -ServerInstance $SQLInstance -Database $SQLDatabase
        #$arrSet = Get-WmiObject Win32_Service -ComputerName $computername | select -ExpandProperty Name
        $ValidateSetAttribute = New-Object System.Management.Automation.ValidateSetAttribute($Customers | Select-Object -ExpandProperty tenantname)

        $AttributeCollection.Add($ValidateSetAttribute)

        # Create and return the dynamic parameter
        $RuntimeParameter = New-Object System.Management.Automation.RuntimeDefinedParameter($ParameterName, [string], $AttributeCollection)
        $RuntimeParameterDictionary.Add($ParameterName, $RuntimeParameter)
        return $RuntimeParameterDictionary
    }
    begin {

        # Bind the parameter to a friendly variable
        $Kunde = $PsBoundParameters[$ParameterName]  
    }
    process {

        import-module partnercenter
        $tid = "<CSP tenant ID>"
        $appid = "<App ID>"
   
        $k = (get-storedcredential -user $appid).password
        $token = (get-storedcredential -user cspuser).getnetworkcredential().password
        $app = New-Object System.Management.Automation.PSCredential -ArgumentList $appid, $k
        #        $partneraccesstoken = New-PartnerAccessToken -RefreshToken $token -Resource "https://api.partnercenter.microsoft.com" -Credential $app -TenantId $tid
        $partneraccesstoken = New-PartnerAccessToken -RefreshToken $token -Credential $app -Tenant $tid -Scopes 'https://api.partnercenter.microsoft.com/user_impersonation' -ServicePrincipal -ApplicationId $appid  #  -Resource "https://api.partnercenter.microsoft.com"
        $partneraccesstoken
        #$connected = Connect-PartnerCenter -AccessToken $partneraccesstoken.AccessToken -ApplicationId $app.username -TenantId $tid -Environment AzureCloud -ServicePrincipal
        $connected = Connect-PartnerCenter -ApplicationId $appid -Credential $app -RefreshToken $token
        if (-not $connected) { throw "Error connecting to partnercenter.." }
        update-storedcredential -user cspuser -secret ($partneraccesstoken.RefreshToken | ConvertTo-SecureString -AsPlainText -Force)

        $SQLInstance = "localhost\SQLExpress"
        $SQLDatabase = "Microsoft365"
        #  $SQLUsername = ""
        #  $SQLPassword = ""

        $today = Get-date
        $dayofyear = "$($today.Year)-$($today.DayOfYear)"
        if (-not $kunde) {
            try {
                $customers = Get-PartnerCustomer
            }
            catch { Write-Host "Error getting tenants from partnercenter." }
        }
        else {

            $customers = Get-PartnerCustomer | Where-Object { $_.domain -eq $kunde }
        }

        # Add users to DB. (not #ext# or contacts)
        # $customer=$customers[9]
        foreach ($customer in $customers ) {
            #$customer=$customers[2]
            Write-Host "Processing tenant: $($customer.Name) ($($customer.domain))"
            # Add Sku to Skus table
            $customerskus = Get-PartnerCustomerSubscribedSku -CustomerId $customer.CustomerId
            if ($customerskus) {
                foreach ($sku in $customerskus) {
                    try {
                        $sqlsku = "insert into skus (SkuID,SkuPartNumber,ProductName) values ('$($sku.SkuID)','$($sku.SkuPartNumber)','$($sku.ProductName)')"
                        invoke-sqlcmd -query $sqlsku -ServerInstance $SQLInstance -Database $SQLDatabase -ErrorAction Stop # -Username $SQLUsername -Password $SQLPassword
                        Write-Output "SkuID ($($sku.ProductName)) added to database"
                    }
                    catch {
                        $sqlsku = "insert into skus (SkuID,SkuPartNumber,ProductName) values ()"
                        #  Write-Output "SkuID ($($sku.ProductName)) already exists in database"
                    }
                    # Add licensed2licensedskus table
                    $sqlls = "select * from licensedskus where DayOfYear='$($dayofyear)' and tenantid='$($customer.CustomerId)' and skuid='$($sku.SkuID)'"
                    $ls = invoke-sqlcmd -query $sqlls -ServerInstance $SQLInstance -Database $SQLDatabase -ErrorAction Stop # -Username $SQLUsername -Password $SQLPassword
                    if (-not $ls ) {
                        # Not registered for today -> insert
                        $sqlls = "insert into licensedskus (DayOfYear,Date,TenantID,SkuID,AvailableUnits,ActiveUnits,CapabilityStatus,ConsumedUnits,LicenseGroupId,SuspendedUnits,TargetType,TotalUnits,WarningUnits) values ('$($dayofyear)','$($today)','$($customer.CustomerId)','$($sku.SkuID)','$($sku.AvailableUnits)','$($sku.ActiveUnits)','$($sku.CapabilityStatus)','$($sku.ConsumedUnits)','$($sku.LicenseGroupId)','$($sku.SuspendedUnits)','$($sku.TargetType)','$($sku.TotalUnits)','$($sku.WarningUnits)')"  #.ToUniversalTime()
                        $ls = invoke-sqlcmd -query $sqlls -ServerInstance $SQLInstance -Database $SQLDatabase -ErrorAction Stop # -Username $SQLUsername -Password $SQLPassword
                    }
                    else {
                        if ($force) {
                            $sqlls = "update licensedskus set DayOfYear='$($dayofyear)',Date='$($today)',TenantID='$($customer.CustomerId)',SkuID='$($sku.SkuID)',AvailableUnits='$($sku.AvailableUnits)',ActiveUnits='$($sku.ActiveUnits)',CapabilityStatus='$($sku.CapabilityStatus)',ConsumedUnits='$($sku.ConsumedUnits)',LicenseGroupId='$($sku.LicenseGroupId)',SuspendedUnits='$($sku.SuspendedUnits)',TargetType='$($sku.TargetType)',TotalUnits='$($sku.TotalUnits)',WarningUnits='$($sku.WarningUnits)' where DayOfYear='$($dayofyear)' and tenantid='$($customer.CustomerId)' and skuid='$($sku.SkuID)'"
                            $ls = invoke-sqlcmd -query $sqlls -ServerInstance $SQLInstance -Database $SQLDatabase -ErrorAction Stop # -Username $SQLUsername -Password $SQLPassword    
                            Write-output " Updating sku ($($sku.Productname))"
                        }
                    }
                }
            }

          }
       
    }
}

This PS script inserts data into “skus” table and “licensedskus” table. Here are the sql management studio scripts to create those.
“SKSU” tabler

USE [Microsoft365]
GO

/****** Object:  Table [dbo].[Skus]    Script Date: 17.01.2021 21:34:11 ******/
SET ANSI_NULLS ON
GO

SET QUOTED_IDENTIFIER ON
GO

CREATE TABLE [dbo].[Skus](
    [SkuID] [varchar](255) NOT NULL,
    [SkuPartNumber] [varchar](255) NULL,
    [ProductName] [varchar](255) NULL,
    [FriendlyName] [varchar](255) NULL,
 CONSTRAINT [PK_Skus] PRIMARY KEY CLUSTERED
(
    [SkuID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO

And lucky you here are the sql MS to create the othe table:

USE [Microsoft365]
GO

/****** Object:  Table [dbo].[LicensedSkus]    Script Date: 17.01.2021 21:38:41 ******/
SET ANSI_NULLS ON
GO

SET QUOTED_IDENTIFIER ON
GO

CREATE TABLE [dbo].[LicensedSkus](
    [ID] [int] IDENTITY(1,1) NOT NULL,
    [DayOfYear] [varchar](10) NULL,
    [Date] [datetime] NULL,
    [TenantID] [varchar](50) NULL,
    [SkuID] [varchar](50) NULL,
    [AvailableUnits] [int] NULL,
    [ActiveUnits] [int] NULL,
    [CapabilityStatus] [varchar](50) NULL,
    [ConsumedUnits] [int] NULL,
    [LicenseGroupId] [varchar](50) NULL,
    [SuspendedUnits] [int] NULL,
    [TargetType] [varchar](50) NULL,
    [TotalUnits] [int] NULL,
    [WarningUnits] [int] NULL,
 CONSTRAINT [PK_LicensedSkus] PRIMARY KEY CLUSTERED
(
    [ID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO

Get-tenantlistindb (from previous post)

Now that we have our tenants listed in a database table, listing them is quite easy.
Added a switch that allow you to also list deleted/removed tenants.

function get-tenantlistindb {
    param(
        [switch]$all
    )
    $SQLInstance = "localhost\SQLExpress"
    $SQLDatabase = "Microsoft365"
    $sqlqr = "select * from [Microsoft365].[dbo].[tenants]"
    if (-not $all) {
        $sqlqr += " where active='1'"
    }
    $tenants = invoke-sqlcmd -query $sqlqr -ServerInstance $SQLInstance -Database $SQLDatabase
    $tenants
}

Log tenants from office 365 to local Db

Hi,
I like to keep control of how many licenses our cutomers use versus how many  have been purchased.
Here is 1st part  my PS script to copy the info from csp to the DB. I will start creating a database and table to keep a list of all the tenants in the csp. You will also need an service principal in the tenant.
To create a SPN in CSP check this :CSP access to tenants using powershell. Part 1
I am also using the storedcredential -> Get Secretserver secret

function Update-TenantListinDB {

# Set all customers in database where active is &lt; 10 to inactive
import-module partnercenter
$tid = "Partnercenter Tenant ID"
$appid = "AppID"
$k = (get-storedcredential -user $appid).password
$token = (get-storedcredential -user cspuser).getnetworkcredential().password
$app = New-Object System.Management.Automation.PSCredential -ArgumentList $appid, $k
$partneraccesstoken = New-PartnerAccessToken -RefreshToken $token -Credential $app -Tenant $tid -Scopes 'https://api.partnercenter.microsoft.com/user_impersonation' -ServicePrincipal -ApplicationId $appid # -Resource "https://api.partnercenter.microsoft.com"
$connected = Connect-PartnerCenter -ApplicationId $appid -Credential $app -RefreshToken $token
if (-not $connected) { throw "Error connecting to partnercenter.." }
update-storedcredential -user cspuser -secret ($partneraccesstoken.RefreshToken | ConvertTo-SecureString -AsPlainText -Force)

$SQLInstance = "localhost\SQLExpress"
$SQLDatabase = "Microsoft365"
$SQLUsername = ""
$SQLPassword = ""

$customers = Get-PartnerCustomer

$deactivated = Invoke-Sqlcmd -Query "update tenants set active='0' where active &lt; '10'" -ServerInstance $SQLInstance -Database $SQLDatabase
Write-Host "Parsing $(($customers).count) tenants."
foreach ($customer in $customers) {
# Check if exists
$select = "select * from tenants where tenantid like '$($customer.customerid)'"
$found = invoke-sqlcmd -query $select -ServerInstance $SQLInstance -Database $SQLDatabase
if ($found -eq $null) {
Write-Host "Inserting tenant:" $customer.Name
$SQLQuery1 = "insert into tenants (tenantid,tenantname,displayname,active) values ('$($customer.CustomerId)','$($customer.Domain)','$($customer.Name)','1')"
invoke-sqlcmd -query $SQLQuery1 -ServerInstance $SQLInstance -Database $SQLDatabase # -Username $SQLUsername -Password $SQLPassword
}
else {
Invoke-Sqlcmd -Query "update tenants set active='1' where tenantID like '$($customer.CustomerId)' and active &lt;'10'" -ServerInstance $SQLInstance -Database $SQLDatabase
}
}
}

I use this code to populat my database (using integrated authentication).
Sql for tabel (Database named Microsoft365):

USE [Microsoft365]
GO

/****** Object: Table [dbo].[tenants] Script Date: 03.01.2021 20:18:23 ******/
SET ANSI_NULLS ON
GO

SET QUOTED_IDENTIFIER ON
GO

CREATE TABLE [dbo].[tenants](
[tenantID] [VARCHAR](255) NOT NULL,
[tenantname] [VARCHAR](255) NOT NULL,
[displayname] [VARCHAR](255) NULL,
[active] [INT] NOT NULL,
CONSTRAINT [PK_tenants] PRIMARY KEY CLUSTERED
(
[tenantID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO

Powershell 2.0 Json to Hash

Resonse from webrequests are normally in Json format and converting back and forth is no problem using convertto/from-json. Only this time some computers did run Powershell 2.0 and did not have any “new” .NET framework installed. So I ran to a halt at this Json converting. Since my data did have predefined data structure I thought I could easily do this by some ‘foreach’ and string handling, but it ended up being to complex. Finally I turned my head to regex, this turned out to be the solution.

function convertfrom-json-onelevel {
Param (
[string[]] $json
)
$hashtable = @{ }
$t = $json | Select-String -Pattern '(["])(?:(?=(\\?))\2.)*?\1' -AllMatches
$hashtable = @{ }
(0..((($t.matches).count - 1) / 2)) | % {
$key = [regex]::Unescape($t.Matches[$_ * 2].Value)
$key = $key.TrimEnd('"')
$key = $key.TrimStart('"')
$value = [regex]::Unescape($t.Matches[$_ * 2 + 1].Value)
$value = $value.TrimEnd('"')
$value = $value.TrimStart('"')
$hashtable.add($key, $value)
}
return $hashtable
}

Get Secretserver secret

Since we are using SecretServer as our credential store it is of great help to be able to get credentials directly from powershell. This is a small function that connects to secretserver webservices and retrieve a secret based on secred ID. The function will connect to the webservice as the signedin user or by a supplied credential or lastly by a predefined stored credential. To use stored credential I’am using functions from https://github.com/cunninghamp/PowerShell-Stored-Credentials .

Usually you would use the PS credential object directly. To get the password as text you could use it from the PSobject referring to the get networkcredential().


$cred=get-secretid -secretID 2007
$password_As_text=$cred.GetNetworkCredential().Password

Or if you need the password in clear text, displayed on screen, you could specify that as a an argument.

The function is made for my usage, so there is definitive roomfor improvement .


function Get-SecretID
{
param(
[parameter(ValueFromPipeline=$True)]
[int] $secretID,
[pscredential]$sscred,
[switch]$Cleartext
)

$where = 'https://secretserverdnsname/secretserver/winauthwebservices/sswinauthwebservice.asmx'

if($sscred -ne $null){
    $ws = New-WebServiceProxy -uri $where -Credential $sscred
}else{

  try{
    $ws = New-WebServiceProxy -uri $where -UseDefaultCredential -ErrorAction SilentlyContinue
    if($ws -eq $null){
      if (!(Test-Path Variable:\ssuser)){
        throw {
          Write-Host "No secretserver user specified or variable 'ssuser' defined.`nThis is to be used by 'get-storedcredential'"
        }
      }
        $credacc=Get-StoredCredential -UserName $ssuser
        $ws = New-WebServiceProxy -uri $where -Credential $credacc -ErrorAction SilentlyContinue
        if($ws -eq $null){throw{Write-host "Unable to connect to SecretServer"}}
    }
  }
  catch{

  }
}

$wsResult = $ws.GetSecret($secretId, $false, $null)
if($wsresult.errors -ne $null){
  $Cred=New-Object PSObject
  $Cred | add-member -NotePropertyName "Username" -NotePropertyValue $wsresult.errors
  $Cred | Add-Member -NotePropertyName "Password" -NotePropertyValue $wsresult.errors
 
  return $Cred
} else {
 
$u=$wsResult.Secret.Items[1].value.ToString()
$ep = ConvertTo-SecureString $wsResult.Secret.Items[2].value.ToString() -AsPlainText -Force
[pscredential]$Cred = New-Object -TypeName "System.Management.Automation.PSCredential" -ArgumentList $u,$ep
if($Cleartext){
  [psobject]$Cred=New-Object PSObject
    $Cred | add-member -NotePropertyName "Username" -NotePropertyValue $u
    $Cred | Add-Member -NotePropertyName "Password" -NotePropertyValue $wsResult.Secret.Items[2].value.ToString()
    $Cred | Add-Member -NotePropertyName "Domain" -NotePropertyValue $wsResult.Secret.Items[0].value.ToString()
  }
return $Cred
}
}

Use powershell to get external IP address

How can you get your external IP address from powershell? I use a simple script to query an external public web service. The service I’m using is hosted by ipinfo.io . I have created a small function that is placed in my powershell library. All my modules are loaded by using powershell profiles.

This simple function uses rest. Usage is simple : get-mypublicip . Could easily be uses in script (get-mypublicip).ip .


function get-mypublicip{
    Write-Verbose "Resolving external IP"
    try {
        $ipaddr = Invoke-RestMethod http://ipinfo.io/json #| Select-Object -ExpandProperty ip
        }
    catch {
        throw "Can't get external IP Address. Quitting."
        }
    if ($ipaddr -eq $null) { throw "Can't get external IP Address. Quitting." }
    Write-Verbose "External IP is $ipaddr"
    return $ipaddr
}

Make sure you have the correct CSP for Your CA

Got some weird errors on our new Skype for Business server install. After a straight forward install users was unable to login from external and some issues regarding conferences. Skype services seemed to start but ended up running with unknown details when get-windowsservices. Also we had one error in the eventviewer on frontende server.

The most important clue was : CA_Failure: InternalError . So this pointed towards a certificate error. What could be wrong with the CA server (A windows server 2016 Enterprise Root CA).

This was the first time I have seen a ECDSA CSP used. Next was to verify S4B requirements. https://docs.microsoft.com/en-us/skypeforbusiness/plan-your-deployment/requirements-for-your-environment/environmental-requirement

  • Encryption key lengths of 1024, 2048, and 4096 are supported. Key lengths of 2048 and greater are recommended.
  • The default digest, or hash signing, algorithm is RSA. The ECDH_P256, ECDH_P384, and ECDH_P521 algorithms are also supported.

Once again check CA configuration:

This CA was installed with the ECDSA_P256 CSP, We did not have the option to reinstall/migrate the CA to a supported version, so our workaround was to install a new standalone CA using RSA256 CSP and use this CA to issue certificates for Edge server internal and frontend certificate. (We published the new CA public key to clients using GPO).

After we assigned the new certificates and rebooted it all seems to work OK. The new certs are now RSA256

msRTCSIP-GroupingID editor.

This is my msRTCSIP-GroupingID editor. I use it to segregate addresslists in Lync solutions. It takes a commaseparated text file containing Company name and GUID for the addresslist assigned to this Company (filename : groupingids.txt) sample:

Name,Value
CompanyA,00000000-0000-0000-0000-000000000010
CompanyB,00000000-0000-0000-0000-000000000011
CompanyC,00000000-0000-0000-0000-000000000012

I have placed this file on the Lync share for my own convenience. Powershell script Groupingeditor.ps1 loads these values to a listbox. It let you search for users and then apply the groupingID to the selected users.

Next Version will contain possibillities to select entire OU’s and list users containing a spesific GroupingID.

GROUPINGEDITOR

groupingids.txt (153B)

groupingeditor.ps1 (4.7KB)

Group Manager in Exchange 2010-2013 are unable to manage group membership.

There was a change in RBAC. Group managers are not able to add or remove members of a distribution Group even if it seems so in the Exchange Management Console.

The only options in ECP was to give them the additional permission to great and remove groups. You can create a new role that will enable this permission again. Thanks to Matthew Byrd at Microsoft who has created a Powershell script that does this for us.

http://blogs.technet.com/b/exchange/archive/2009/11/18/how-to-manage-groups-that-i-already-own-in-exchange-2010.aspx

Install Windows Phone 7 SDK on windows 2008 R2

Aaron Stebner’s WebLog shows us how to install Windows Phone 7 SDK on Windows 2008 R2.

http://blogs.msdn.com/b/astebner/archive/2010/05/02/10005980.aspx

  1. Download the Windows Phone Developer Tools web bootstrapper and save it to your hard drive
  2. Extract the contents of the setup package by running vm_web.exe /x and choosing a path to extract to
  3. Go to the folder you extracted to in step 2 and open the file baseline.dat in notepad
  4. Look for the section named [gencomp7788]

    Note – you have to change this exact section – this is the one that controls the OS version blocking behavior in Windows Phone Developer Tools setup.

  5. Change the value InstallOnLHS from 1 to 0
  6. Change the value InstallOnWin7Server from 1 to 0
  7. Save and close baseline.dat
  8. Run setup.exe /web from the folder you extracted to in step 2

Updated: This also works for WP7.1 sdk Laughing