Tag Archives: PowerShell

Tail a file on Windows

On almost every Unix system, we have tail -f to watch the end of *really really big* files.

When faced with a 36GB log file on Windows the tooling is often lacking.

I borrowed / adapted a little PowerShell function to extract the last n log lines from a file, and write to a new file:
https://gist.github.com/crossan007/b5e8ac4579ba61eb1967315657406751

Partially borrowed from: https://stackoverflow.com/questions/36507343/get-last-n-lines-or-bytes-of-a-huge-file-in-windows-like-unixs-tail-avoid-ti

Un-Approve SharePoint List Item Previous Versions

I recently had a change request against a SharePoint Forms Library I had created a few years ago – the request was to adjust the permissions so that form submitters could see only the forms that they’ve submitted (and not others).

This is a generally straightforward action on new libraries: enable ” Require content approval for submitted items?”, and change “Who should see draft items in this document library?” to

However, enabling these settings seems to have caused the items that already existed to have an Approval Status of “Approved, ” despite a pending Approval workflow.  This caused the undesired effect of allowing users who do not hold the “Approve” permission level to access previous version of items still in the approval workflow.

I needed to reject previous versions of forms where the current version had not yet been approved.  On lots of items.

I found numerous examples from google how to use PowerShell to set the Approval Status of list items; however, nearly every example dealt with only the current version of a list item – making no mention of altering the approval status of previous versions of list items.

Additionally, I found a few posts attempting to manipulate attributes for previous versions;  the responses for each of these inquires were varied:

  • “you can’t – history is read-only,”
  • “you can migrate the documents to a new list, and re-build the history”
  • “you can delete the old versions”

I even found a mega-thread on TechNet how to “List and Delete List Item Versions using PowerShell,” and a “Complete Guide to Getting and Setting Fields Using PowerShell”

None of these options accomplished what I was seeking:  to simply remove the approval on previous versions.

Finally, I resorted to simply poking at the objects from PowerShell (never under-estimate the power of Get-Member to explore objects) Attempting to modify the properties on a previous version would yeild the error message “Unable to index into an object of type Microsoft.SharePoint.SPListItemVersion” (Link)

Ok – different approach:  I know my desired action is feasible via the UI for single list items:

So, I opened Chrome developer tools and captured the command sent when clicking “Reject this version”: A POST to “/_layouts/versions.aspx” with the ItemID, and an “op” value of “TakeOffline”. A quick google search revealed a server-side object model equilavent: Microsoft.SharePoint.SPFile.TakeOffline

My solution: invoke SPListItem.File.TakeOffline() for every file which is currently pending and has a previously approved version:

 

ADFS Username Behavior

Problem

ADFS 4.0 on Windows Server 2016 tells users to log in with their full email address “someone@example.com.”  This generates many support requests, and complaints about too much typing.

Additionally, some extranet users may have email addresses not on the domain, and it’s unclear which email address they should supply.

This affects both the ADFS log in page, and the ADFS password change page.

Solution Methodology

ADFS Server 4.0 has PowerShell cmdlets to manage the content delivered to users during authentication requests: https://technet.microsoft.com/windows-server-docs/identity/ad-fs/operations/ad-fs-user-sign-in-customization

We’ll focus on the following

Get-AdfsWebTheme

and

Set-AdfsWebTheme

Of particular interest here is that we’re able to modify the JavaScript that runs on these pages.

Steps

