1 # delegation management
3 # Copyright Matthieu Patou mat@samba.org 2010
4 # Copyright Stefan Metzmacher metze@samba.org 2011
5 # Copyright Bjoern Baumbach bb@sernet.de 2011
7 # This program is free software; you can redistribute it and/or modify
8 # it under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 3 of the License, or
10 # (at your option) any later version.
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
17 # You should have received a copy of the GNU General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
21 import samba
.getopt
as options
23 from samba
import provision
24 from samba
import dsdb
25 from samba
.samdb
import SamDB
26 from samba
.auth
import system_session
27 from samba
.dcerpc
import security
28 from samba
.ndr
import ndr_pack
, ndr_unpack
29 from samba
.netcmd
.common
import _get_user_realm_domain
30 from samba
.netcmd
import (
38 class cmd_delegation_show(Command
):
39 """Show the delegation setting of an account."""
41 synopsis
= "%prog <accountname> [options]"
43 takes_optiongroups
= {
44 "sambaopts": options
.SambaOptions
,
45 "credopts": options
.CredentialsOptions
,
46 "versionopts": options
.VersionOptions
,
50 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
51 metavar
="URL", dest
="H"),
54 takes_args
= ["accountname"]
56 def show_security_descriptor(self
, sam
, security_descriptor
):
57 dacl
= security_descriptor
.dacl
58 desc_type
= security_descriptor
.type
60 warning_info
= ('Security Descriptor of attribute '
61 'msDS-AllowedToActOnBehalfOfOtherIdentity')
63 if dacl
is None or not desc_type
& security
.SEC_DESC_DACL_PRESENT
:
64 self
.errf
.write(f
'Warning: DACL not present in {warning_info}!\n')
67 if not desc_type
& security
.SEC_DESC_SELF_RELATIVE
:
68 self
.errf
.write(f
'Warning: DACL in {warning_info} lacks '
69 f
'SELF_RELATIVE flag!\n')
77 # Convert the trustee SID into a DN if we can.
79 res
= sam
.search(f
'<SID={trustee}>',
81 except ldb
.LdbError
as err
:
83 if num
!= ldb
.ERR_NO_SUCH_OBJECT
:
91 if (ace
.type == security
.SEC_ACE_TYPE_ACCESS_DENIED
92 or ace
.type == security
.SEC_ACE_TYPE_ACCESS_DENIED_OBJECT
):
93 self
.errf
.write(f
'Warning: ACE in {warning_info} denies '
94 f
'access for trustee {trustee}!\n')
95 # Ignore the ACE if it denies access
97 elif (ace
.type != security
.SEC_ACE_TYPE_ACCESS_ALLOWED
98 and ace
.type != security
.SEC_ACE_TYPE_ACCESS_ALLOWED_OBJECT
):
99 # Ignore the ACE if it doesn't explicitly allow access
102 inherit_only
= ace
.flags
& security
.SEC_ACE_FLAG_INHERIT_ONLY
103 object_inherit
= ace
.flags
& security
.SEC_ACE_FLAG_OBJECT_INHERIT
104 container_inherit
= (
105 ace
.flags
& security
.SEC_ACE_FLAG_CONTAINER_INHERIT
)
106 inherited_ace
= ace
.flags
& security
.SEC_ACE_FLAG_INHERITED_ACE
108 if inherit_only
and not object_inherit
and not container_inherit
:
109 # Ignore the ACE if it is propagated only to child objects, but
110 # neither of the object and container inherit flags are set.
113 if container_inherit
:
114 self
.errf
.write(f
'Warning: ACE for trustee {trustee} has '
115 f
'unexpected CONTAINER_INHERIT flag set in '
116 f
'{warning_info}!\n')
120 self
.errf
.write(f
'Warning: ACE for trustee {trustee} has '
121 f
'unexpected INHERITED_ACE flag set in '
122 f
'{warning_info}!\n')
125 if not ace
.access_mask
:
126 # Ignore the ACE if it doesn't grant any permissions.
131 self
.outf
.write(' Principals that may delegate to this '
135 self
.outf
.write(f
'msDS-AllowedToActOnBehalfOfOtherIdentity: '
138 def run(self
, accountname
, H
=None, credopts
=None, sambaopts
=None, versionopts
=None):
139 lp
= sambaopts
.get_loadparm()
140 creds
= credopts
.get_credentials(lp
)
143 paths
= provision
.provision_paths_from_lp(lp
, lp
.get("realm"))
148 sam
= SamDB(path
, session_info
=system_session(),
149 credentials
=creds
, lp
=lp
)
150 # TODO once I understand how, use the domain info to naildown
151 # to the correct domain
152 (cleanedaccount
, realm
, domain
) = _get_user_realm_domain(accountname
,
155 res
= sam
.search(expression
="sAMAccountName=%s" %
156 ldb
.binary_encode(cleanedaccount
),
157 scope
=ldb
.SCOPE_SUBTREE
,
158 attrs
=["userAccountControl", "msDS-AllowedToDelegateTo",
159 "msDS-AllowedToActOnBehalfOfOtherIdentity"])
161 raise CommandError("Unable to find account name '%s'" % accountname
)
163 raise CommandError("Found multiple accounts.")
165 uac
= int(res
[0].get("userAccountControl")[0])
166 allowed
= res
[0].get("msDS-AllowedToDelegateTo")
167 allowed_from
= res
[0].get("msDS-AllowedToActOnBehalfOfOtherIdentity", idx
=0)
169 self
.outf
.write("Account-DN: %s\n" % str(res
[0].dn
))
170 self
.outf
.write("UF_TRUSTED_FOR_DELEGATION: %s\n"
171 % bool(uac
& dsdb
.UF_TRUSTED_FOR_DELEGATION
))
172 self
.outf
.write("UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION: %s\n" %
173 bool(uac
& dsdb
.UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION
))
176 self
.outf
.write(" Services this account may delegate to:\n")
178 self
.outf
.write("msDS-AllowedToDelegateTo: %s\n" % a
)
179 if allowed_from
is not None:
181 security_descriptor
= ndr_unpack(security
.descriptor
, allowed_from
)
183 self
.errf
.write("Warning: Security Descriptor of attribute "
184 "msDS-AllowedToActOnBehalfOfOtherIdentity "
185 "could not be unmarshalled!\n")
187 self
.show_security_descriptor(sam
, security_descriptor
)
190 class cmd_delegation_for_any_service(Command
):
191 """Set/unset UF_TRUSTED_FOR_DELEGATION for an account."""
193 synopsis
= "%prog <accountname> [(on|off)] [options]"
195 takes_optiongroups
= {
196 "sambaopts": options
.SambaOptions
,
197 "credopts": options
.CredentialsOptions
,
198 "versionopts": options
.VersionOptions
,
202 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
203 metavar
="URL", dest
="H"),
206 takes_args
= ["accountname", "onoff"]
208 def run(self
, accountname
, onoff
, H
=None, credopts
=None, sambaopts
=None,
216 raise CommandError("invalid argument: '%s' (choose from 'on', 'off')" % onoff
)
218 lp
= sambaopts
.get_loadparm()
219 creds
= credopts
.get_credentials(lp
)
220 paths
= provision
.provision_paths_from_lp(lp
, lp
.get("realm"))
226 sam
= SamDB(path
, session_info
=system_session(),
227 credentials
=creds
, lp
=lp
)
228 # TODO once I understand how, use the domain info to naildown
229 # to the correct domain
230 (cleanedaccount
, realm
, domain
) = _get_user_realm_domain(accountname
,
233 search_filter
= "sAMAccountName=%s" % ldb
.binary_encode(cleanedaccount
)
234 flag
= dsdb
.UF_TRUSTED_FOR_DELEGATION
236 sam
.toggle_userAccountFlags(search_filter
, flag
,
237 flags_str
="Trusted-for-Delegation",
239 except Exception as err
:
240 raise CommandError(err
)
243 class cmd_delegation_for_any_protocol(Command
):
244 """Set/unset UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION (S4U2Proxy) for an account."""
246 synopsis
= "%prog <accountname> [(on|off)] [options]"
248 takes_optiongroups
= {
249 "sambaopts": options
.SambaOptions
,
250 "credopts": options
.CredentialsOptions
,
251 "versionopts": options
.VersionOptions
,
255 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
256 metavar
="URL", dest
="H"),
259 takes_args
= ["accountname", "onoff"]
261 def run(self
, accountname
, onoff
, H
=None, credopts
=None, sambaopts
=None,
270 raise CommandError("invalid argument: '%s' (choose from 'on', 'off')" % onoff
)
272 lp
= sambaopts
.get_loadparm()
273 creds
= credopts
.get_credentials(lp
, fallback_machine
=True)
274 paths
= provision
.provision_paths_from_lp(lp
, lp
.get("realm"))
280 sam
= SamDB(path
, session_info
=system_session(),
281 credentials
=creds
, lp
=lp
)
282 # TODO once I understand how, use the domain info to naildown
283 # to the correct domain
284 (cleanedaccount
, realm
, domain
) = _get_user_realm_domain(accountname
,
287 search_filter
= "sAMAccountName=%s" % ldb
.binary_encode(cleanedaccount
)
288 flag
= dsdb
.UF_TRUSTED_TO_AUTHENTICATE_FOR_DELEGATION
290 sam
.toggle_userAccountFlags(search_filter
, flag
,
291 flags_str
="Trusted-to-Authenticate-for-Delegation",
293 except Exception as err
:
294 raise CommandError(err
)
297 class cmd_delegation_add_service(Command
):
298 """Add a service principal to msDS-AllowedToDelegateTo so that an account may delegate to it."""
300 synopsis
= "%prog <accountname> <principal> [options]"
302 takes_optiongroups
= {
303 "sambaopts": options
.SambaOptions
,
304 "credopts": options
.CredentialsOptions
,
305 "versionopts": options
.VersionOptions
,
309 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
310 metavar
="URL", dest
="H"),
313 takes_args
= ["accountname", "principal"]
315 def run(self
, accountname
, principal
, H
=None, credopts
=None, sambaopts
=None,
318 lp
= sambaopts
.get_loadparm()
319 creds
= credopts
.get_credentials(lp
)
320 paths
= provision
.provision_paths_from_lp(lp
, lp
.get("realm"))
326 sam
= SamDB(path
, session_info
=system_session(),
327 credentials
=creds
, lp
=lp
)
328 # TODO once I understand how, use the domain info to naildown
329 # to the correct domain
330 (cleanedaccount
, realm
, domain
) = _get_user_realm_domain(accountname
,
333 res
= sam
.search(expression
="sAMAccountName=%s" %
334 ldb
.binary_encode(cleanedaccount
),
335 scope
=ldb
.SCOPE_SUBTREE
,
336 attrs
=["msDS-AllowedToDelegateTo"])
338 raise CommandError("Unable to find account name '%s'" % accountname
)
340 raise CommandError("Found multiple accounts.")
344 msg
["msDS-AllowedToDelegateTo"] = ldb
.MessageElement([principal
],
346 "msDS-AllowedToDelegateTo")
349 except Exception as err
:
350 raise CommandError(err
)
353 class cmd_delegation_del_service(Command
):
354 """Delete a service principal from msDS-AllowedToDelegateTo so that an account may no longer delegate to it."""
356 synopsis
= "%prog <accountname> <principal> [options]"
358 takes_optiongroups
= {
359 "sambaopts": options
.SambaOptions
,
360 "credopts": options
.CredentialsOptions
,
361 "versionopts": options
.VersionOptions
,
365 Option("-H", "--URL", help="LDB URL for database or target server", type=str,
366 metavar
="URL", dest
="H"),
369 takes_args
= ["accountname", "principal"]
371 def run(self
, accountname
, principal
, H
=None, credopts
=None, sambaopts
=None,
374 lp
= sambaopts
.get_loadparm()
375 creds
= credopts
.get_credentials(lp
)
376 paths
= provision
.provision_paths_from_lp(lp
, lp
.get("realm"))
382 sam
= SamDB(path
, session_info
=system_session(),
383 credentials
=creds
, lp
=lp
)
384 # TODO once I understand how, use the domain info to naildown
385 # to the correct domain
386 (cleanedaccount
, realm
, domain
) = _get_user_realm_domain(accountname
,
389 res
= sam
.search(expression
="sAMAccountName=%s" %
390 ldb
.binary_encode(cleanedaccount
),
391 scope
=ldb
.SCOPE_SUBTREE
,
392 attrs
=["msDS-AllowedToDelegateTo"])
394 raise CommandError("Unable to find account name '%s'" % accountname
)
396 raise CommandError("Found multiple accounts.")
400 msg
["msDS-AllowedToDelegateTo"] = ldb
.MessageElement([principal
],
402 "msDS-AllowedToDelegateTo")
405 except Exception as err
:
406 raise CommandError(err
)
409 class cmd_delegation_add_principal(Command
):
410 """Add a principal to msDS-AllowedToActOnBehalfOfOtherIdentity that may delegate to an account."""
412 synopsis
= "%prog <accountname> <principal> [options]"
414 takes_optiongroups
= {
415 "sambaopts": options
.SambaOptions
,
416 "credopts": options
.CredentialsOptions
,
417 "versionopts": options
.VersionOptions
,
421 Option("-H", "--URL", help="LDB URL for database or target server",
422 type=str, metavar
="URL", dest
="H"),
425 takes_args
= ["accountname", "principal"]
427 def run(self
, accountname
, principal
, H
=None, credopts
=None, sambaopts
=None,
430 lp
= sambaopts
.get_loadparm()
431 creds
= credopts
.get_credentials(lp
)
432 paths
= provision
.provision_paths_from_lp(lp
, lp
.get("realm"))
438 sam
= SamDB(path
, session_info
=system_session(),
439 credentials
=creds
, lp
=lp
)
440 # TODO once I understand how, use the domain info to naildown
441 # to the correct domain
442 cleanedaccount
, _
, _
= _get_user_realm_domain(accountname
, sam
)
444 account_res
= sam
.search(
445 expression
="sAMAccountName=%s" %
446 ldb
.binary_encode(cleanedaccount
),
447 scope
=ldb
.SCOPE_SUBTREE
,
448 attrs
=["msDS-AllowedToActOnBehalfOfOtherIdentity"])
449 if len(account_res
) == 0:
450 raise CommandError(f
"Unable to find account name '{accountname}'")
451 elif len(account_res
) != 1:
452 raise CommandError("Found multiple accounts.")
454 data
= account_res
[0].get(
455 "msDS-AllowedToActOnBehalfOfOtherIdentity", idx
=0)
457 # Create the security descriptor if it is not present.
458 owner_sid
= security
.dom_sid(security
.SID_BUILTIN_ADMINISTRATORS
)
460 security_desc
= security
.descriptor()
461 security_desc
.revision
= security
.SD_REVISION
462 security_desc
.type = (security
.SEC_DESC_DACL_PRESENT |
463 security
.SEC_DESC_SELF_RELATIVE
)
464 security_desc
.owner_sid
= owner_sid
469 security_desc
= ndr_unpack(security
.descriptor
, data
)
471 raise CommandError(f
"Security Descriptor of attribute "
472 f
"msDS-AllowedToActOnBehalfOfOtherIdentity "
473 f
"for account '{accountname}' could not be "
476 dacl
= security_desc
.dacl
479 # Create the DACL if it is not present.
480 dacl
= security
.acl()
481 dacl
.revision
= security
.SECURITY_ACL_REVISION_ADS
484 # TODO once I understand how, use the domain info to naildown
485 # to the correct domain
486 cleanedprinc
, _
, _
= _get_user_realm_domain(principal
, sam
)
488 princ_res
= sam
.search(expression
="sAMAccountName=%s" %
489 ldb
.binary_encode(cleanedprinc
),
490 scope
=ldb
.SCOPE_SUBTREE
,
492 if len(princ_res
) == 0:
493 raise CommandError(f
"Unable to find principal name '{principal}'")
494 elif len(princ_res
) != 1:
495 raise CommandError("Found multiple accounts.")
497 princ_sid
= security
.dom_sid(
498 sam
.schema_format_value(
500 princ_res
[0].get("objectSID", idx
=0)).decode("utf-8"))
504 # Check that there is no existing ACE for this principal.
505 if any(ace
.trustee
== princ_sid
for ace
in aces
):
507 f
"ACE for principal '{principal}' already present in Security "
508 f
"Descriptor of attribute "
509 f
"msDS-AllowedToActOnBehalfOfOtherIdentity for account "
512 # Create the new ACE.
514 ace
.type = security
.SEC_ACE_TYPE_ACCESS_ALLOWED
516 ace
.access_mask
= security
.SEC_ADS_GENERIC_ALL
517 ace
.trustee
= princ_sid
524 security_desc
.dacl
= dacl
526 new_data
= ndr_pack(security_desc
)
528 # Set the new security descriptor. First, delete the original value to
529 # detect a race condition if someone else updates the attribute at the
532 msg
.dn
= account_res
[0].dn
534 msg
["0"] = ldb
.MessageElement(
535 data
, ldb
.FLAG_MOD_DELETE
,
536 "msDS-AllowedToActOnBehalfOfOtherIdentity")
537 msg
["1"] = ldb
.MessageElement(
538 new_data
, ldb
.FLAG_MOD_ADD
,
539 "msDS-AllowedToActOnBehalfOfOtherIdentity")
542 except ldb
.LdbError
as err
:
544 if num
== ldb
.ERR_NO_SUCH_ATTRIBUTE
:
546 f
"Refused to update attribute "
547 f
"msDS-AllowedToActOnBehalfOfOtherIdentity for account "
548 f
"'{accountname}': a conflicting attribute update "
549 f
"occurred simultaneously.")
551 raise CommandError(err
)
554 class cmd_delegation_del_principal(Command
):
555 """Delete a principal from msDS-AllowedToActOnBehalfOfOtherIdentity that may no longer delegate to an account."""
557 synopsis
= "%prog <accountname> <principal> [options]"
559 takes_optiongroups
= {
560 "sambaopts": options
.SambaOptions
,
561 "credopts": options
.CredentialsOptions
,
562 "versionopts": options
.VersionOptions
,
566 Option("-H", "--URL", help="LDB URL for database or target server",
567 type=str, metavar
="URL", dest
="H"),
570 takes_args
= ["accountname", "principal"]
572 def run(self
, accountname
, principal
, H
=None, credopts
=None, sambaopts
=None,
575 lp
= sambaopts
.get_loadparm()
576 creds
= credopts
.get_credentials(lp
)
577 paths
= provision
.provision_paths_from_lp(lp
, lp
.get("realm"))
583 sam
= SamDB(path
, session_info
=system_session(),
584 credentials
=creds
, lp
=lp
)
585 # TODO once I understand how, use the domain info to naildown
586 # to the correct domain
587 cleanedaccount
, _
, _
= _get_user_realm_domain(accountname
, sam
)
589 account_res
= sam
.search(
590 expression
="sAMAccountName=%s" %
591 ldb
.binary_encode(cleanedaccount
),
592 scope
=ldb
.SCOPE_SUBTREE
,
593 attrs
=["msDS-AllowedToActOnBehalfOfOtherIdentity"])
594 if len(account_res
) == 0:
595 raise CommandError("Unable to find account name '%s'" % accountname
)
596 elif len(account_res
) != 1:
597 raise CommandError("Found multiple accounts.")
599 data
= account_res
[0].get(
600 "msDS-AllowedToActOnBehalfOfOtherIdentity", idx
=0)
602 raise CommandError(f
"Attribute "
603 f
"msDS-AllowedToActOnBehalfOfOtherIdentity for "
604 f
"account '{accountname}' not present!")
607 security_desc
= ndr_unpack(security
.descriptor
, data
)
609 raise CommandError(f
"Security Descriptor of attribute "
610 f
"msDS-AllowedToActOnBehalfOfOtherIdentity for "
611 f
"account '{accountname}' could not be "
614 dacl
= security_desc
.dacl
616 raise CommandError(f
"DACL not present on Security Descriptor of "
618 f
"msDS-AllowedToActOnBehalfOfOtherIdentity for "
619 f
"account '{accountname}'!")
621 # TODO once I understand how, use the domain info to naildown
622 # to the correct domain
623 cleanedprinc
, _
, _
= _get_user_realm_domain(principal
, sam
)
625 princ_res
= sam
.search(expression
="sAMAccountName=%s" %
626 ldb
.binary_encode(cleanedprinc
),
627 scope
=ldb
.SCOPE_SUBTREE
,
629 if len(princ_res
) == 0:
630 raise CommandError(f
"Unable to find principal name '{principal}'")
631 elif len(princ_res
) != 1:
632 raise CommandError("Found multiple accounts.")
634 princ_sid
= security
.dom_sid(
635 sam
.schema_format_value(
637 princ_res
[0].get("objectSID", idx
=0)).decode("utf-8"))
641 # Remove any ACEs relating to the specified principal.
642 aces
= [ace
for ace
in old_aces
if ace
.trustee
!= princ_sid
]
644 # Raise an error if we didn't find any.
645 if len(aces
) == len(old_aces
):
646 raise CommandError(f
"Unable to find ACE for principal "
647 f
"'{principal}' in Security Descriptor of "
649 f
"msDS-AllowedToActOnBehalfOfOtherIdentity for "
650 f
"account '{accountname}'.")
652 dacl
.num_aces
= len(aces
)
655 security_desc
.dacl
= dacl
657 new_data
= ndr_pack(security_desc
)
659 # Set the new security descriptor. First, delete the original value to
660 # detect a race condition if someone else updates the attribute at the
663 msg
.dn
= account_res
[0].dn
664 msg
["0"] = ldb
.MessageElement(
665 data
, ldb
.FLAG_MOD_DELETE
,
666 "msDS-AllowedToActOnBehalfOfOtherIdentity")
667 msg
["1"] = ldb
.MessageElement(
668 new_data
, ldb
.FLAG_MOD_ADD
,
669 "msDS-AllowedToActOnBehalfOfOtherIdentity")
672 except ldb
.LdbError
as err
:
674 if num
== ldb
.ERR_NO_SUCH_ATTRIBUTE
:
676 f
"Refused to update attribute "
677 f
"msDS-AllowedToActOnBehalfOfOtherIdentity for account "
678 f
"'{accountname}': a conflicting attribute update "
679 f
"occurred simultaneously.")
681 raise CommandError(err
)
684 class cmd_delegation(SuperCommand
):
685 """Delegation management."""
688 subcommands
["show"] = cmd_delegation_show()
689 subcommands
["for-any-service"] = cmd_delegation_for_any_service()
690 subcommands
["for-any-protocol"] = cmd_delegation_for_any_protocol()
691 subcommands
["add-service"] = cmd_delegation_add_service()
692 subcommands
["del-service"] = cmd_delegation_del_service()
693 subcommands
["add-principal"] = cmd_delegation_add_principal()
694 subcommands
["del-principal"] = cmd_delegation_del_principal()