Skip to content

Commit 2999d82

Browse files
authored
feat: add support for configuration policies (#154)
* feat: add support for configuration policies * fix: more assignment changes
1 parent 819e42b commit 2999d82

File tree

9 files changed

+836
-1
lines changed

9 files changed

+836
-1
lines changed

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,5 @@ export {
1212
GroupPolicyDefinitionValue,
1313
Group,
1414
DetectedApp,
15+
DeviceManagementConfigurationPolicy,
1516
} from '@microsoft/microsoft-graph-types-beta'
Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
import { Client } from '@microsoft/microsoft-graph-client'
2+
import { DeviceConfigurationPolicies } from './deviceConfigurationPolicies'
3+
import { mockClient } from '../../../__fixtures__/@microsoft/microsoft-graph-client'
4+
5+
describe('Device Configuration Policies', () => {
6+
let graphClient: Client
7+
let configurationPolicies: DeviceConfigurationPolicies
8+
9+
const configurationPolicy = {
10+
name: 'test policy',
11+
'@odata.type': '#microsoft.graph.deviceManagementConfigurationPolicy',
12+
}
13+
14+
const policyAssignment = {
15+
'@odata.type': '#microsoft.graph.deviceManagementConfigurationPolicyAssignment',
16+
target: {
17+
'@odata.type': '#microsoft.graph.groupAssignmentTarget',
18+
groupId: '1',
19+
},
20+
}
21+
22+
beforeEach(() => {
23+
graphClient = mockClient() as never as Client
24+
configurationPolicies = new DeviceConfigurationPolicies(graphClient)
25+
})
26+
27+
it('should list all configuration policies', async () => {
28+
jest.spyOn(graphClient.api(''), 'get').mockResolvedValueOnce({
29+
value: [configurationPolicy],
30+
'@odata.nextLink': 'next',
31+
})
32+
jest.spyOn(graphClient.api(''), 'get').mockResolvedValueOnce({
33+
value: [configurationPolicy],
34+
})
35+
36+
const result = await configurationPolicies.list()
37+
expect(result).toEqual([configurationPolicy, configurationPolicy])
38+
})
39+
40+
it('should get a configuration policy', async () => {
41+
jest.spyOn(graphClient.api(''), 'get').mockResolvedValue(configurationPolicy)
42+
const result = await configurationPolicies.get('id')
43+
expect(result).toEqual(configurationPolicy)
44+
})
45+
46+
it('should create a configuration policy', async () => {
47+
jest.spyOn(graphClient.api(''), 'post').mockResolvedValue(configurationPolicy)
48+
const result = await configurationPolicies.create(configurationPolicy)
49+
expect(result).toEqual(configurationPolicy)
50+
})
51+
52+
it('should update a configuration policy', async () => {
53+
jest.spyOn(graphClient.api(''), 'patch')
54+
const result = await configurationPolicies.update('id', configurationPolicy)
55+
expect(result).toBeUndefined()
56+
})
57+
58+
it('should delete a configuration policy', async () => {
59+
jest.spyOn(graphClient.api(''), 'delete')
60+
const result = await configurationPolicies.delete('id')
61+
expect(result).toBeUndefined()
62+
})
63+
64+
describe('setAssignments', () => {
65+
it('should assign to all devices with exclusions', async () => {
66+
const spy = jest.spyOn(graphClient.api(''), 'post')
67+
68+
await configurationPolicies.setAssignments('id', {
69+
allDevices: true,
70+
excludeGroups: ['group1', 'group2'],
71+
})
72+
73+
expect(spy).toHaveBeenCalledWith({
74+
assignments: [
75+
{
76+
id: '',
77+
source: 'direct',
78+
target: {
79+
'@odata.type': '#microsoft.graph.allDevicesAssignmentTarget',
80+
deviceAndAppManagementAssignmentFilterType: 'none',
81+
},
82+
},
83+
{
84+
id: '',
85+
source: 'direct',
86+
target: {
87+
'@odata.type': '#microsoft.graph.exclusionGroupAssignmentTarget',
88+
deviceAndAppManagementAssignmentFilterType: 'none',
89+
groupId: 'group1',
90+
},
91+
},
92+
{
93+
id: '',
94+
source: 'direct',
95+
target: {
96+
'@odata.type': '#microsoft.graph.exclusionGroupAssignmentTarget',
97+
deviceAndAppManagementAssignmentFilterType: 'none',
98+
groupId: 'group2',
99+
},
100+
},
101+
],
102+
})
103+
})
104+
105+
it('should assign to all licensed users', async () => {
106+
const spy = jest.spyOn(graphClient.api(''), 'post')
107+
108+
await configurationPolicies.setAssignments('id', {
109+
allUsers: true,
110+
})
111+
112+
expect(spy).toHaveBeenCalledWith({
113+
assignments: [
114+
{
115+
id: '',
116+
source: 'direct',
117+
target: {
118+
'@odata.type': '#microsoft.graph.allLicensedUsersAssignmentTarget',
119+
deviceAndAppManagementAssignmentFilterType: 'none',
120+
},
121+
},
122+
],
123+
})
124+
})
125+
126+
it('should assign to specific included groups', async () => {
127+
const spy = jest.spyOn(graphClient.api(''), 'post')
128+
129+
await configurationPolicies.setAssignments('id', {
130+
includeGroups: ['group1', 'group2'],
131+
})
132+
133+
expect(spy).toHaveBeenCalledWith({
134+
assignments: [
135+
{
136+
id: '',
137+
source: 'direct',
138+
target: {
139+
'@odata.type': '#microsoft.graph.groupAssignmentTarget',
140+
deviceAndAppManagementAssignmentFilterType: 'none',
141+
groupId: 'group1',
142+
},
143+
},
144+
{
145+
id: '',
146+
source: 'direct',
147+
target: {
148+
'@odata.type': '#microsoft.graph.groupAssignmentTarget',
149+
deviceAndAppManagementAssignmentFilterType: 'none',
150+
groupId: 'group2',
151+
},
152+
},
153+
],
154+
})
155+
})
156+
157+
it('should throw error when including groups with allDevices', async () => {
158+
await expect(
159+
configurationPolicies.setAssignments('id', {
160+
allDevices: true,
161+
includeGroups: ['group1'],
162+
}),
163+
).rejects.toThrow('Cannot include specific groups when allDevices is true')
164+
})
165+
166+
it('should support mix of include and exclude groups', async () => {
167+
const spy = jest.spyOn(graphClient.api(''), 'post')
168+
169+
await configurationPolicies.setAssignments('id', {
170+
includeGroups: ['group1'],
171+
excludeGroups: ['group2'],
172+
})
173+
174+
expect(spy).toHaveBeenCalledWith({
175+
assignments: [
176+
{
177+
id: '',
178+
source: 'direct',
179+
target: {
180+
'@odata.type': '#microsoft.graph.groupAssignmentTarget',
181+
deviceAndAppManagementAssignmentFilterType: 'none',
182+
groupId: 'group1',
183+
},
184+
},
185+
{
186+
id: '',
187+
source: 'direct',
188+
target: {
189+
'@odata.type': '#microsoft.graph.exclusionGroupAssignmentTarget',
190+
deviceAndAppManagementAssignmentFilterType: 'none',
191+
groupId: 'group2',
192+
},
193+
},
194+
],
195+
})
196+
})
197+
})
198+
199+
describe('pagination', () => {
200+
it('should handle pagination for list method', async () => {
201+
const firstPage = {
202+
value: [{ ...configurationPolicy, id: '1' }],
203+
'@odata.nextLink': 'https://graph.microsoft.com/beta/next-page',
204+
}
205+
const secondPage = {
206+
value: [{ ...configurationPolicy, id: '2' }],
207+
}
208+
209+
jest.spyOn(graphClient.api(''), 'get')
210+
.mockResolvedValueOnce(firstPage)
211+
.mockResolvedValueOnce(secondPage)
212+
213+
const result = await configurationPolicies.list()
214+
215+
expect(result).toHaveLength(2)
216+
expect(result[0].id).toBe('1')
217+
expect(result[1].id).toBe('2')
218+
})
219+
})
220+
})
Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
import { Client } from '@microsoft/microsoft-graph-client'
2+
import { DeviceManagementConfigurationPolicy } from '@microsoft/microsoft-graph-types-beta'
3+
4+
interface AssignmentTarget {
5+
'@odata.type': string
6+
deviceAndAppManagementAssignmentFilterType: 'none' | 'include' | 'exclude'
7+
groupId?: string
8+
}
9+
10+
interface Assignment {
11+
id?: string
12+
source?: 'direct'
13+
target: AssignmentTarget
14+
}
15+
16+
interface AssignmentOptions {
17+
includeGroups?: string[]
18+
excludeGroups?: string[]
19+
allDevices?: boolean
20+
allUsers?: boolean
21+
}
22+
23+
export class DeviceConfigurationPolicies {
24+
constructor(private readonly graphClient: Client) {}
25+
26+
/**
27+
* List all device management configuration policies
28+
*
29+
* @returns
30+
*/
31+
async list(): Promise<DeviceManagementConfigurationPolicy[]> {
32+
let res = await this.graphClient.api('/deviceManagement/configurationPolicies').get()
33+
const configurationPolicies: DeviceManagementConfigurationPolicy[] = res.value
34+
35+
while (res['@odata.nextLink']) {
36+
const nextLink = res['@odata.nextLink'].replace('https://graph.microsoft.com/beta', '')
37+
res = await this.graphClient.api(nextLink).get()
38+
const nextConfigurationPolicies = res.value as DeviceManagementConfigurationPolicy[]
39+
configurationPolicies.push(...nextConfigurationPolicies)
40+
}
41+
42+
return configurationPolicies
43+
}
44+
45+
/**
46+
* Get a device management configuration policy
47+
* @param configurationPolicyId
48+
* @returns
49+
*/
50+
async get(configurationPolicyId: string): Promise<DeviceManagementConfigurationPolicy> {
51+
return await this.graphClient
52+
.api(`/deviceManagement/configurationPolicies/${configurationPolicyId}`)
53+
.get()
54+
}
55+
56+
/**
57+
* Create a device management configuration policy
58+
* @param configurationPolicy
59+
* @returns
60+
*/
61+
async create(
62+
configurationPolicy: DeviceManagementConfigurationPolicy,
63+
): Promise<DeviceManagementConfigurationPolicy> {
64+
return this.graphClient
65+
.api('/deviceManagement/configurationPolicies')
66+
.post(configurationPolicy)
67+
}
68+
69+
/**
70+
* Update a device management configuration policy
71+
* @param configurationPolicyId
72+
* @param configurationPolicy
73+
*/
74+
async update(
75+
configurationPolicyId: string,
76+
configurationPolicy: DeviceManagementConfigurationPolicy,
77+
): Promise<void> {
78+
await this.graphClient
79+
.api(`/deviceManagement/configurationPolicies/${configurationPolicyId}`)
80+
.patch(configurationPolicy)
81+
}
82+
83+
/**
84+
* Delete a device management configuration policy
85+
* @param configurationPolicyId
86+
*/
87+
async delete(configurationPolicyId: string): Promise<void> {
88+
await this.graphClient
89+
.api(`/deviceManagement/configurationPolicies/${configurationPolicyId}`)
90+
.delete()
91+
}
92+
93+
/**
94+
* Set assignments for a configuration policy
95+
*
96+
* THIS WILL OVERWRITE ANY EXISTING ASSIGNMENTS!
97+
* @param id - The ID of the configuration policy
98+
* @param options - Assignment options including groups to include/exclude and whether to assign to all devices/users
99+
* @returns Promise<void>
100+
*/
101+
async setAssignments(id: string, options: AssignmentOptions): Promise<void> {
102+
const assignments: Assignment[] = []
103+
104+
// Add all devices assignment if specified
105+
if (options.allDevices) {
106+
assignments.push({
107+
id: '',
108+
source: 'direct',
109+
target: {
110+
'@odata.type': '#microsoft.graph.allDevicesAssignmentTarget',
111+
deviceAndAppManagementAssignmentFilterType: 'none',
112+
},
113+
})
114+
115+
// When all devices is selected, we can only have exclusion groups
116+
if (options.includeGroups?.length) {
117+
throw new Error('Cannot include specific groups when allDevices is true')
118+
}
119+
}
120+
121+
// Add all licensed users assignment if specified
122+
if (options.allUsers) {
123+
assignments.push({
124+
id: '',
125+
source: 'direct',
126+
target: {
127+
'@odata.type': '#microsoft.graph.allLicensedUsersAssignmentTarget',
128+
deviceAndAppManagementAssignmentFilterType: 'none',
129+
},
130+
})
131+
}
132+
133+
// Add included groups
134+
if (options.includeGroups?.length) {
135+
assignments.push(
136+
...options.includeGroups.map(
137+
(groupId): Assignment => ({
138+
id: '',
139+
source: 'direct',
140+
target: {
141+
'@odata.type': '#microsoft.graph.groupAssignmentTarget',
142+
deviceAndAppManagementAssignmentFilterType: 'none',
143+
groupId,
144+
},
145+
}),
146+
),
147+
)
148+
}
149+
150+
// Add excluded groups
151+
if (options.excludeGroups?.length) {
152+
assignments.push(
153+
...options.excludeGroups.map(
154+
(groupId): Assignment => ({
155+
id: '',
156+
source: 'direct',
157+
target: {
158+
'@odata.type': '#microsoft.graph.exclusionGroupAssignmentTarget',
159+
deviceAndAppManagementAssignmentFilterType: 'none',
160+
groupId,
161+
},
162+
}),
163+
),
164+
)
165+
}
166+
167+
await this.graphClient
168+
.api(`/deviceManagement/configurationPolicies('${id}')/assign`)
169+
.post({ assignments })
170+
}
171+
}

0 commit comments

Comments
 (0)