Skip to content

Commit

Permalink
feat: add support for configuration policies (#154)
Browse files Browse the repository at this point in the history
* feat: add support for configuration policies

* fix: more assignment changes
  • Loading branch information
santese authored Jan 15, 2025
1 parent 819e42b commit 2999d82
Show file tree
Hide file tree
Showing 9 changed files with 836 additions and 1 deletion.
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ export {
GroupPolicyDefinitionValue,
Group,
DetectedApp,
DeviceManagementConfigurationPolicy,
} from '@microsoft/microsoft-graph-types-beta'
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
import { Client } from '@microsoft/microsoft-graph-client'
import { DeviceConfigurationPolicies } from './deviceConfigurationPolicies'
import { mockClient } from '../../../__fixtures__/@microsoft/microsoft-graph-client'

describe('Device Configuration Policies', () => {
let graphClient: Client
let configurationPolicies: DeviceConfigurationPolicies

const configurationPolicy = {
name: 'test policy',
'@odata.type': '#microsoft.graph.deviceManagementConfigurationPolicy',
}

const policyAssignment = {
'@odata.type': '#microsoft.graph.deviceManagementConfigurationPolicyAssignment',
target: {
'@odata.type': '#microsoft.graph.groupAssignmentTarget',
groupId: '1',
},
}

beforeEach(() => {
graphClient = mockClient() as never as Client
configurationPolicies = new DeviceConfigurationPolicies(graphClient)
})

it('should list all configuration policies', async () => {
jest.spyOn(graphClient.api(''), 'get').mockResolvedValueOnce({
value: [configurationPolicy],
'@odata.nextLink': 'next',
})
jest.spyOn(graphClient.api(''), 'get').mockResolvedValueOnce({
value: [configurationPolicy],
})

const result = await configurationPolicies.list()
expect(result).toEqual([configurationPolicy, configurationPolicy])
})

it('should get a configuration policy', async () => {
jest.spyOn(graphClient.api(''), 'get').mockResolvedValue(configurationPolicy)
const result = await configurationPolicies.get('id')
expect(result).toEqual(configurationPolicy)
})

it('should create a configuration policy', async () => {
jest.spyOn(graphClient.api(''), 'post').mockResolvedValue(configurationPolicy)
const result = await configurationPolicies.create(configurationPolicy)
expect(result).toEqual(configurationPolicy)
})

it('should update a configuration policy', async () => {
jest.spyOn(graphClient.api(''), 'patch')
const result = await configurationPolicies.update('id', configurationPolicy)
expect(result).toBeUndefined()
})

it('should delete a configuration policy', async () => {
jest.spyOn(graphClient.api(''), 'delete')
const result = await configurationPolicies.delete('id')
expect(result).toBeUndefined()
})

describe('setAssignments', () => {
it('should assign to all devices with exclusions', async () => {
const spy = jest.spyOn(graphClient.api(''), 'post')

await configurationPolicies.setAssignments('id', {
allDevices: true,
excludeGroups: ['group1', 'group2'],
})

expect(spy).toHaveBeenCalledWith({
assignments: [
{
id: '',
source: 'direct',
target: {
'@odata.type': '#microsoft.graph.allDevicesAssignmentTarget',
deviceAndAppManagementAssignmentFilterType: 'none',
},
},
{
id: '',
source: 'direct',
target: {
'@odata.type': '#microsoft.graph.exclusionGroupAssignmentTarget',
deviceAndAppManagementAssignmentFilterType: 'none',
groupId: 'group1',
},
},
{
id: '',
source: 'direct',
target: {
'@odata.type': '#microsoft.graph.exclusionGroupAssignmentTarget',
deviceAndAppManagementAssignmentFilterType: 'none',
groupId: 'group2',
},
},
],
})
})

it('should assign to all licensed users', async () => {
const spy = jest.spyOn(graphClient.api(''), 'post')

await configurationPolicies.setAssignments('id', {
allUsers: true,
})

expect(spy).toHaveBeenCalledWith({
assignments: [
{
id: '',
source: 'direct',
target: {
'@odata.type': '#microsoft.graph.allLicensedUsersAssignmentTarget',
deviceAndAppManagementAssignmentFilterType: 'none',
},
},
],
})
})

it('should assign to specific included groups', async () => {
const spy = jest.spyOn(graphClient.api(''), 'post')

await configurationPolicies.setAssignments('id', {
includeGroups: ['group1', 'group2'],
})

expect(spy).toHaveBeenCalledWith({
assignments: [
{
id: '',
source: 'direct',
target: {
'@odata.type': '#microsoft.graph.groupAssignmentTarget',
deviceAndAppManagementAssignmentFilterType: 'none',
groupId: 'group1',
},
},
{
id: '',
source: 'direct',
target: {
'@odata.type': '#microsoft.graph.groupAssignmentTarget',
deviceAndAppManagementAssignmentFilterType: 'none',
groupId: 'group2',
},
},
],
})
})

it('should throw error when including groups with allDevices', async () => {
await expect(
configurationPolicies.setAssignments('id', {
allDevices: true,
includeGroups: ['group1'],
}),
).rejects.toThrow('Cannot include specific groups when allDevices is true')
})

it('should support mix of include and exclude groups', async () => {
const spy = jest.spyOn(graphClient.api(''), 'post')

await configurationPolicies.setAssignments('id', {
includeGroups: ['group1'],
excludeGroups: ['group2'],
})

expect(spy).toHaveBeenCalledWith({
assignments: [
{
id: '',
source: 'direct',
target: {
'@odata.type': '#microsoft.graph.groupAssignmentTarget',
deviceAndAppManagementAssignmentFilterType: 'none',
groupId: 'group1',
},
},
{
id: '',
source: 'direct',
target: {
'@odata.type': '#microsoft.graph.exclusionGroupAssignmentTarget',
deviceAndAppManagementAssignmentFilterType: 'none',
groupId: 'group2',
},
},
],
})
})
})

describe('pagination', () => {
it('should handle pagination for list method', async () => {
const firstPage = {
value: [{ ...configurationPolicy, id: '1' }],
'@odata.nextLink': 'https://graph.microsoft.com/beta/next-page',
}
const secondPage = {
value: [{ ...configurationPolicy, id: '2' }],
}

jest.spyOn(graphClient.api(''), 'get')
.mockResolvedValueOnce(firstPage)
.mockResolvedValueOnce(secondPage)

const result = await configurationPolicies.list()

expect(result).toHaveLength(2)
expect(result[0].id).toBe('1')
expect(result[1].id).toBe('2')
})
})
})
171 changes: 171 additions & 0 deletions src/lib/deviceConfigurationPolicies/deviceConfigurationPolicies.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import { Client } from '@microsoft/microsoft-graph-client'
import { DeviceManagementConfigurationPolicy } from '@microsoft/microsoft-graph-types-beta'

interface AssignmentTarget {
'@odata.type': string
deviceAndAppManagementAssignmentFilterType: 'none' | 'include' | 'exclude'
groupId?: string
}

interface Assignment {
id?: string
source?: 'direct'
target: AssignmentTarget
}

interface AssignmentOptions {
includeGroups?: string[]
excludeGroups?: string[]
allDevices?: boolean
allUsers?: boolean
}

export class DeviceConfigurationPolicies {
constructor(private readonly graphClient: Client) {}

/**
* List all device management configuration policies
*
* @returns
*/
async list(): Promise<DeviceManagementConfigurationPolicy[]> {
let res = await this.graphClient.api('/deviceManagement/configurationPolicies').get()
const configurationPolicies: DeviceManagementConfigurationPolicy[] = res.value

while (res['@odata.nextLink']) {
const nextLink = res['@odata.nextLink'].replace('https://graph.microsoft.com/beta', '')
res = await this.graphClient.api(nextLink).get()
const nextConfigurationPolicies = res.value as DeviceManagementConfigurationPolicy[]
configurationPolicies.push(...nextConfigurationPolicies)
}

return configurationPolicies
}

/**
* Get a device management configuration policy
* @param configurationPolicyId
* @returns
*/
async get(configurationPolicyId: string): Promise<DeviceManagementConfigurationPolicy> {
return await this.graphClient
.api(`/deviceManagement/configurationPolicies/${configurationPolicyId}`)
.get()
}

/**
* Create a device management configuration policy
* @param configurationPolicy
* @returns
*/
async create(
configurationPolicy: DeviceManagementConfigurationPolicy,
): Promise<DeviceManagementConfigurationPolicy> {
return this.graphClient
.api('/deviceManagement/configurationPolicies')
.post(configurationPolicy)
}

/**
* Update a device management configuration policy
* @param configurationPolicyId
* @param configurationPolicy
*/
async update(
configurationPolicyId: string,
configurationPolicy: DeviceManagementConfigurationPolicy,
): Promise<void> {
await this.graphClient
.api(`/deviceManagement/configurationPolicies/${configurationPolicyId}`)
.patch(configurationPolicy)
}

/**
* Delete a device management configuration policy
* @param configurationPolicyId
*/
async delete(configurationPolicyId: string): Promise<void> {
await this.graphClient
.api(`/deviceManagement/configurationPolicies/${configurationPolicyId}`)
.delete()
}

/**
* Set assignments for a configuration policy
*
* THIS WILL OVERWRITE ANY EXISTING ASSIGNMENTS!
* @param id - The ID of the configuration policy
* @param options - Assignment options including groups to include/exclude and whether to assign to all devices/users
* @returns Promise<void>
*/
async setAssignments(id: string, options: AssignmentOptions): Promise<void> {
const assignments: Assignment[] = []

// Add all devices assignment if specified
if (options.allDevices) {
assignments.push({
id: '',
source: 'direct',
target: {
'@odata.type': '#microsoft.graph.allDevicesAssignmentTarget',
deviceAndAppManagementAssignmentFilterType: 'none',
},
})

// When all devices is selected, we can only have exclusion groups
if (options.includeGroups?.length) {
throw new Error('Cannot include specific groups when allDevices is true')
}
}

// Add all licensed users assignment if specified
if (options.allUsers) {
assignments.push({
id: '',
source: 'direct',
target: {
'@odata.type': '#microsoft.graph.allLicensedUsersAssignmentTarget',
deviceAndAppManagementAssignmentFilterType: 'none',
},
})
}

// Add included groups
if (options.includeGroups?.length) {
assignments.push(
...options.includeGroups.map(
(groupId): Assignment => ({
id: '',
source: 'direct',
target: {
'@odata.type': '#microsoft.graph.groupAssignmentTarget',
deviceAndAppManagementAssignmentFilterType: 'none',
groupId,
},
}),
),
)
}

// Add excluded groups
if (options.excludeGroups?.length) {
assignments.push(
...options.excludeGroups.map(
(groupId): Assignment => ({
id: '',
source: 'direct',
target: {
'@odata.type': '#microsoft.graph.exclusionGroupAssignmentTarget',
deviceAndAppManagementAssignmentFilterType: 'none',
groupId,
},
}),
),
)
}

await this.graphClient
.api(`/deviceManagement/configurationPolicies('${id}')/assign`)
.post({ assignments })
}
}
Loading

0 comments on commit 2999d82

Please sign in to comment.