O Sertão será Cloud

Implementing the Strategy Pattern in Automating Microsoft Intune Device Management Functions App…

Implementing the Strategy Pattern in Automating Microsoft Intune Device Management Functions App with Typescript

Automating device management policies in Microsoft Intune is crucial for ensuring organizational compliance and device security. Utilizing the Strategy Pattern for this automation through Azure Functions can bring flexibility and maintainability to your codebase. In this article, we’ll explore how to implement this solution using TypeScript.

What is the Strategy Pattern?

The Strategy Pattern is a behavioral design pattern that allows a family of algorithms to be defined, encapsulated, and made interchangeable. This means you can change the algorithm that a class uses without altering the code of the class itself. In our example, this allows us to define different strategies for creating and assigning device policies and switch between them as needed.

What is the Microsoft Intune?

Microsoft Intune is an enterprise mobility management (EMM) service that helps organizations manage their mobile devices and Windows PCs seamlessly. It enables management of applications, security policies, software updates, and more, all from a single cloud-based platform.

Project Structure

We’ll structure our project into three main parts:

  1. Service Principal: Responsible for authenticating and obtaining access tokens from Azure EntraID.
  2. Strategies: Contains specific implementations for creating and assigning policies.
  3. PoliciesContext: A class that orchestrates the creation and assignment of policies using the strategies.

Environment Setup

Before we begin, you need to set up some environment variables in the Azure Portal:

Creating a Service Principal

Adding required permissions

  • CLIENT_ID: Azure EntraID client ID for authentication.
  • CLIENT_SECRET: Azure EntraID client secret.
  • TENANT_ID: Azure EntraID tenant ID.

1. Configuring Authentication with Azure EntraID

First, let’s set up the ServicePrincipal class, which handles obtaining the necessary access token to call Microsoft Graph APIs.

import { ClientSecretCredential } from "@azure/identity";

/**
* ServicePrincipal class to handle Azure EntraID authentication.
*/
export class ServicePrincipal {
/** The resource URL for Azure management. */
private readonly resource: string = "https://management.azure.com/.default";

/**
* Creates an instance of ServicePrincipal.
*
* @param {string} tenantId - The Azure EntraID tenant ID.
* @param {string} clientId - The client ID of the Azure EntraID app.
* @param {string} clientSecret - The client secret of the Azure EntraID app.
*/
constructor(
private readonly tenantId: string,
private readonly clientId: string,
private readonly clientSecret: string
) {}

/**
* Retrieves an access token from Azure EntraID.
*
* @returns {Promise<string>} - A promise that resolves to the access token.
* @throws {Error} - Throws an error if the access token cannot be acquired.
*/
public async getAccessToken(): Promise<string> {
try {
const credential = new ClientSecretCredential(this.tenantId, this.clientId, this.clientSecret);
const tokenResponse = await credential.getToken(this.resource);

if (!tokenResponse) {
throw new Error("Failed to acquire access token");
}

return tokenResponse.token;
} catch (error) {
console.error("Error acquiring access token:", error);
throw new Error("Failed to acquire access token");
}
}
}

2. Implementing Strategies

Next, we’ll define two strategies: one for compliance policies and another for Windows update policies. Each strategy implements methods for creating and assigning policies.

import axios, { AxiosRequestConfig } from "axios";
import { CompliancePolicy } from "../interfaces/CompliancePolicy";
import { CreatePolicyStrategy } from "../interfaces/Strategy";

/**
* Interface representing the output of a Compliance policy.
*/
export interface OutputCompliancePolicy extends CompliancePolicy {
id: string;
createdDateTime: Date;
lastModifiedDateTime: Date;
}

/**
* Strategy for creating compliance policies using Microsoft Graph API.
* @implements {CreatePolicyStrategy<OutputCompliancePolicy>}
*/
export class CompliancePolicyStrategy implements CreatePolicyStrategy<OutputCompliancePolicy> {
private graphBaseUrl: string = "https://graph.microsoft.com/beta";

/**
* Creates a new compliance policy.
* @param {string} accessToken - The access token for authenticating the request.
* @param {string} name - The display name of the policy.
* @param {string} description - The description of the policy.
* @returns {Promise<{ status: number; body: OutputCompliancePolicy; }>} The HTTP status code and the created policy object.
*/
async createPolicy(accessToken: string, name: string, description: string): Promise<{ status: number; body: OutputCompliancePolicy; }> {
const url = `${this.graphBaseUrl}/deviceManagement/deviceCompliancePolicies`;
const policy: CompliancePolicy = {
"@odata.type": "#microsoft.graph.windows10CompliancePolicy",
activeFirewallRequired: true,
antiSpywareRequired: true,
antivirusRequired: true,
bitLockerEnabled: true,
codeIntegrityEnabled: true,
defenderEnabled: true,
description: description,
deviceThreatProtectionEnabled: false,
deviceThreatProtectionRequiredSecurityLevel: "unavailable",
displayName: name,
passwordRequiredType: "deviceDefault",
roleScopeTagIds: ["0"],
rtpEnabled: true,
scheduledActionsForRule: [
{
ruleName: "PasswordRequired",
scheduledActionConfigurations: [
{
actionType: "block",
gracePeriodHours: 12,
notificationMessageCCList: [],
notificationTemplateId: "",
},
{
actionType: "retire",
gracePeriodHours: 4320,
notificationMessageCCList: [],
notificationTemplateId: "",
},
],
},
],
secureBootEnabled: true,
signatureOutOfDate: true,
tpmRequired: true,
};

const config: AxiosRequestConfig = {
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
};

const response = await axios.post<OutputCompliancePolicy>(url, policy, config);
return {
status: 201,
body: response.data,
};
}
}
import axios, { AxiosRequestConfig } from "axios";
import { WindowsUpdateForBusinessPolicy } from "../interfaces/WindowsUpdateForBusinessPolicy";
import { CreatePolicyStrategy } from "../interfaces/Strategy";

