-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy path17-ext.Rmd
817 lines (641 loc) · 40.7 KB
/
17-ext.Rmd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
# Writing Group Policy Extensions
```{r, echo=FALSE, out.width="30%", fig.align='center'}
knitr::include_graphics("ext-images/write.png")
```
The chapter will explain how to write a Group Policy Extension for Samba's Winbind. Group Policy is a delivery mechanism for distributing system settings and company policies to machines joined to an Active Directory domain. Unix/Linux machines running Samba's Winbind can also deploy these policies.
## Creating the Server Side Extension {#sse}
\index{Server Side Extensions}
### Administrative Templates {#admx}
The first step to deploying Group Policy is to create a Server Side Extension (SSE). There are multiple ways to create an SSE, but here we'll only discuss Administrative Templates (ADMX). The purpose of the SSE is to deploy policies to the SYSVOL share. Theoretically, you could manually deploy any file (even plain text) to the SYSVOL and then write a Client Side Extension that parses it, but ADMX can be read and modified by the Group Policy Management Editor, which makes administration of policies simpler.
ADMX files are simply XML files which explain to the Group Policy Management Console how to display and store a policy in the SYSVOL. AMDX files always store policies in Registry.pol files. Samba provides a mechanism for parsing these, which we'll discuss later.
\index{Administrative Templates}
Below is a simple example of an ADMX template, and it's corresponding ADML file.
**samba.admx:**
```xml
<policyDefinitions revision="1.0" schemaVersion="1.0">
<policyNamespaces>
<using prefix="windows"
namespace="Microsoft.Policies.Windows" />
</policyNamespaces>
<supersededAdm fileName="" />
<resources minRequiredRevision="1.0" />
<categories>
<category name="CAT_SAMBA"
displayName="$(string.CAT_SAMBA)" />
<category name="CAT_UNIX_SETTINGS"
displayName="$(string.CAT_UNIX_SETTINGS)">
<parentCategory ref="CAT_SAMBA" />
</category>
</categories>
<policies>
<policy name="POL_DAILY_SCRIPTS" class="Machine"
displayName="$(string.POL_DAILY_SCRIPTS)"
explainText="$(string.POL_DAILY_SCRIPTS_Help)"
presentation="$(presentation.POL_DAILY_SCRIPTS)"
key="Software\Policies\Samba\Unix Settings">
<parentCategory ref="CAT_UNIX_SETTINGS" />
<supportedOn ref="windows:SUPPORTED_WindowsVista" />
<elements>
<list id="LST_DAILY_SCRIPTS"
key="Software\Policies\Samba\
Unix Settings\Daily Scripts"
valueName="Daily Scripts" />
</elements>
</policy>
</policies>
</policyDefinitions>
```
**en-US/samba.adml:**
```xml
<policyDefinitionResources revision="1.0" schemaVersion="1.0">
<displayName>
</displayName>
<description>
</description>
<resources>
<stringTable>
<string id="CAT_SAMBA">Samba</string>
<string id="CAT_UNIX_SETTINGS">Unix Settings</string>
<string id="POL_DAILY_SCRIPTS">Daily Scripts</string>
<string id="POL_DAILY_SCRIPTS_Help">
This policy setting allows you to execute commands,
either local or on remote storage, daily.
</string>
</stringTable>
<presentationTable>
<presentation id="POL_DAILY_SCRIPTS">
<listBox refId="LST_DAILY_SCRIPTS">
Script and arguments
</listBox>
</presentation>
</presentationTable>
</resources>
</policyDefinitionResources>
```
The meaning of the various tags are explained in Microsoft's Group Policy documentation at https://docs.microsoft.com/en-us/previous-versions/windows/desktop/policy/admx-schema. Before the endless documentation and confusing XML scares you away, be aware there is an easier way!
#### ADMX Migrator
FullArmor created the ADMX Migrator to simplify the shift for system administrators from the old ADM policy templates to ADMX. Fortunately, this tool also serves our purpose for assisting us in easily creating these templates for our SSE. Unfortunately, the tool hasn't seen any development in the past 8 years, and wont run in Windows 10 (or any Unix/Linux platform, for that matter). I had to dredge up a Windows 7 VM in order to install and use the tool.
\index{Administrative Templates}
##### Creating the Administrative Template
1. Open ADMX Migrator
2. Right click on ADMX Templates in the left tree view, and click New Template.
3. Give your template a name, and click OK.
4. Right click on the new template in the left tree view, and click New Category.
```{r, out.width="70%", echo=FALSE, fig.align='center', fig.pos = 'H'}
knitr::include_graphics("ext-images/new-category.png")
```
5. Give the Category a name. This name will be displayed in the Group Policy Management Editor under Administrative Templates. You can choose to nest template under an existing category, or simply add it as a new root.
::: {#info style="color: green;"}
Note: You can also add sub-categories under this category. After clicking OK, right click the category you created and select New Category.
:::
6. Next, create your policy by right clicking on your new category, and selecting New Policy Setting.
```{r, out.width="70%", echo=FALSE, fig.align='center', fig.pos = 'H'}
knitr::include_graphics("ext-images/new-policy.png")
```
7. Because we'll be applying these settings to a Linux machine, the Registry fields are mostly meaningless, but they are required. Your policies will be stored under these keys on the SYSVOL in the Registry.pol file. Choose some sensible Registry Key, such as 'Software\\Policies\\Samba\\Unix Settings', and a Registry Value Name, such as 'Daily Scripts' (these are the values used for Samba's cron.daily policy). The Display Name is the name that will be displayed for this policy in the Group Policy Management Editor. I usually make this the same as the Registry Value Name, but it doesn't need to be.
8. Select whether this policy will be applied to a Machine, a User, or to Both in the Class field. In our example, we could potentially set Both, then our Client Side Extension would need to handle both cron.daily scripts (the Machine) and also User crontab entries. Click OK for your policy to be created.
9. Your new policy will appear in the middle list view. Highlight it, and you will see a number of tabs below for configuring the policy.
```{r, out.width="70%", echo=FALSE, fig.align='center', fig.pos = 'H'}
knitr::include_graphics("ext-images/new-policy-list-view.png")
```
10. Select the Values tab and set the Enabled Value Type. In this case, we'll use String, since our cron commands will be saved to the Registry.pol as a string. In the Value field, you can set a default enabled value (this is optional).
11. Select the Presentation tab, right click in the Elements view, and click New Element > ListBox (or a different presentation, depending on the policy). If you look at the samba.adml file from the previous section, you'll notice that the presentationTable contains a listBox item. That's what we're creating here.
12. Choose an element Label, this will be the name for the list displayed in the Group Policy Management Editor.
13. Choose a Registry Key. This will be pre-populated with the parent Registry Key you gave when creating the policy. Append something to the key to make it unique. We'll use 'Software\\Policies\\Samba\\Unix Settings\\Daily Scripts' for our cron.daily policy.
14. Navigate to the Explain tab, and add an explanation of what this policy is and what it does. This will be displayed to users in the Group Policy Management Editor.
15. Now right click on your template name in the left tree, and select Save As.
16. Finally, you'll need to deploy your new policy definition to the SYSVOL. It should be saved to the Policies\\PolicyDefinitions (the Group Policy Central Store) directory. These instructions from Microsoft can assist you in setting up your Group Policy Central Store.
```{r, out.width="70%", echo=FALSE, fig.align='center', fig.pos = 'H'}
knitr::include_graphics("ext-images/sse.png")
```
### samba-tool gpo manage
The `samba-tool gpo manage` command is a tool provided by the Samba team for managing Group Policy Objects (GPOs). This command provides a number of subcommands that allow you to add, remove, and list policies within a GPO.
Adding subcommands to `samba-tool gpo manage` is one way to create a Server Side Extension (SSE) for Group Policy. Each `samba-tool gpo manage` command generally provides 3 subcommands for each policy; add, remove, and list. These subcommands allow you to add new policies to a GPO, remove existing policies from a GPO, and list the policies that are currently configured in a GPO.
Group Policy SSEs should be added to `samba-tool` in the `python/samba/netcmd/` `gpo.py` file.
#### Subcommands
To add python subcommands using the `SuperCommand` class, you will need to create a new class that inherits from the `SuperCommand` class, and define a subcommands attribute that lists the subcommands that are supported by your command. The subcommands attribute will be a dictionary containing keys with new command names, paired with instances of `Command` classes which implement the command.
For example:
```python
class cmd_scripts(SuperCommand):
"""Manage Scripts Group Policy Objects"""
subcommands = {}
subcommands["add"] = cmd_add_script()
subcommands["list"] = cmd_list_script()
subcommands["remove"] = cmd_remove_script()
```
Your `SuperCommand` will then need to be tied into an existing `samba-tool` command, for example:
```python
class cmd_manage(SuperCommand):
"""Manage Group Policy Objects"""
subcommands = {}
subcommands["sudoers"] = cmd_sudoers()
subcommands["security"] = cmd_security()
subcommands["smb_conf"] = cmd_smb_conf()
subcommands["symlink"] = cmd_symlink()
subcommands["files"] = cmd_files()
subcommands["openssh"] = cmd_openssh()
subcommands["motd"] = cmd_motd()
subcommands["issue"] = cmd_issue()
subcommands["access"] = cmd_access()
subcommands["scripts"] = cmd_scripts()
```
Notice that the `cmd_scripts` `SuperCommand` from earlier has been appended to the `cmd_manage` list of `subcommands`.
##### Implementing an Add Subcommand
To write a command that adds a new policy to a Group Policy Object (GPO), you will need to create a class that inherits from the Command class provided by the samba.netcmd module, and define a run method that takes a series of arguments and options, and contains the code that will be executed when the command is run.
The run method should begin by connecting to a Domain Controller (DC) using the `smb_connection` function defined in `python/samba/netcmd/gpo.py`. It can then retrieve the data from the GPO's Registry.pol file using the loadfile method of the connection object returned by the `smb_connection` function, or create a new file object if the file does not exist.
Next, the method can parse the data in the `Registry.pol` file using the `ndr_unpack` function from the `samba.ndr` module, which will return a file object representing the data in the file. We will then add a key to the list of entries in `Registry.pol` file object.
Finally, we save the modified file object back to the GPO's `Registry.pol` file on the DC using the savefile method of the connection object.
The class should also define a synopsis attribute, which provides a brief summary of the command.
Here is an example of what it might look:
```python
class cmd_add_script(Command):
"""Adds Script Group Policy to the sysvol
This command adds a script policy to the sysvol.
Example:
samba-tool gpo manage scripts add \
{31B2F340-016D-11D2-945F-00C04FB984F9} MACHINE daily \
test_script.sh '\\-n \\-p all'
policy_class is defined as either MACHINE or USER.
freq is defined as either Daily, Monthly, Weekly, or Hourly.
"""
synopsis = "%prog <gpo> <policy_class> <freq> <script> "
"[args] [options]"
takes_optiongroups = {
"sambaopts": options.SambaOptions,
"versionopts": options.VersionOptions,
"credopts": options.CredentialsOptions,
}
takes_options = [
Option("-H", "--URL",
help="LDB URL for database or target server",
type=str, metavar="URL", dest="H"),
]
takes_args = ["gpo", "policy_class", "freq", "script",
"args?"]
def run(self, gpo, policy_class, freq, script, args=None,
H=None, sambaopts=None, credopts=None,
versionopts=None):
policy_class = policy_class.upper()
if policy_class not in ['MACHINE', 'USER']:
raise CommandError("'%s' is not a valid policy_class."
" Choose from MACHINE or USER" % policy_class)
freq = freq.title()
if freq not in ['Daily', 'Monthly', 'Weekly', 'Hourly']:
raise CommandError("'%s' is not a valid frequency. "
"Choose from Daily, Monthly, Weekly, or "
"Hourly" % freq)
self.lp = sambaopts.get_loadparm()
self.creds = credopts.get_credentials(self.lp,
fallback_machine=True)
if not os.path.exists(script):
raise CommandError(
"Script '%s' does not exist" % script)
# We need to know writable DC to setup SMB connection
if H and H.startswith('ldap://'):
dc_hostname = H[7:]
self.url = H
else:
dc_hostname = netcmd_finddc(self.lp, self.creds)
self.url = dc_url(self.lp, self.creds, dc=dc_hostname)
# SMB connect to DC
conn = smb_connection(dc_hostname,
'sysvol',
lp=self.lp,
creds=self.creds)
realm = self.lp.get('realm')
pol_file = '\\'.join([realm.lower(), 'Policies', gpo,
'%s\\Registry.pol' % policy_class])
try:
pol_data = ndr_unpack(preg.file,
conn.loadfile(pol_file))
except NTSTATUSError as e:
if e.args[0] in [NT_STATUS_OBJECT_NAME_INVALID,
NT_STATUS_OBJECT_NAME_NOT_FOUND,
NT_STATUS_OBJECT_PATH_NOT_FOUND]:
# The file doesn't exist, so create it
pol_data = preg.file()
elif e.args[0] == NT_STATUS_ACCESS_DENIED:
raise CommandError("The authenticated user does "
"not have sufficient privileges")
else:
raise
reg_key = b'Software\\Policies\\Samba\\Unix Settings'
keyname = b'%s\\%s Scripts' % (reg_key, get_bytes(freq))
entry = '%s %s' % (script,
args if args is not None else '')
e = preg.entry()
e.keyname = keyname
e.valuename = reg_key
e.type = 1
e.data = get_bytes(entry)
entries = list(pol_data.entries)
entries.append(e)
pol_data.num_entries = len(entries)
pol_data.entries = entries
try:
conn.savefile(pol_file, ndr_pack(pol_data))
except NTSTATUSError as e:
if e.args[0] == NT_STATUS_ACCESS_DENIED:
raise CommandError("The authenticated user does "
"not have sufficient privileges")
raise
```
##### Implementing a List Subcommand
To write a command that lists the policies for a Group Policy Object (GPO), you would need to create a class that inherits from the `Command` class provided by the `samba.netcmd` module, and define a `run` method that takes a series of arguments and options, and contains the code that will be executed when the command is run.
The run method should begin by connecting to a Domain Controller (DC) using the `smb_connection` function defined in `python/samba/netcmd/gpo.py`. It can then retrieve the data from the GPO's Registry.pol file using the loadfile method of the connection object returned by the `smb_connection` function.
Next, the method can parse the data in the `Registry.pol` file using the `ndr_unpack` function from the `samba.ndr` module, which will return a file object representing the data in the file. The method can then iterate over the keys and values in the file object, printing out the names and data for each key.
The class should also define a synopsis attribute, which provides a brief summary of the command.
Here is an example of what it might look:
```python
class cmd_list_script(Command):
"""List Script Group Policy from the sysvol
This command lists the script policies currently set on the sysvol.
Example:
samba-tool gpo manage scripts list \
{31B2F340-016D-11D2-945F-00C04FB984F9}
"""
synopsis = "%prog <gpo> [options]"
takes_optiongroups = {
"sambaopts": options.SambaOptions,
"versionopts": options.VersionOptions,
"credopts": options.CredentialsOptions,
}
takes_options = [
Option("-H", "--URL",
help="LDB URL for database or target server",
type=str, metavar="URL", dest="H"),
]
takes_args = ["gpo"]
def run(self, gpo, H=None, sambaopts=None, credopts=None,
versionopts=None):
self.lp = sambaopts.get_loadparm()
self.creds = credopts.get_credentials(self.lp,
fallback_machine=True)
# We need to know writable DC to setup SMB connection
if H and H.startswith('ldap://'):
dc_hostname = H[7:]
self.url = H
else:
dc_hostname = netcmd_finddc(self.lp, self.creds)
self.url = dc_url(self.lp, self.creds, dc=dc_hostname)
# SMB connect to DC
conn = smb_connection(dc_hostname,
'sysvol',
lp=self.lp,
creds=self.creds)
realm = self.lp.get('realm')
pol_file = '\\'.join([realm.lower(), 'Policies', gpo,
'%s\\Registry.pol'])
for policy_class in ['MACHINE', 'USER']:
self.outf.write('%s:\n' % policy_class)
try:
pol_data = ndr_unpack(preg.file,
conn.loadfile(pol_file % policy_class))
except NTSTATUSError as e:
if e.args[0] in [NT_STATUS_OBJECT_NAME_INVALID,
NT_STATUS_OBJECT_NAME_NOT_FOUND,
NT_STATUS_OBJECT_PATH_NOT_FOUND]:
# The file doesn't exist,
# so there is nothing to list
continue
elif e.args[0] == NT_STATUS_ACCESS_DENIED:
raise CommandError("The authenticated user "
"does not have sufficient privileges")
else:
raise
reg_key = 'Software\\Policies\\Samba\\Unix Settings'
for e in pol_data.entries:
if e.valuename == "**delvals.":
continue
if e.keyname.startswith(reg_key) and \
e.keyname.endswith('Scripts'):
self.outf.write("\t%s:\n" % e.keyname)
self.outf.write("\t\t%s\n" % e.data)
```
##### Implementing a Remove Subcommand
To write a command that removes a policy from a Group Policy Object (GPO), you would need to create a class that inherits from the `Command` class provided by the `samba.netcmd` module, and define a `run` method that takes a series of arguments and options, and contains the code that will be executed when the command is run.
The run method should begin by connecting to a Domain Controller (DC) using the `smb_connection` function defined in `python/samba/netcmd/gpo.py`. It can then retrieve the data from the GPO's Registry.pol file using the loadfile method of the connection object returned by the `smb_connection` function.
Next, the method can parse the data in the Registry.pol file using the `ndr_unpack` function, which will return a file object representing the data in the file. It then checks whether the entry specified in the "script" variable exists in the list of entries contained in the `pol_data` object. If the entry does exist, it is removed from the list and the number of entries in the list is updated.
Finally, we save the modified file object back to the GPO's `Registry.pol` file on the DC using the savefile method of the connection object.
The class should also define a synopsis attribute, which provides a brief summary of the command.
Here is an example of what it might look:
```python
class cmd_remove_script(Command):
"""Removes Script Group Policy from the sysvol
This command removes a script policy from the sysvol.
Example:
samba-tool gpo manage scripts remove \
{31B2F340-016D-11D2-945F-00C04FB984F9} MACHINE daily \
'test_script.sh \\-n \\-p all'
policy_class is defined as either MACHINE or USER.
freq is defined as either Daily, Monthly, Weekly, or Hourly.
"""
synopsis = "%prog <gpo> <policy_class> <freq> <script>"
"[options]"
takes_optiongroups = {
"sambaopts": options.SambaOptions,
"versionopts": options.VersionOptions,
"credopts": options.CredentialsOptions,
}
takes_options = [
Option("-H", "--URL",
help="LDB URL for database or target server",
type=str, metavar="URL", dest="H"),
]
takes_args = ["gpo", "policy_class", "freq", "script"]
def run(self, gpo, policy_class, freq, script, H=None,
sambaopts=None, credopts=None, versionopts=None):
policy_class = policy_class.upper()
if policy_class not in ['MACHINE', 'USER']:
raise CommandError("'%s' is not a valid policy_class."
" Choose from MACHINE or USER" % policy_class)
freq = freq.title()
if freq not in ['Daily', 'Monthly', 'Weekly', 'Hourly']:
raise CommandError("'%s' is not a valid frequency. "
"Choose from Daily, Monthly, "
"Weekly, or Hourly" % freq)
self.lp = sambaopts.get_loadparm()
self.creds = credopts.get_credentials(self.lp,
fallback_machine=True)
# We need to know writable DC to setup SMB connection
if H and H.startswith('ldap://'):
dc_hostname = H[7:]
self.url = H
else:
dc_hostname = netcmd_finddc(self.lp, self.creds)
self.url = dc_url(self.lp, self.creds, dc=dc_hostname)
# SMB connect to DC
conn = smb_connection(dc_hostname,
'sysvol',
lp=self.lp,
creds=self.creds)
realm = self.lp.get('realm')
pol_file = '\\'.join([realm.lower(), 'Policies', gpo,
'%s\\Registry.pol' % policy_class])
try:
pol_data = ndr_unpack(preg.file,
conn.loadfile(pol_file))
except NTSTATUSError as e:
if e.args[0] in [NT_STATUS_OBJECT_NAME_INVALID,
NT_STATUS_OBJECT_NAME_NOT_FOUND,
NT_STATUS_OBJECT_PATH_NOT_FOUND]:
raise CommandError("Cannot remove script '%s' "
"because it does not exist" % script)
elif e.args[0] == NT_STATUS_ACCESS_DENIED:
raise CommandError("The authenticated user does "
"not have sufficient privileges")
else:
raise
script = script.strip()
if script in ([e.data.strip() for e in pol_data.entries] \
if pol_data else []):
entries = [e for e in pol_data.entries \
if e.data.strip() != script]
pol_data.num_entries = len(entries)
pol_data.entries = entries
try:
conn.savefile(pol_file, ndr_pack(pol_data))
except NTSTATUSError as e:
if e.args[0] == NT_STATUS_ACCESS_DENIED:
raise CommandError("The authenticated user "
"does not have sufficient"
" privileges")
raise
else:
raise CommandError("Cannot remove '%s' because it"
" does not exist" % script)
```
## Creating the Client Side Extension {#cse}
The following script defines a Group Policy Client Side Extension (CSE) in Python, which will be called by Samba's Winbind to deploy our newly created policy. A CSE is a program that runs on a client machine and processes Group Policy Objects (GPOs) that are applied to the machine. The CSE processes the GPOs by applying the policies they contain to the client machine.
\index{Client Side Extensions}
```python
#!/usr/bin/python3
import os, re
from samba.gpclass import gp_pol_ext, gp_file_applier,
register_gp_extension, unregister_gp_extension,
list_gp_extensions
from tempfile import NamedTemporaryFile
from samba.gp.util.logging import log
from samba import getopt as options
import optparse
intro = '''
### autogenerated by samba
#
# This file is generated by the gp_scripts_ext Group Policy
# Client Side Extension. To modify the contents of this file,
# modify the appropriate Group Policy objects which apply
# to this machine. DO NOT MODIFY THIS FILE DIRECTLY.
#
'''
class gp_scripts_ext(gp_pol_ext, gp_file_applier):
def __str__(self):
return 'Unix Settings/Scripts'
def process_group_policy(self, deleted_gpo_list,
changed_gpo_list):
# Iterate over GPO guids and their previous settings,
# reverting changes made by this GPO.
for guid, settings in deleted_gpo_list:
# Use the unapply() function from the base class
# gp_file_applier to remove the files.
if str(self) in settings:
for attribute, script in \
settings[str(self)].items():
# Delete the applied policy
self.unapply(guid, attribute, script)
# Iterate over GPO objects, applying new policies found
# in the SYSVOL
for gpo in changed_gpo_list:
if gpo.file_sys_path:
reg_key = 'Software\\Policies\\' + \
'Samba\\Unix Settings'
sections = { '%s\\Daily Scripts' % reg_key :
'/etc/cron.daily',
'%s\\Monthly Scripts' % reg_key :
'/etc/cron.monthly',
'%s\\Weekly Scripts' % reg_key :
'/etc/cron.weekly',
'%s\\Hourly Scripts' % reg_key :
'/etc/cron.hourly'
}
# Load the contents of the Registry.pol
# from the SYSVOL
pol_file = 'MACHINE/Registry.pol'
path = os.path.join(gpo.file_sys_path, pol_file)
pol_conf = self.parse(path)
if not pol_conf:
continue
# Gather the list of policies to apply
policies = {}
for e in pol_conf.entries:
if e.keyname in sections.keys() and \
e.data.strip():
if e.keyname not in policies:
policies[e.keyname] = []
policies[e.keyname].append(e.data)
# Specify the applier function, which will be
# used to apply the policy.
def applier_func(keyname, entries):
ret = []
cron_dir = sections[e.keyname]
for data in entries:
with NamedTemporaryFile(prefix='gp_',
mode="w+",
delete=False,
dir=cron_dir) as f:
contents = '#!/bin/sh\n%s' % intro
contents += '%s\n' % data
f.write(contents)
os.chmod(f.name, 0o700)
ret.append(f.name)
return ret
# For each policy in the Registry.pol,
# apply the settings
for keyname, entries in policies.items():
# Each GPO applies only one set of each type
# of script, so so the attribute matches the
# keyname.
attribute = keyname
# The value hash is generated from the script
# entries, ensuring any changes to this GPO
# will cause the scripts to be rewritten.
value_hash = self.generate_value_hash(*entries)
self.apply(gpo.name, attribute, value_hash,
applier_func, keyname, entries)
# Cleanup any old scripts that are no longer
# part of the policy
self.clean(gpo.name, keep=policies.keys())
def rsop(self, gpo):
output = {}
pol_file = 'MACHINE/Registry.pol'
if gpo.file_sys_path:
path = os.path.join(gpo.file_sys_path, pol_file)
pol_conf = self.parse(path)
if not pol_conf:
return output
for e in pol_conf.entries:
key = e.keyname.split('\\')[-1]
if key.endswith('Scripts') and e.data.strip():
if key not in output.keys():
output[key] = []
output[key].append(e.data)
return output
if __name__ == "__main__":
parser = optparse.OptionParser('gp_scripts_ext.py [options]')
sambaopts = options.SambaOptions(parser)
parser.add_option_group(sambaopts)
parser.add_option('--register',
help='Register extension to Samba',
action='store_true')
parser.add_option('--unregister',
help='Unregister extension from Samba',
action='store_true')
(opts, args) = parser.parse_args()
# We're collecting the Samba loadparm simply to
# find our smb.conf file
lp = sambaopts.get_loadparm()
# This is a random unique GUID, which identifies this CSE.
# Any random GUID will do.
ext_guid = '{5930022C-94FF-4ED5-A403-CFB4549DB6F0}'
if opts.register:
# The extension path is the location of this file. This
# script should be executed from a permanent location.
ext_path = os.path.realpath(__file__)
# The machine and user parameters tell Samba whether to
# apply this extension to the computer, to individual
# users, or to both.
register_gp_extension(ext_guid, 'gp_scripts_ext',
ext_path, smb_conf=lp.configfile,
machine=True, user=False)
elif opts.unregister:
# Remove the extension and do not apply policy.
unregister_gp_extension(ext_guid)
# List the currently installed Group Policy Client Side
# Extensions
exts = list_gp_extensions(lp.configfile)
for guid, data in exts.items():
print(guid)
for k, v in data.items():
print('\t%s: %s' % (k, v))
```
The CSE is defined by the `gp_scripts_ext` class, which is derived from the `gp_pol_ext` and `gp_file_applier` classes. The `gp_pol_ext` class provides a framework for processing GPOs, and the `gp_file_applier` class provides functions for applying GPO policies to files on the client machine.
### The gp\_ext and gp\_applier Python Classes
Your CSE must be a class that inherits from subclasses of `gp_ext` and `gp_applier`. The `gp_pol_ext` is a subclass of `gp_ext` that provides simplified parsing of Registry.pol files. If you choose to store your policies in ini/inf files in the SYSVOL (instead of using Administrative Templates), then you can inherit from the `gp_inf_ext` instead. The `gp_file_applier` is a subclass of `gp_applier` which provides convenience functions for applying and unapplying policies which add files to the machine.
If your class inherits from either `gp_pol_ext` or `gp_inf_ext`, it has a `parse()` function defined, which takes a single filename. The parse() function will parse the contents of the policy file and return it in a sensible format.
If for some reason you choose to store data on the SYSVOL in some other format (such as in XML, etc), you'll need to subclass `gp_ext`, then implement a `read()` function, like this:
```python
import xml.etree.ElementTree
class gp_xml_ext(gp_ext):
def read(self, data_file):
return xml.etree.ElementTree.parse(data_file)
```
The `read()` function is called by `parse()`, passing it a local filename tied to the systems SYSVOL cache. Then within `process_group_policy()` you can call `parse()` to fetch the parsed data from the SYSVOL.
The `gp_file_applier` class implements the helper functions `apply` and `unapply`. If you instead inherit from `gp_applier` directly, you'll need to implement `apply` and `unapply` yourself. The `gp_applier` class provides various helper functions for assisting you in creating the `apply` and `unapply` functions. The `apply` and `unapply` functions are responsible for both adding and removing policy, as well as managing the Group Policy Cache contents.
\index{Group Policy Cache}
### Process Group Policy
The CSE has two main functions: `process_group_policy` and `rsop`. The `process_group_policy` function is called by the Group Policy engine on the client machine to process the GPOs that apply to the machine. It takes two arguments: `deleted_gpo_list` and `changed_gpo_list`. The `deleted_gpo_list` argument is a list of GPOs that MUST be removed from the machine, and the `changed_gpo_list` argument is a list of GPOs that have been changed (or are new) and MUST be re-applied to the machine.
The `process_group_policy` function serves two primary purposes; it applies new policy, and it removes old policy. It first iterates over the `deleted_gpo_list`, using the unapply function from the `gp_file_applier` class to remove the files that were applied by the GPOs.
```python
for guid, settings in deleted_gpo_list:
if str(self) in settings:
for attribute, script in settings[str(self)].items():
self.unapply(guid, attribute, script)
```
The `deleted_gpo_list` is a dictionary which contains the guids of Group Policy Objects, with associated settings which were previously applied. This list of applied settings is generated by the second loop (`changed_gpo_list`) while it is applying policy.
The `process_group_policy` function then iterates over the `changed_gpo_list`, applying the policies contained in the GPOs to the client machine. This second loop is a little more involved. When we iterate over `changed_gpo_list`, we're actually iterating over a list of GPO objects. The attributes of the object are:
* `gpo.name`: The GUID of the GPO.
* `gpo.file_sys_path`: A physical path to a cache of GPO on the local filesystem.
There are other methods and attributes, but these are the only ones important to a CSE.
The primary purpose of this loop is to iterate over the GPOs, read their policy in the SYSVOL, then check the sections for the Registry Key we created in our Server Side Extension. If our policy Registry Key exists, then we read the entry and apply the policy.
In our example, we find the 'Software\\Policies\\Samba\\Unix Settings\\Daily Scripts' policy, then read the script contents from Registry.pol entry and write the script to a local file.
The `applier_func` function is called to apply the policies contained in the GPOs to the client machine. It takes two arguments: keyname and entries. The keyname argument is the name of the policy being applied, and the entries argument is a list of the policy entries. The function writes the policy entries to randomly generated file names in the appropriate cron directories on the client machine, and returns the names of the temporary files. The file names will be stored in the Group Policy Cache, for retrieving later for the `deleted_gpo_list`.
\index{Group Policy Cache}
### Resultant Set of Policy
The `rsop` function in the extension is optional. It should return a dictionary containing key/value pairs of what our current policy will apply or has applied. The function is passed a list of GPO objects (similar to our `changed_gpo_list`), and we should parse the list similar to how we did in `process_group_policy`.
The `rsop` function generates the output for our Resultant Set of Policy. RSoP is a feature in Group Policy that allows you to determine the effective settings that are applied to a user or a computer as a result of Group Policy processing. RSoP provides a report that shows the policies that have been applied to the user or computer, as well as any conflicts or errors that may have occurred during Group Policy processing. RSoP can be used to troubleshoot Group Policy issues, to verify that the correct policies are being applied, and to determine the impact of Group Policy on a particular user or computer.
This function enables the `samba-gpupdate --rsop` command (see Chapter \@ref(samba-gpupdate)).
\index{Resultant Set of Policy}
### Registering/Unregistering a Client Side Extension
The CSE also includes a function for registering and unregistering the CSE with the Group Policy engine on the client machine. While the example code provides a detailed example of how to register an extension, the basic requirement is simply to call `register_gp_extension()`.
\index{Client Side Extensions}
```python
ext_guid = '{5930022C-94FF-4ED5-A403-CFB4549DB6F0}'
ext_path = os.path.realpath(__file__)
register_gp_extension(ext_guid, 'gp_scripts_ext', ext_path,
smb_conf='/etc/samba/smb.conf', machine=True, user=False)
```
The extension guid can be any random guid. It simply must be unique among all extensions that you register to the host. The extension path is literally just the path to the source file containing your CSE.
You must pass your smb.conf file to the extension, so it knows where to store the list of registered extensions. You also must specify whether to apply this extension to the machine, or to individual users (or to both).
Unregistering the extension is simple. You call the `unregister_gp_extension()` and pass it the unique guid you previously chose which represents this CSE.
#### Registering/Unregistering a Client Side Extension via samba-tool
Alternatively, as of Samba 4.18, Client Side Extension registration can be managed using `samba-tool`.
The `samba-tool gpo cse register` command is used to register a Group Policy Client Side Extension (CSE) on a Linux client. The command takes two arguments: the path to the CSE file and the name of the CSE. It also accepts two options: `--machine` or `--user`. The command does not enable the CSE for either Machine or User policy by default.
To enable a CSE for Machine policy, you must use the `--machine` option when running the `samba-tool gpo cse register` command. For example, to register a CSE file located at `/root/policies/gp_test_ext.py` with the name `gp_test_ext` and enable it for Machine policy, the command would be:
```shell
samba-tool gpo cse register /root/policies/gp_test_ext.py \
gp_test_ext --machine
```
When registering a CSE, `samba-tool` will automatically generate a unique random GUID to identify the extension. To find the unique GUID of your extension, you can use the `samba-tool gpo cse list` command.
The output of the `samba-tool gpo cse list` command shows the GUID of the CSE, the file name, the extension name, and whether machine policy or user policy are enabled. For example:
```
> samba-tool gpo cse list
UniqueGUID : {5d159033-f613-4a60-90e7-87e0f6847fbf}
FileName : /root/policies/gp_test_ext.py
ProcessGroupPolicy : gp_test_ext
MachinePolicy : True
UserPolicy : False
```
To unregister a CSE, the `samba-tool gpo cse unregister` command is used, with the unique GUID of the CSE as the argument. For example, to unregister a CSE with the GUID {5d159033-f613-4a60-90e7-87e0f6847fbf}, the command would be:
```shell
samba-tool gpo cse unregister \
{5d159033-f613-4a60-90e7-87e0f6847fbf}
```
Note that the above commands are run on the Linux client machine (not from a Samba ADDC), and must be executed as the root user.
These commands are useful for registering custom CSEs, but can also be utilized to backport CSEs from newer versions of Samba. For example, if you'd like to utilize the Chrome policy which isn't available in the installed version of Samba, you can fetch the `python/samba/gp/gp_chromium_ext.py` file from the Samba master branch, then activate the policy via:
```shell
wget bit.ly/3H3vMba -O /root/policies/gp_chromium_ext.py
samba-tool gpo cse register /root/policies/gp_chromium_ext.py \
gp_chrome_ext --machine
samba-tool gpo cse register /root/policies/gp_chromium_ext.py \
gp_chromium_ext --machine
```
Notice that we ran the register command twice. This is because the `gp_chromium_ext.py` contains two CSEs, `gp_chrome_ext` and `gp_chromium_ext`.