mirror of
https://github.com/samba-team/samba.git
synced 2025-12-12 12:23:50 +03:00
Signed-off-by: Jennifer Sutton <jennifersutton@catalyst.net.nz> Reviewed-by: Douglas Bagnall <douglas.bagnall@catalyst.net.nz> Reviewed-by: Gary Lockyer <gary@catalyst.net.nz>
224 lines
8.4 KiB
Python
224 lines
8.4 KiB
Python
# samba-tool commands to manage Key Credential Links on a computer
|
|
#
|
|
# Copyright © Douglas Bagnall <dbagnall@samba.org> 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 <http://www.gnu.org/licenses/>.
|
|
|
|
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 <computername> [options] <pubkey>"
|
|
|
|
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 <computername> [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 <computername> [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(),
|
|
}
|