SharePoint Field Customizers are client-side components that run inside the context of a SharePoint page.
Currently, a React project is available out-of-the-box with the second option of “No JavaScript framework” for those ambitious enough to add their own preferred UI libraries.
SETUP YOUR DEVELOPMENT ENVIRONMENT
This article is not going to go into the details on how to set up your development environment. A good article can be found here.
My only recommendation is to use NVM instead of the default installation of NodeJS.
NVM allows you to install multiple versions of NodeJS and handles the heavy-lifting in swapping between them.
CREATE A FIELD CUSTOMIZER EXTENSION
As part of the article, I will demonstrate how to create a Field Customizer Extension that will display human-readable file size in Bytes, Kb, Mb, Gb, Tb, Pb, Eb, Zb, Yb from a number value store in a Sharepoint number column. In my project, I use a React component called file-bytes-formatter, but once you get the hang of it, you will be able to extend these to most React components.
Follow these steps to create the Field Customizer Extension template project. These steps have been adapted from the official Build your first Field Customizer extension article.
1) Create a new project directory in a convenient location
md Bytes-Field-Extension
2) Go to the project directory
cd Bytes-Field-Extension
3) Run the Yeoman SharePoint Generator
yo @microsoft/sharepoint
4) When prompted, enter the following values (select the default option for all prompts omitted below):
What is your solution name?: bytes-field-extension
Will the components in the solution require permissions to access web APIs that are unique and not shared with other components in the tenant?: (y/n) n
Which type of client-side component to create?: Extension
Which type of client-side extension to create?: Field Customizer
What is your Field Customizer name?: Bytes Field Extension
What is your Field Customizer description?: This Field Customizer Extension will return human-readable file size in Bytes, Kb, Mb, Gb, Tb, Pb, Eb, Zb, Yb
Which framework would you like to use?: React
At this point, Yeoman installs the required dependencies and scaffolds the solution files along with the HelloWorld extension. This might take a few minutes. When prompted, enter the following values (select the default option for all prompts omitted below):
5) Install file-bytes-formatter React component
npm install file-bytes-formatter
6) Type the following into the console to start Visual Studio Code or open project in your favourite editor
code .
7) Within src\extensions\bytesFieldExtension\BytesFieldExtensionFieldCustomizer.ts add the following line under the last import command.
import Fsf from "file-bytes-formatter"
This will make the file-bytes-formatter component available in your project.
8) Within src\extensions\bytesFieldExtension\BytesFieldExtensionFieldCustomizer.ts change this line
This will parse the text from the SharePoint column through the file-bytes-formatter component. The spit function is used to remove any commas that SharePoint might have added to style the number.
9) Make a note of the ClientSideComponentId in sharepoint\assets\elements.xml. You will need this later.
10) Save project and close project
PACKAGING FIELD CUSTOMIZER EXTENSION
1) In the console window, enter the following command to package your client-side solution that contains the extension so that we get the basic structure ready for packaging:
gulp bundle --ship
2) Execute the following command so that the solution package is created:
gulp package-solution --ship
The command creates the package: ./sharepoint/solution/bytes-field-extension.sppkg:
DEPLOYING FIELD CUSTOMIZER EXTENSION
After deploying the bytes-field-extension.sppkg into your environment (see Deploy your client-side web part to a SharePoint page if you are unfamiliar with this process) you need to configure the field that you want this formatter to take effect on.
Media files have become an important part of business content. Seeing that in some environments we cannot explicitly block MP3s, how do we identify what files are business recordings, and which are (possibly illegal) music?
IsMP3Song.exe is my attempt to address this issue.
The idea is very simple, pass the filename to the tool and it returns a score indicating the confidence of it deeming the file as a commercial MP3.
The tool achieves this by doing a web-service call to iTunes and then scoring the iTunes results with the filename of the MP3.
Demo Execution
As you can see, the first couple of executions return a score because these are identified as commercial songs and the last one is scored as not commercial.
These scores are also returned by the application as an exit code, allowing return values to be used for other logic, such as email notifications or file classifications.
USING WITH POWERSHELL
Going a step further, the PowerShell snippet below can be used as a starting point to loop through folders and score all the MP3 files found.
using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks;
namespace IsMP3Song { public class Song { public string ArtistName; public string TrackName; public int Match; } class Program { static void Main(string[] args) { List<Song> songs = new List<Song>();
try { if (args.Length == 1) { string searchTerm = FormatSearchTerm(args[0]); string jsonReturn = GetRequest($"https://itunes.apple.com/search?term={searchTerm}"); dynamic jsonCollection = JObject.Parse(jsonReturn); foreach (var result in jsonCollection.results) { dynamic jsonObject = JObject.Parse(result.ToString()); double match = ((CalculateSimilarity(args[0], $"{jsonObject.artistName} - {jsonObject.trackName}")) * 100);
Song song = new Song(); song.ArtistName = jsonObject.artistName; song.TrackName = jsonObject.trackName; song.Match = (int)match; songs.Add(song); }
if (songs.Count > 0) { Song bestMatch = songs.OrderByDescending(s => s.Match).First(); Console.WriteLine($"{bestMatch.ArtistName} - {bestMatch.TrackName} [{bestMatch.Match}]"); Environment.Exit(bestMatch.Match); } else { Console.WriteLine($"Seems that is not a commercial MP3..."); Environment.Exit(0); }
The Windows Firewall provides an important layer of protection and a rich interface to configure it. Unfortunately, at the time of writing, the Active Directory Group Policy section for Windows Firewall with Advanced Security does not allow for the concept of item level filter, resulting in a GPO for each policy with different filtering requirements.
For example, if a handful of servers require FTP ports to be open and another group requires RDP to be allowed, you will have to create two separate GPOs.
Over time these GPOs really add up.
The solution to this is to do “Firewall as Code” using PowerShell.
Investigation
Well-behaved applications automatically register firewall rules which means that applications will happily continue to work when the firewall is enabled.
Unfortunately, some applications do not include this as part of their install process. These applications require a manual rule to be configured.
Involve your environment application owners, especially for providing information of running processes and supplying network diagrams. I recommend, however, that you do not directly ask application owners which ports an application needs. I’ve found that if you do that, you will usually get a massive list of irrelevant ports back, mainly because application owners will just give you a list of open ports found on the application servers.
Always check that ports which are required are limited to a specific process. This way you can whitelist the application, allowing all the required ports without actually worrying which protocols or ports are required.
The key to enabling Windows Firewall involves pre-staging firewall rules well in advance of actually flipping the switch. This is achieved with a PowerShell script, executed at an interval on all devices. Conditions within the script selectively create the appropriate rule.
For example, you only want MSSQL firewall rule on servers running MSSQL,
Create PowerShell Script
Although many options exist that would be able to achieve this, I picked PowerShell because of its popularity amongst Windows Administrators.
The PowerShell script to achieve this consists of the following concepts;
Environmental Variables
Global Variables
Local Variables
Conditions
Rules
Environmental Variables
Specifically, to creating path conditions and rules, it is always better to use Environmental variables if the application is installed into one of these common locations.
To get these variable names, use the following PowerShell command
# List PowerShell's Environmental Variables Get-Childitem -Path Env:* |Sort-Object Name
Global Variables
Global variables are variables that are used multiple times within the script thus, it makes sense to move these to the top of the file so that they are only evaluated once. The only downside to having multiple instances of these throughout the file is that the script will take longer to execute.
When a variable is changed from one condition to another and/or needs to be re-evaluated from one condition to the next, use a local variable.
Conditions
Conditions determine if a particular rule will be applied. That said, a rule without a condition is considered a global rule because it applies to all devices
Some examples:
Domain Roles
switch -regex ($computer.DomainRole) { "0|1" { #Rules for workstations go here } "2|3" { #Rules for servers go here } "4|5" { #Rules for domain controllers go here } }
Group membership
If ($computerGroupMembers -contains "Group01") { #Rules go here }
Multiple group membership
If ($computerGroupMembers -contains "Group01" -or $computerGroupMembers -contains "Group01") { #Rules go here }
Application binary exists at path using environmental variables
If (Test-Path -Path "$($Env:programfiles)\SomePath\SomeApp.exe"){ #Rules go here } If (Test-Path -Path "$($Env:SystemDrive)\SomePath\SomeApp.exe "){ #Rules go here } If (Test-Path -Path "$(${Env:ProgramFiles(x86)})\SomePath\SomeApp.exe") { #Rules go here }
Application binary running (notice the local variable)
$processPath = (Get-Process -Name SomeProcessName).path; If (Test-Path -Path "$($processPath)") { #Rules go here }
Rules
Each rule name must be unique and descriptive and if applicable to a specific protocol, add the protocol to the rule name within parentheses.
Please note: PowerShell has a powerful CMDLet called New-NetFirewallRule that vastly simplifies the addition of firewall rule. Unfortunately, older versions of PowerShell do not contain it and as such, NETSH is used because it is compatible with more devices. The downside of NETSH is that we first need to delete the rule to ensure we do not create duplicate rules on subsequent executions.
A token is generated for a supplied account with the desired password.
Example of a token: k8vVeIYZeI+6rkvlvw8eLOEnHK2yTcBfHQP4UEZrCgigcagy7+qt969LISkmHH/7CS5KfVWLEZh8cZMzCkVYGw==
This
token (an AES-256 encrypted version of the username and the password)
is passed to the SecurelySetPassword tool which is executed at start-up
via an Active Directory Group Policy.
The token is decrypted and used to set the password for the specified account to the desired password.
1) Run SecurelySetPassword.exe USERNAME PASSWORD (Note how the generated token is different on each run, this is because the value is salted for added security)
2) Copy token. It will be used in the implementation steps
3) To test the token, run SecurelySetPassword.exe TOKEN (Note for a successful test the user needs to exists)
Step 3: Copy SecurelySetPassword.exe to a network share
Copy SecurelySetPassword.exe to a network share accessible by all users (such as NETLOGON share)
Step 4: Implement Active Directory Group Policy
1) Start Microsoft Group Policy Management Console (GPMC.msc)
2) Create and link a new Group Policy with the desired scope
3) Browse to Computer Configuration > Preferences > Windows Settings > Files and add a new file object
4) Set the Source files(s) path to the location of SecurelySetPassword.exe (\\ittelligence.com\NETLOGON\Software\SecurelySetPassword\SecurelySetPassword.exe in my case)
5) Set the Destination file to %CommonAppdataDir%\SecurelySetPassword\SecurelySetPassword.exe
6) Browse to Computer Configuration > Preferences > Control Panel Settings > Scheduled Tasks and add a new scheduled task object
7) On the Triggers tab create new trigger and set to At startup
8) On the Actions tab create a Start a program action to %CommonAppdataDir%\SecurelySetPassword\SecurelySetPassword.exe with token as argument
I
hope you found this tutorial useful. You are encouraged to ask
questions, report any bugs or make any other comments about it below.
Prerequisites: The following assumptions have been made in this tutorial. Readers should have a basic working knowledge of Microsoft Active Directory, SQL Server and Visual Studio software.
Step 1: Create ACTIVE DIRECTORY SERVICE ACCOUNT
Create an Active directory service account with password reset as well as user account unlock permissions.
Step 2: Download Visual Studio Project
1) Download the provided source zip file by clicking this link (See below)
2) Extract and open the project in Visual Studio
Step 3: Create database
Note: The
basic steps for creating the database are listed below. Explaining MS
SQL functionality is beyond the scope of this article, but I am happy to
answer any questions in the comments section below.
1) From the Open Project in Visual Studio, open ModelSSPR.edmx
2) Right-click on white-space on the diagram page
3) Then select Generate Database from Model as shown below
4) Save the SQL script and use it on Microsoft SQL Server to build the database schema
5) Create an MS SQL user and grant it DB owner rights
step 4: Modify config file
1) From the open project in Visual Studio
2) Replace the ADConnectionString connection string with the Active Directory LDAP string for the domain used in the Create Active Directory Service Account (Step 1)
3) Replace the SSPREntities connection string with the connection string of the database used in the Create Database (Step 3)
4) Configure ADMembershipProvider to the account created in the Create Active Directory Service Account (Step 1)
5) Replace the appSettings values with the correct information for the domain and account used in the Create Active Directory Service Account (Step 1)
Step 5: Publish Site
Please Note: Explaining
Visual Studio publishing is beyond the scope of this article, but I am
happy to answer any questions in the comments section below.
1) From the open project in Visual Studio
2) Publish site with the Visual Studio Publishing wizard
step 6: Testing Site
Registering password hints
1) Browse to site published in Publish Site (Step 5)
2) Click on Log in
3) Specify the Username and Password for the account to register for self-service password reset.
Note: Username must be in UPN format
4) Create password hints by adding questions and answers
Note: At least four hints need to be specified to utilize the self-service password reset function.
Self-Service Password Reset Request
1) Browse to the site published in the Publish Site (Step 5)
2) Click on Reset Password
3) Enter the Username for the account to reset the password for as shown below
Note: Username must be in UPN format
4) Enter answers to the security questions and provide new password
Note: Three random questions will be selected out of the hints configured
5) Click Reset Password
6) If the password was successfully reset, the following screen will display
I
hope you found this tutorial useful. You are encouraged to ask
questions, report any bugs or make any other comments about it below.
Basically, Alan used an opensource C# racing game to build a whole course on Azure Stream Analytics and the processing of telemetry data generated by it.
I took inspiration from it and manipulated a C++ opensource game OpenTTD to output JSON messages to disk which is then streamed to Azure via an Eventhub.
After scouring the code I found a procedure called OnNewDay which is called for each vehicle when a day is over.
This procedure was perfect for adding a function that would log out the vehicle information
Here is the full listing of LogVehicleEvent (bear in mind I primarily code in C#)
bool LogVehicleEvent(Vehicle *v) { /* OwnerByte owner = v->owner;///< Which company owns the vehicle? EngineID engine_type = v->engine_type;///< The type of engine used for this vehicle. int CargoDaysInTransit = &v->cargo.DaysInTransit;///< Number of days cargo has been in transit CargoID cargo_type = this->cargo_type;///< type of cargo this vehicle is carrying byte cargo_subtype = this->cargo_subtype;///< Used for livery refits (NewGRF variations) uint16 cargo_cap = this->cargo_cap;///< total capacity uint16 refit_cap = this->refit_cap;///< Capacity left over from before last refit. VehicleCargoList cargo = this->cargo;///< The cargo this vehicle is carrying int8 trip_occupancy = this->trip_occupancy;///< NOSAVE: Occupancy of vehicle of the current trip (updated after leaving a station). byte day_counter = this->day_counter;///< Increased by one for each day byte vehstatus = this->vehstatus;///< Status */
Rect coord = v->coord;///< NOSAVE: Graphical bounding box of the vehicle, i.e. what to redraw on moves. Money value = v->value;///< Value of the vehicle Money profit_this_year = v->profit_this_year;///< Profit this year << 8, low 8 bits are fract Money profit_last_year = v->profit_last_year;///< Profit last year << 8, low 8 bits are fract Year build_year = v->build_year;///< Year the vehicle has been built. Date max_age = v->max_age;///< Maximum age Date age = v->age;///< Age in days UnitID unitnumber = v->unitnumber;///< unit number, for display purposes only uint16 cur_speed = v->cur_speed;///< current speed Date date_of_last_service = v->date_of_last_service;///< Last date the vehicle had a service at a depot. byte breakdowns_since_last_service = v->breakdowns_since_last_service;///< Counter for the amount of breakdowns. Order current_order = v->current_order;///< The current order (+ status, like: loading) StationID last_station_visited = v->last_station_visited;///< The last station we stopped at. StationID last_loading_station = v->last_loading_station;///< Last station the vehicle has stopped at and could possibly leave from with any cargo loaded.
if (unitnumber != 0) { string name; string type; time_t now = time(0);
switch (v->type) { case VEH_TRAIN: type = "Train"; break; case VEH_ROAD: type = "Road Vehicle"; break; case VEH_SHIP: type = "Ship"; break; case VEH_AIRCRAFT: type = "Aircraft"; break; default: type = "Unknown Vehicle"; }
if (v->name != NULL) { name = v->name; } else { name = type + " " + to_string(unitnumber); }
//Get Station Name from Station ID string GetTheStationName(StationID s) { string lastVisitedStation = ""; try { BaseStation *station = BaseStation::Get(s);
//Generate Random String string GetRandomString(uint l = 15, std::string charIndex = "abcdefghijklmnaoqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890") { // l and charIndex can be customized, but I've also initialized them.
uint length = rand() % l + 1; // length of the string is a random value that can be up to 'l' characters.
uint ri[15]; /* array of random values that will be used to iterate through random indexes of 'charIndex' */
for (uint i = 0; i < length; ++i) ri[i] = rand() % charIndex.length(); // assigns a random number to each index of "ri"
std::string rs = ""; // random string that will be returned by this function
for (uint i = 0; i < length; ++i) rs += charIndex[ri[i]]; // appends a random amount of random characters to "rs"
if (rs.empty()) GetRandomString(l, charIndex); // if the outcome is empty, then redo the generation (this doesn't work, help?) else return rs; }
The result is that a file is created every game day for each vehicle
And finally, here is an example after it passed through Azure Stream Analytics
Query
SELECT Name, Type, avg(CurrentSpeed) as AvgSpeed, max(CurrentSpeed) as MaxSpeed INTO [OpenTTDOutput] FROM [OpenTTDInput] GROUP BY Name, Type, TumblingWindow(minute,10)
c) On the Encrypt tab, enter the password for the account that will be performing the automated placement task. Encrypt it with key bRK92kDpCqpnPMEtFp1cdJXixgqOqSKFUZ and record encrypted password
d) On the Settings tab, enter the domain information, connection username and the encrypted password recorded in step c. Configure UserNameMatch to a RegEx query that will match user account format. If you do not have a specific format, use .*
e) On the UserFolderLocations tab, specify folders to monitor to redundant user data
f) On the UsersToSkip tab, specify user folders to skip
Using this process will keep recover space by removing old/redundant user data, just make sure that you have backups to cheap/slower storage in case you need to recover data.
To be able to update computer descriptions you need to delegate rights.
Add the following permissions to Active Directory either to the root of the domain or any other Organizational Unit. You would add it to an Organizational Unit if you only want to use this process for some computers
b) Powershell Script
Below is the Powershell script used to update the computer description.
It is important to note that you should not change the format of the message if you are planning to use my automated object placement process.
This script will be used within a group policy in step c
try { # Get current user name $strUserName = $env:username;
# Get current computer name $strComputerName = $env:computername;
c) On the Encrypt tab, enter the password for the account that will be performing the automated placement task. Encrypt it with key 2xCJvezFBYWQPBeHy7USdajK55M8skww and record encrypted password
d) On the Settings tab, enter the domain information, connection user name and the encrypted password recorded in step 2c.
Specify which objects AutoAD should create automatically
e) Specify Active Directory information. The format for these are Subnet/Bit Mask|AD Site Name|Computer DN|User DN
Subnet/Bit Mask: The subnet and mask (in bit format) for the specific entry
AD Site: The Active Directory site to which the subnet belongs
Computer DN: The distinguished name of the organizational unit where to move computers to for computer objects in this subnet
User DN: The distinguished name of the organizational unit where to move users to for user objects in this subnet
Please Note: Ensure that you do not allow users/admins to gain any additional permissions by moving users from one container to another. The reason for this is that a user move might be forced to an incorrect OU if descriptions are tampered with.
f) Specify any user DNs that should be skipped
g) Specify any computer DNs that should be skipped
DEMO EXECUTION
After implementing ComputerDescriptionUpdate.ps1 notice how computer descriptions are automatically updated
AutoAD.exe output
Sites and subnets automatically created by AutoAD
Organizational Units automatically created by AutoAD
Object placement (example 1)
Object placement (example 2)
Object placement (example 3)
Object placement (example 4)
CONCLUSION
Using this process will keep Active Directory organized and objects in the correct Organizational Units
Sets a unique, secure password on each password reset
Helpdesk employee users do not need to use or install RSAT (at least not for those only resetting passwords)
End-users do not get passwords such as Password1 or Company1 and continue with this bad practice by continuing with passwords such as Password2 or Company2
Active Directory provided a mechanism to set account password via GPO preferences. Unfortunately this was done with an encrypted cpassword value for which the decryption key was published and subsequently the functionality was blocked.
Later, Local Administrator Password Solution (LAPS), was adopted by Microsoft and it is well suited for the task.
The approach described in this article is an alternative method to LAPS which can be used if the schema extension requirement of LAPS cannot be done. It also allows for multiple account management.
For this solution you will need:
File Share
SQL Server
Group Policy
PasswordImporter
PasswordViewer
Both PasswordImport and PasswordViewer is available within the same download here
Create share
Create a folder on a server which will hold the passwords reset output files from computers that get the GPO.
It is important to only allow administrators to view files within this folder. Everyone (authenticated users) should only have create/write permission (as shown below). The reason for this is that password will be stored here in clear-text until the PasswordImporter
tool processed it.
Create GPO
Create a GPO and filter it to the computer objects for the computers you wish to manage passwords for.
The startup script needs to be set to save password text files to the share created in an earlier step.
Create SQL Table
Use the following SQL script to create a table within SQL to store passwords. Assign a group to this group to SQL and grant it SELECT rights on the Passwords table. Add all your administrators and support personnel into this group.
CREATE TABLE [dbo].[Passwords](
[ID] [int] IDENTITY(1,1) NOT NULL,
[ComputerName] [varchar](255) NOT NULL,
[Password] [varchar](255) NOT NULL,
CONSTRAINT [PK_Passwords] 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]
Below is an example of the passwords stored encrypted within SQL
Setup PasswordImporter
PasswordImporter is an application that reads password files from the share and import is, encrypted, into SQL. After a successful import, the password file is deleted.
Open Configurator and specify Location, ConnectionString and LogFile values. Note that Location and ConnectionString needs to be encrypted with encryption key wL!PIQj%EeWj%L^e$nEpdjFzL0d9%Y1#
Schedule this application to run every few minutes to import passwords to SQL database
Setup PasswordViewer
PasswordViewer is an application that shows the specified computers password. This application should be distributed to all administrators and support personnel.
Open Configurator and specify FQDN, NetBIOS and ConnectionString values. Note that ConnectionString needs to provide placeholder values for Domain, Username and Password, for example:
Data source=YOURSQLSERVER;initial catalog=YOURDATABASE;Integrated Security=SSPI; User Id = [DOMAINNAME\USERNAME]; Password = [PASSWORD]