Use PowerShell to manage custom ADFS Themes

  1. Export the Default ADFS Theme using this snippet:
     Export-ADFSWebTheme -Name "Default" -DirectoryPath c:\test
  2. Use your  favorite editor to open c:\test\script\onload.js
  3. Add the snippets from below (as desired) into onload.js
  4. Create a New ADFS Theme
     New-AdfsWebTheme -Name BetterDefault -SourceName c:\test 
    1. Set your new theme as the default (best for testing)
       Set-ADFSWebConfig -ActiveThemeName BetterDefault 
  5. Alternatively, you may update an existing theme with your code changes
    Set-AdfsWebTheme -TargetName "Default" -AdditionalFileResource @{Uri=“/adfs/portal/script/onload.js”;Path=“C:\theme\script\onload.js"}

Placeholder Text Solution

To update the “someone@example.com” placeholder on both the login and the password change ADFS pages, paste this code into your onload.js, and update your ADFS theme.

function UpdatePlaceholders() {
    var userName;
    if (typeof Login != 'undefined'){
        userName = document.getElementById(Login.userNameInput) 
    }
    if (typeof UpdatePassword != 'undefined'){
        userName = document.getElementById(UpdatePassword.userNameInput);
    }
    if (typeof userName != 'undefined'){
        userName.setAttribute("placeholder","Username");
    }
}

document.addEventListener("DOMContentLoaded", function(){
  // Handler when the DOM is fully loaded
  UpdatePlaceholders()
});

 

Formatting of the Username field

For single-domain organizations, it may be less than desirable to force users to enter the domain name as part of their username. To “fix” this requirement of entering usernames in a format of “domain\username” or “username@domain.com”, paste the following code into your onload.js.  Make sure to update your domain where appropriate.

Logon Username Format Solution

 


if (typeof Login != 'undefined'){
    Login.submitLoginRequest = function () { 
    var u = new InputUtil();
    var e = new LoginErrors();
    var userName = document.getElementById(Login.userNameInput);
    var password = document.getElementById(Login.passwordInput);

    if (userName.value && !userName.value.match('[@\\\\]')) 
    {
        var userNameValue = 'example.org\\' + userName.value;
        document.forms['loginForm'].UserName.value = userNameValue;
    }

    if (!userName.value) {
       u.setError(userName, e.userNameFormatError);
       return false;
    }


    if (!password.value) 
    {
        u.setError(password, e.passwordEmpty);
        return false;
    }
    document.forms['loginForm'].submit();
    return false;
};
}

Password Change Username Formatting Solution


if (typeof UpdatePassword != 'undefined'){
    UpdatePassword.submitPasswordChange = function () { 
    var u = new InputUtil();
    var e = new UpdErrors();

    var userName = document.getElementById(UpdatePassword.userNameInput);
    var oldPassword = document.getElementById(UpdatePassword.oldPasswordInput);
    var newPassword = document.getElementById(UpdatePassword.newPasswordInput);
    var confirmNewPassword = document.getElementById(UpdatePassword.confirmNewPasswordInput);

    if (userName.value && !userName.value.match('[@\\\\]')) 
    {
        var userNameValue = 'example.org\\' + userName.value;
        document.forms['updatePasswordForm'].UserName.value = userNameValue;
    }

    if (!userName.value) {
       u.setError(userName, e.userNameFormatError);
       return false;
    }

    if (!oldPassword.value) {
        u.setError(oldPassword, e.oldPasswordEmpty);
        return false;
    }

    if (oldPassword.value.length > maxPasswordLength) {
        u.setError(oldPassword, e.oldPasswordTooLong);
        return false;
    }

    if (!newPassword.value) {
        u.setError(newPassword, e.newPasswordEmpty);
        return false;
    }

    if (!confirmNewPassword.value) {
        u.setError(confirmNewPassword, e.confirmNewPasswordEmpty);
        return false;
    }

    if (newPassword.value.length > maxPasswordLength) {
        u.setError(newPassword, e.newPasswordTooLong);
        return false;
    }

    if (newPassword.value !== confirmNewPassword.value) {
        u.setError(confirmNewPassword, e.mismatchError);
        return false;
    }

    return true;
};
}

Thanks for reading!  If you have any questions, feel free to send me a tweet @crossan007.

Exchange Dynamic Distribution Group Delivery Problems

Consider the following:

An Exchange Dynamic Distribution Group has a valid recipient filter, and the filter generates the desired resultant set of recipients with the following PowerShell command:

Get-Recipient - RecipientPreviewFilter $(Get-DynamicDistributionGroup "name").RecipientFilter

However, when a user sends a message to the group, no messages are delivered, and the sender does not receive an NDR.

One possible cause of this issue is a property of the dynamic distribution group called RecipientContainer.  This is similar to the SearchBase attribute of the Get-ADUser cmdlet: it specifies the container in which to apply the RecipientFilter.  Therefore, the RecipientContainer must be the OU (Or a parent of) in which the desired users are stored.

More info here: https://www.corelan.be/index.php/2008/11/05/dynamic-distribution-lists-not-working-as-expected-0-recipients-during-mail-routing/

Pushing Calendar Events with the EWS API

We’ve had a need to populate users’ calendars with data from an internal FileMaker Database, so I dug around in the EWS API, and came up with a script that uses the FileMaker ODBC Connection, and the EWS API to accomplish the task:

First things first, we need to install the EWS Managed API on the machine that will run the script.

After the EWS Managed API is installed, we need to reference it in our PowerShell script:

Add-Type -Path “C:\Program Files\Microsoft\Exchange\Web Services\2.2\Microsoft.Exchange.WebServices.dll”

Next, we need to set up a System.Net.NetworkCredential object for the account we’ll use to push these events.  This account must have at least modify permission on the target users’ calendar.

$Credentials = new-object system.net.NetworkCredential(“CalendarAccessAccount”,”SuperStrongPa$$w0Rd!”,”litware”)

Next, we need to Create anMicrosoft.Exchange.WebServices.Data.ExchangeService object:

$version = [Microsoft.Exchange.WebServices.Data.ExchangeVersion]::Exchange2013_SP1
$service = new-object Microsoft.Exchange.WebServices.Data.ExchangeService($version)

We don’t want to use the default credentials, Instead we want to authenticate using the service account specified earlier:

$service.UseDefaultCredentials = $false
$service.Credentials=$Credentials

And, presuming AutoDiscover is set up correctly in our domain, we want to let EWS figure out the server address, port, etc:

$service.AutodiscoverUrl(“TargetMailbox@litware.com”)

Next, we need to reference the user’s calendar (it’s really just a folder as far as the API is concerned):

$folderid = new-object Microsoft.Exchange.WebServices.Data.FolderId([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::Calendar, “TargetMailbox@litware.com”)

And finally, we build the appointment object:

$Appointment = New-Object Microsoft.Exchange.WebServices.Data.Appointment -ArgumentList $Service
$appointment.Subject = “Test111”
$appointment.Body = “Test111”
$appointment.Start = $(Get-Date).AddHours(6)
$appointment.End =$(Get-Date).AddHours(9)

Don’t forget to save it:

$appointment.Save($folderid)

 

All in all, we can wrap this up as a function:

Function CreateAppointment($User,$Credentials)
{
$mailboxName=$User
$version = [Microsoft.Exchange.WebServices.Data.ExchangeVersion]::Exchange2013_SP1
$service = new-object Microsoft.Exchange.WebServices.Data.ExchangeService($version)
$service.UseDefaultCredentials = $false
$service.Credentials=$Credentials
$service.AutodiscoverUrl($mailboxName)

$folderid = new-object Microsoft.Exchange.WebServices.Data.FolderId([Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::Calendar, $mailboxName)

$Appointment = New-Object Microsoft.Exchange.WebServices.Data.Appointment -ArgumentList $Service
$appointment.Subject = “Test Subject”
$appointment.Body = “Test Body”
$appointment.Start = $(Get-Date).AddHours(6)
$appointment.End =$(Get-Date).AddHours(9)

$appointment.Save($folderid)

}

Now we can call the function from, say, with a loop so as to iterate through each user in a CSV:

$users = Import-CSV “Users.csv”

$Credentials = new-object system.net.NetworkCredential(“CalendarAccessAccount”,”SuperStrongPa$$w0Rd!”,”litware”)

Foreach ($User in $Users)

{

CreateAppointment $User $Credentials

}

 

More to come later on the FileMaker ODBC Connection…

FIM Portal No Access for FIM Admin Account

Today’s adventure with Forefront Identity Manager started when I was unable to log into the FIM portal.  Some digging revealed that the accountName attribute for my admin user had been set to null (probably from too much tinkering with sync rules).

I realized that the accountName was probably the issue by two indicators: there was no account name attribute for the FIM Admin object in the FIM Synchronization Service Manager application, and because the query below referencing the ObjectValueString table lacked some attributes. The change-fimadmin.ps1 script helped me determine these SQL sanity check queries.

I had already eliminated the usual suspects for not being able to access the portal (ObjectSID, MPRs, etc), so this stumped me for a little while

Anyway, I needed a way to get back in the portal (and I didn’t want to re-install), so I came up with this script that uses the FIM PowerShell modules to set the accountName attribute of the FIM Admin user (identified by the well-known admin user GUID).

I used the script on How to Use PowerShell to Set the Required Attributes for the FIM Portal Access as a starting point, modifying it to set only the accountName attribute.

$adminAccountName=”accountNameHere”

If(@(get-pssnapin | where-object {$_.Name -eq “FIMAutomation”} ).count -eq 0) {add-pssnapin FIMAutomation}

Function SetAttribute
{
PARAM($CurObject, $AttributeName, $AttributeValue)
END
{
$ImportChange = New-Object Microsoft.ResourceManagement.Automation.ObjectModel.ImportChange
$ImportChange.Operation = 1
$ImportChange.AttributeName = $AttributeName
$ImportChange.AttributeValue = $AttributeValue
$ImportChange.FullyResolved = 1
$ImportChange.Locale = “Invariant”
If ($CurObject.Changes -eq $null) {$CurObject.Changes = (,$ImportChange)}
Else {$CurObject.Changes += $ImportChange}
}
}
$curObject= export-fimconfig -uri $URI –onlyBaseResources -customconfig (“/Person[ObjectID='{7fb2b853-24f0-4498-9534-4e10589723c4}’]”)

$ImportObject = New-Object Microsoft.ResourceManagement.Automation.ObjectModel.ImportObject

$ImportObject.ObjectType = $curObject.ResourceManagementObject.ObjectType
$ImportObject.TargetObjectIdentifier = $CurObject.ResourceManagementObject.ObjectIdentifier
$ImportObject.SourceObjectIdentifier = $CurObject.ResourceManagementObject.ObjectIdentifier
$ImportObject.State = 1

SetAttribute -CurObject $ImportObject -AttributeName “AccountName” -AttributeValue $adminAccountName
$ImportObject | Import-FIMConfig -uri $URI -ErrorVariable Err -ErrorAction SilentlyContinue

 

After running this script, you should be able to log into the FIM portal again.

Helpful places to look also include the FIMService database.  Particularly the ObjectValueString and UserSecurityIdentifiers Tables.

 

The following query represents the values for the FIM Admin User, and should yield 7 rows(Attribute Keys 1,66,68,70,117,125,132)

SELECT TOP 1000 [AttributeID]
,[ObjectKey]
,[ObjectTypeKey]
,[AttributeKey]
,[SequenceID]
,[LocaleKey]
,[ValueString]
,[Multivalued]
FROM [FIMService].[fim].[ObjectValueString]

where ObjectKey =2340

The following query represents the SID, in HEX form, of the FIM Admin User, and should yield 1 row:

SELECT TOP 1000 [UserObjectKey]
,[SecurityIdentifier]
FROM [FIMService].[fim].[UserSecurityIdentifiers]
where UserObjectKey =2340

 

 

 

Cleaning Up Exchange Messages with Search-Mailbox

Like most sysadmins, I receive notifications from end users about SPAM showing up in their inbox.  While not all spam can be avoided, we can deal with it.  I wanted to lessen the impact of already delivered spam and potentially avert a crisis if the same phishing email is sent to all 1500 mailboxes, so I whipped up this script to search out and destroy these messages from my Exchange environment:

$Subject = “About your last transaction”
$StartDate = $(‘1/1/2015’)
$BodyLanguage = “sellam.fr”
$TargetMailbox = “spamdump”
$TargetFolder = “WHD2918”

$Search = [scriptblock]::Create(“Received>=`”$StartDate`” and Subject:`”$Subject`” and `”$BodyLanguage`””)

Get-Mailbox -ResultSize Unlimited | Search-Mailbox -SearchQuery $Search -targetmailbox $TargetMailbox -targetfolder $TargetFolder -loglevel full -logonly

Note the last flag in the last line of the script: “-logonly.”  Be very careful to run the command with this command the first go-round.  This ensures that the query you specify does not grab messages that it shouldn’t (and you wind up deleting everyone’s entire mailbox).  The result of logonly is an excel file in the target mailbox with the headers of the resultant messages.

After reviewing the messages, replace -logonly with -deletecontent.  This will actually move the messages from the users’ mailboxes into the target mailbox.

If you want to modify the query, take a look into how Search-Mailbox actually works.   Search-Mailbox uses KQL, so be sure to brush up on the syntax.  If you’ve beocme accustomed to the powershell boolean operators such as “-and,” You’ll be unpleasantly surprised when you learn that the same operator will evaluate to “not and” in KQL

Forgot SharePoint Farm User Account Password

I had recently patched an inherited SharePoint 2010 Farm up to the December 2014 CU.  I’m currently prepping to migrate the farm to SharePoint 2013, but I needed to get it patched in the interim.

I successfully applied SP2, and the the December 2014 CU (14.0.7140.5000 – Much thanks to Todd Klindt’s SharePoint Admin Blog for the easy build number lookup), and all seemed well.

That is, until I had to change an extranet user’s email address.  These users don’t have mail accounts in our Exchange environment, but we do populate the mail attribute in AD with their corporate email address.  I made the change to the attribute in AD, and attempted to run the User Profile Synchronization (Central Administration | Manage Service Applications | User Profile Service Application | Start Profile Synchronization).

This action failed because the User Profile Synchronization Service was not running on the server! (Central Administration | Manage Services on Server).

I attempted to start the service but was prompted for the DOMAIN\SPFarm account! I searched all archives and documents, but found no reference to this password!  UH OH!!!!!

I finally found this post: http://joelblogs.co.uk/2012/09/22/recovering-passwords-for-sharepoint-2010-farm-web-application-and-service-application-accounts/

I had full administrative access to the server on which Central Administration was installed, so all I had to do was run a “one liner” in PowerShell.  Could it really be that easy?!

Here’s how easy it is:

&$env:windir\\system32\\inetsrv\\appcmd.exe list apppool 
 "SharePoint Central Administration v4" /text:ProcessModel.Password

I ran the command in my dev environment first (we always test foreign code outside of production, right?), and got this!

No Way.  That’s my Farm account password….in PLAIN TEXT! WHOA SCARY!

So, If you ever find yourself forgetting any of your IIS Application Pool Account Passwords, you now have the tool to recover it!

HOO-RAH!