From 8ed39fa33f9dd917aae291ce6aad222d95654ec1 Mon Sep 17 00:00:00 2001 From: Douglas Bagnall Date: Sun, 17 Aug 2025 09:57:55 +0000 Subject: [PATCH] samba-tool: copy user_keytrust to computer keytrust This is exactly a copy of user/keytrust.py to computer_keytrust.py with a title-case-preserving `s/user/computer/`. It works. The Computer model differs from the User model in that it appends a '$' to the end of account names if it senses the lack, otherwise these commands are using the same code paths. Signed-off-by: Douglas Bagnall Reviewed-by: Gary Lockyer --- python/samba/netcmd/computer.py | 3 + python/samba/netcmd/computer_keytrust.py | 223 +++++++++++++++++++++++ 2 files changed, 226 insertions(+) create mode 100644 python/samba/netcmd/computer_keytrust.py diff --git a/python/samba/netcmd/computer.py b/python/samba/netcmd/computer.py index bb0b6ec0335..cd5389cf8ec 100644 --- a/python/samba/netcmd/computer.py +++ b/python/samba/netcmd/computer.py @@ -36,6 +36,8 @@ from samba.samdb import SamDB from samba.common import get_bytes from subprocess import check_call, CalledProcessError from . import common +from .computer_keytrust import cmd_computer_keytrust + from samba import ( dsdb, @@ -724,3 +726,4 @@ class cmd_computer(SuperCommand): subcommands["list"] = cmd_computer_list() subcommands["show"] = cmd_computer_show() subcommands["move"] = cmd_computer_move() + subcommands["keytrust"] = cmd_computer_keytrust() diff --git a/python/samba/netcmd/computer_keytrust.py b/python/samba/netcmd/computer_keytrust.py new file mode 100644 index 00000000000..e0e52bf497f --- /dev/null +++ b/python/samba/netcmd/computer_keytrust.py @@ -0,0 +1,223 @@ +# samba-tool commands to manager Key Credential Links on a computer +# +# Copyright © Douglas Bagnall 2025 +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import ldb +import samba.getopt as options +from samba.domain.models import Computer +from samba.domain.models.exceptions import ModelError +from samba.netcmd import Command, CommandError, Option, SuperCommand +from samba.netcmd import exception_to_command_error +from samba.key_credential_link import (create_key_credential_link, + kcl_in_list, + filter_kcl_list) + + +class cmd_computer_keycredentiallink_add(Command): + """Add a key-credential-link.""" + + synopsis = "%prog [options] " + + takes_args = ["computername", "pubkey"] + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "credopts": options.CredentialsOptions, + "hostopts": options.HostOptions, + } + + takes_options = [ + Option("--link-target", metavar="DN", + help="link to this DN (default: this computer's DN)"), + Option("--encoding", default='auto', choices=('pem', 'der', 'auto'), + help="Key format (optional)"), + Option("--force", default=False, action='store_true', + help="proceed with operations that seems ill-fated"), + ] + + @exception_to_command_error(ValueError, ModelError, FileNotFoundError) + def run(self, computername, pubkey, + hostopts=None, sambaopts=None, credopts=None, + link_target=None, encoding='auto', force=False): + + samdb = self.ldb_connect(hostopts, sambaopts, credopts) + computer = Computer.find(samdb, computername) + + if link_target is None: + link_target = computer.dn + + with open(pubkey, 'rb') as f: + data = f.read() + + try: + link = create_key_credential_link(samdb, + link_target, + data, + encoding=encoding, + force=force) + except ldb.LdbError as e: + # with --force, we will end up with CONSTRAINT_VIOLATION + # at computer.save(), rather than NO_SUCH_OBJECT now. + if e.args[0] == ldb.ERR_NO_SUCH_OBJECT: + raise CommandError(f"Link target '{link_target}' does not exist") + raise + + if not force and kcl_in_list(link, computer.key_credential_link): + # It is not allowed to have duplicate linked attributes, + # which in the case of key credential links means having + # the same key blob and the same DN target. + # + # It is still possible to have the same key material and + # DN target if other fields (e.g. creation date) in the + # blob differ. The creation date is set with one second + # resolution in create_key_credential_link() just above, + # which puts us in the awkward position of creating a race + # if people are running samba-tool in a script. + # + # While the uniqueness invariant is a feature of AD/DSDB, + # not of key credential links, duplicates are not going to + # be useful, so we try to avoid this by checking first + # unless --force is used. + # + # if --force is used to add a key for the second time in + # the same second, computer.save() below will raise an + # ERR_ATTRIBUTE_OR_VALUE_EXISTS LdbError. + raise CommandError(f"Computer {computername} " + "already has this key credential link") + + computer.key_credential_link.append(link) + computer.save(samdb) + + +class cmd_computer_keycredentiallink_delete(Command): + """Delete a key-credential-link.""" + + synopsis = "%prog [options]" + + takes_args = ["computername"] + + takes_options = [ + Option("--link-target", metavar="DN", + help="Delete this key credential link (a DN)"), + Option("--fingerprint", metavar="HH:HH:..", + help="Delete the key credential link with this key fingerprint"), + Option("--all", action='store_true', + help="Delete all key credential links"), + Option("-n", "--dry-run", action='store_true', + help="Do nothing but print what would happen"), + ] + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "credopts": options.CredentialsOptions, + "hostopts": options.HostOptions, + } + + @exception_to_command_error(ValueError, ModelError) + def run(self, computername, hostopts=None, sambaopts=None, credopts=None, + link_target=None, fingerprint=None, all=False, dry_run=False): + + samdb = self.ldb_connect(hostopts, sambaopts, credopts) + computer = Computer.find(samdb, computername) + + keycredlinks = computer.key_credential_link + + if all: + goners = keycredlinks + else: + goners = filter_kcl_list(samdb, + keycredlinks, + link_target=link_target, + fingerprint=fingerprint) + + keepers = [x for x in keycredlinks if x not in goners] + nk = len(keepers) + + if dry_run: + self.message("Without --dry-run, this would happen:") + if not goners: + self.message("NO key credential links are deleted") + for x in goners: + self.message(f"DELETE {x} (fingerprint {x.fingerprint()})") + self.message('') + for x in keepers: + self.message(f"KEEP {x} (fingerprint {x.fingerprint()})") + + self.message(f"{computername} would now have {nk} key credential link" + f"{'' if nk == 1 else 's'}") + return + + if not goners: + # fail without traceback if the filter matches no links + raise CommandError("no key credential links deleted") + + computer.key_credential_link = keepers + computer.save(samdb) + + for x in goners: + self.message(f"Deleted {x} (fingerprint {x.fingerprint()})") + self.message('') + for x in keepers: + self.message(f"Keeping {x} (fingerprint {x.fingerprint()})") + + self.message(f"{computername} now has {nk} key credential link" + f"{'' if nk == 1 else 's'}") + + +class cmd_computer_keycredentiallink_view(Command): + """View a computer's key credential links.""" + synopsis = "%prog [options]" + + takes_args = ["computername"] + + takes_options = [ + Option("-v", "--verbose", help="Be verbose", action="store_true"), + ] + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "credopts": options.CredentialsOptions, + "hostopts": options.HostOptions, + } + + @exception_to_command_error(ValueError, ModelError) + def run(self, computername, hostopts=None, sambaopts=None, credopts=None, + verbose=False): + + samdb = self.ldb_connect(hostopts, sambaopts, credopts) + computer = Computer.find(samdb, computername) + + if verbose: + verbosity = 3 + else: + verbosity = 2 + + n = len(computer.key_credential_link) + self.message(f"{computername} has {n} key credential link" + f"{'' if n == 1 else 's'}\n") + + for kcl in computer.key_credential_link: + self.message(kcl.description(verbosity), '') + + +class cmd_computer_keytrust(SuperCommand): + """Manage key-credential links on a computer.""" + + subcommands = { + "add": cmd_computer_keycredentiallink_add(), + "delete": cmd_computer_keycredentiallink_delete(), + "view": cmd_computer_keycredentiallink_view(), + } -- 2.47.3