# 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(),
    }
