O Sertão será Cloud

Implementando o Strategy Pattern na Automação de Functions App para Gerenciamento de Dispositivos…

Implementando o Strategy Pattern na Automação de Functions App para Gerenciamento de Dispositivos do Microsoft Intune com Typescript

A automação de políticas de gerenciamento de dispositivos no Microsoft Intune é uma tarefa crítica para garantir a conformidade e a segurança dos dispositivos da organização. Utilizar o Strategy Pattern para essa automação através de Azure Functions pode trazer flexibilidade e manutenibilidade ao código. Neste artigo, vamos explorar como implementar essa solução utilizando TypeScript.

O que é o Strategy Pattern?

O Strategy Pattern é um padrão de design comportamental que permite que uma família de algoritmos seja definida, encapsulada e tornada intercambiável. Isso significa que você pode alterar o algoritmo que uma classe utiliza sem alterar o código dessa classe. No contexto de nosso exemplo, isso significa que podemos definir diferentes estratégias para criar e atribuir políticas de dispositivos e alternar entre elas conforme necessário.

O que é o Microsoft Intune?

O Microsoft Intune é um serviço de gerenciamento de mobilidade empresarial (EMM) que ajuda as organizações a gerenciar seus dispositivos móveis e PCs com Windows de forma integrada. Ele permite o gerenciamento de aplicativos, políticas de segurança, atualizações de software e muito mais, tudo a partir de uma única plataforma na nuvem.

Estrutura do Projeto

Vamos estruturar nosso projeto em três partes principais:

  1. Service Principal: Responsável por autenticar e obter tokens de acesso do Entra ID.
  2. Strategies: Contém as implementações específicas para criação e atribuição de políticas.
  3. PoliciesContext: Classe que orquestra a criação e atribuição de políticas utilizando as estratégias.

Configuração do Ambiente

Antes de começarmos, é necessário configurar algumas variáveis de ambiente no Azure Portal:

Criando um Service Principal

Adicionando as permissões necessárias

  • CLIENT_ID: ID do cliente Azure EntraID para autenticação.
  • CLIENT_SECRET: Segredo do cliente Azure EntraID.
  • TENANT_ID: ID do locatário Azure EntraID.

1. Configurando a Autenticação com Azure EntraID

Primeiro, vamos configurar a classe ServicePrincipal, responsável por obter o token de acesso necessário para chamar as APIs do Microsoft Graph.

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

/**
* ServicePrincipal class to handle Azure AD 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. Implementando as Estratégias

A seguir, vamos definir duas estratégias: uma para políticas de conformidade e outra para políticas de atualização do Windows. Cada estratégia implementa métodos para criar e atribuir políticas.

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. Orquestrando com PoliciesContext

A classe PoliciesContext utiliza as estratégias para criar e atribuir políticas de forma intercambiável.

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. Implementando a Azure Function

Finalmente, vamos implementar a Azure Function que utilizará o PoliciesContext para criar e atribuir políticas.

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;

Conclusão

A utilização do Strategy Pattern para automatizar a criação e atribuição de políticas no Microsoft Intune através de Azure Functions permite uma grande flexibilidade e facilidade de manutenção do código. Com essa abordagem, é possível adicionar ou modificar estratégias sem alterar a estrutura principal da aplicação, garantindo um código limpo e organizado.

Referencias