/**
* Interface representing the output of a Windows Update for Business policy.
*/
export interface OutputWindowsUpdateForBusinessPolicy extends WindowsUpdateForBusinessPolicy {
id: string;
createdDateTime: Date;
lastModifiedDateTime: Date;
}

/**
* Strategy for creating Windows Update for Business policies using Microsoft Graph API.
* @implements {CreatePolicyStrategy<OutputWindowsUpdateForBusinessPolicy>}
*/
export class WindowsUpdateForBusinessPolicyStrategy implements CreatePolicyStrategy<OutputWindowsUpdateForBusinessPolicy> {
private graphBaseUrl: string = "https://graph.microsoft.com/beta";

/**
* Creates a new Windows Update for Business policy.
* @param {string} accessToken - The access token for authenticating the request.
* @param {string} name - The display name of the policy.
* @param {string} description - The description of the policy.
* @returns {Promise<{ status: number; body: OutputWindowsUpdateForBusinessPolicy; }>} The HTTP status code and the created policy object.
*/
async createPolicy(accessToken: string, name: string, description: string): Promise<{ status: number; body: OutputWindowsUpdateForBusinessPolicy; }> {
const url = `${this.graphBaseUrl}/deviceManagement/deviceConfigurations`;
const windowsUpdateConfig: WindowsUpdateForBusinessPolicy = {
"@odata.type": "#microsoft.graph.windowsUpdateForBusinessConfiguration",
allowWindows11Upgrade: true,
automaticUpdateMode: "autoInstallAtMaintenanceTime",
autoRestartNotificationDismissal: "notConfigured",
businessReadyUpdatesOnly: "userDefined",
deadlineForFeatureUpdatesInDays: 5,
deadlineForQualityUpdatesInDays: 5,
deadlineGracePeriodInDays: 3,
description: description,
displayName: name,
driversExcluded: false,
engagedRestartDeadlineInDays: null,
engagedRestartSnoozeScheduleForFeatureUpdatesInDays: null,
engagedRestartSnoozeScheduleInDays: null,
engagedRestartTransitionScheduleForFeatureUpdatesInDays: null,
engagedRestartTransitionScheduleInDays: null,
featureUpdatesDeferralPeriodInDays: 0,
featureUpdatesPaused: false,
featureUpdatesRollbackWindowInDays: 10,
installationSchedule: {
"@odata.type": "#microsoft.graph.windowsUpdateActiveHoursInstall",
activeHoursEnd: "17:00:00.0000000",
activeHoursStart: "08:00:00.0000000"
},
microsoftUpdateServiceAllowed: true,
postponeRebootUntilAfterDeadline: false,
qualityUpdatesDeferralPeriodInDays: 10,
qualityUpdatesPaused: false,
roleScopeTagIds: [],
scheduleImminentRestartWarningInMinutes: null,
scheduleRestartWarningInHours: null,
skipChecksBeforeRestart: false,
updateNotificationLevel: "restartWarningsOnly",
updateWeeks: null,
userPauseAccess: "enabled",
userWindowsUpdateScanAccess: "enabled"
};

const config: AxiosRequestConfig = {
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
},
};

const response = await axios.post<OutputWindowsUpdateForBusinessPolicy>(url, windowsUpdateConfig, config);
return {
status: 201,
body: response.data,
};
}
}

3. Orchestrating with PoliciesContext

The PoliciesContext class uses strategies to create and assign policies interchangeably.

import { AssignPolicyStrategy, CreatePolicyStrategy } from "../interfaces/Strategy";
import { AssignPolicyToGroupStrategy } from "../strategies/AssignPolicyToGroupStrategy";

/**
* Context class for managing policies.
* @template T - The type of policy to be managed.
*/
export class PoliciesContext<T> {
private assignPolicyStrategy: AssignPolicyStrategy;

/**
* Creates an instance of PoliciesContext.
* @param {string} name - The name of the policy to be created.
* @param {string} description - The description of the policy to be created.
* @param {CreatePolicyStrategy<T>} createPolicyStrategy - The strategy for creating policies.
*/
constructor(
private readonly name: string,
private readonly description: string,
private createPolicyStrategy: CreatePolicyStrategy<T>
) {
this.assignPolicyStrategy = new AssignPolicyToGroupStrategy();
}

/**
* Posts a new policy using the specified create policy strategy.
* @param {string} accessToken - The access token for authenticating the request.
* @returns {Promise<{ status: number; body: T; }>} The HTTP status code and the created policy object.
*/
async postPolicy(accessToken: string): Promise<{ status: number; body: T; }> {
return this.createPolicyStrategy.createPolicy(accessToken, this.name, this.description);
}

/**
* Assigns a policy to a group using the assigned policy strategy.
* @param {string} policyId - The ID of the policy to assign.
* @param {string} groupId - The ID of the group to assign the policy to.
* @param {string} accessToken - The access token for authenticating the request.
* @returns {Promise<{ status: number; body: { ok: boolean }; }>} The HTTP status code and the assignment result.
*/
async assignPolicy(policyId: string, groupId: string, accessToken: string): Promise<{ status: number; body: { ok: boolean }; }> {
return this.assignPolicyStrategy.assignPolicy(policyId, groupId, accessToken);
}
}

4. Implementing the Azure Function

Finally, let’s implement the Azure Function that uses PoliciesContext to create and assign policies.

import { AzureFunction, Context, HttpRequest } from "@azure/functions";
import { ServicePrincipal } from "../src/service/ServicePrincipal";
import { CompliancePolicyStrategy, OutputCompliancePolicy } from "../src/strategies/CompliancePolicyStrategy";
import { PoliciesContext } from "../src/service/PoliciesContext";

/**
* HTTP trigger function for processing compliance policy assignments.
*
* @param {Context} context - The context object for the Azure Function.
* @param {HttpRequest} req - The HTTP request object.
* @returns {Promise<void>} - A promise that resolves when the function completes.
*/
const httpTrigger: AzureFunction = async function (context: Context, req: HttpRequest): Promise<void> {
try {
context.log('HTTP trigger function processed a request.');

// Validate request method
if (req.method !== "POST") {
context.res = {
status: 405,
body: "Method Not Allowed",
};
return;
}

// Extract environment variables
const clientId: string = String(process.env.CLIENT_ID);
const clientSecret: string = String(process.env.CLIENT_SECRET);
const tenantId: string = String(process.env.TENANT_ID);

// Initialize ServicePrincipal with Azure EntraID credentials
context.log('Initializing ServicePrincipal with tenantId:', tenantId);
const servicePrincipal = new ServicePrincipal(tenantId, clientId, clientSecret);

// Request access token from Azure EntraID
context.log('Requesting access token');
const accessToken: string = await servicePrincipal.getAccessToken();
context.log('Access token received');

// Initialize CompliancePolicies instance with request parameters
const compliancePolicyStrategy = new CompliancePolicyStrategy();
const policyContext = new PoliciesContext<OutputCompliancePolicy>(
this.name,
this.description,
compliancePolicyStrategy
);
context.log('Initialized CompliancePolicies with name:', req.body.name, 'and description:', req.body.description);

// Check if complianceid is provided; if not, create a new compliance policy
let complianceid: string;
if (!req.body.complianceid) {
context.log('No complianceid provided, creating a new compliance policy');
const compliancePolicy = await policyContext.postPolicy(accessToken);
complianceid = compliancePolicy.body.id;
context.log('Created compliance policy with id:', complianceid);
} else {
complianceid = req.body.complianceid;
context.log('Using provided complianceid:', complianceid);
}

// Assign the policy to the specified group
context.log('Assigning policy with complianceid:', complianceid, 'to groupId:', req.body.groupId);
const response = await policyContext.assignPolicy(
complianceid,
req.body.groupId,
accessToken
);

// Prepare and send the HTTP response
context.res = {
status: 201,
body: response.body
};
context.log('Response sent');
} catch (error) {
context.log.error('Error processing request:', error.message);
context.res = {
status: 500,
body: { message: 'Internal Server Error' }
};
}
};

export default httpTrigger;

Conclusion

Using the Strategy Pattern to automate creation and assignment of policies in Microsoft Intune through Azure Functions allows for great flexibility and ease of code maintenance. With this approach, you can add or modify strategies without changing the main structure of the application, ensuring clean and organized code.

References


Implementing the Strategy Pattern in Automating Microsoft Intune Device Management Functions App… was originally published in Stackademic on Medium, where people are continuing the conversation by highlighting and responding to this story.