github.com/thajeztah/cli@v0.0.0-20240223162942-dc6bfac81a8b/cli/command/trust/signer_remove.go (about)

     1  package trust
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"os"
     7  	"strings"
     8  
     9  	"github.com/docker/cli/cli"
    10  	"github.com/docker/cli/cli/command"
    11  	"github.com/docker/cli/cli/command/image"
    12  	"github.com/docker/cli/cli/trust"
    13  	"github.com/pkg/errors"
    14  	"github.com/spf13/cobra"
    15  	"github.com/theupdateframework/notary/client"
    16  	"github.com/theupdateframework/notary/tuf/data"
    17  )
    18  
    19  type signerRemoveOptions struct {
    20  	signer   string
    21  	repos    []string
    22  	forceYes bool
    23  }
    24  
    25  func newSignerRemoveCommand(dockerCli command.Cli) *cobra.Command {
    26  	options := signerRemoveOptions{}
    27  	cmd := &cobra.Command{
    28  		Use:   "remove [OPTIONS] NAME REPOSITORY [REPOSITORY...]",
    29  		Short: "Remove a signer",
    30  		Args:  cli.RequiresMinArgs(2),
    31  		RunE: func(cmd *cobra.Command, args []string) error {
    32  			options.signer = args[0]
    33  			options.repos = args[1:]
    34  			return removeSigner(cmd.Context(), dockerCli, options)
    35  		},
    36  	}
    37  	flags := cmd.Flags()
    38  	flags.BoolVarP(&options.forceYes, "force", "f", false, "Do not prompt for confirmation before removing the most recent signer")
    39  	return cmd
    40  }
    41  
    42  func removeSigner(ctx context.Context, dockerCLI command.Cli, options signerRemoveOptions) error {
    43  	var errRepos []string
    44  	for _, repo := range options.repos {
    45  		fmt.Fprintf(dockerCLI.Out(), "Removing signer \"%s\" from %s...\n", options.signer, repo)
    46  		if _, err := removeSingleSigner(ctx, dockerCLI, repo, options.signer, options.forceYes); err != nil {
    47  			fmt.Fprintln(dockerCLI.Err(), err.Error()+"\n")
    48  			errRepos = append(errRepos, repo)
    49  		}
    50  	}
    51  	if len(errRepos) > 0 {
    52  		return errors.Errorf("error removing signer from: %s", strings.Join(errRepos, ", "))
    53  	}
    54  	return nil
    55  }
    56  
    57  func isLastSignerForReleases(roleWithSig data.Role, allRoles []client.RoleWithSignatures) (bool, error) {
    58  	var releasesRoleWithSigs client.RoleWithSignatures
    59  	for _, role := range allRoles {
    60  		if role.Name == releasesRoleTUFName {
    61  			releasesRoleWithSigs = role
    62  			break
    63  		}
    64  	}
    65  	counter := len(releasesRoleWithSigs.Signatures)
    66  	if counter == 0 {
    67  		return false, errors.New("all signed tags are currently revoked, use docker trust sign to fix")
    68  	}
    69  	for _, signature := range releasesRoleWithSigs.Signatures {
    70  		for _, key := range roleWithSig.KeyIDs {
    71  			if signature.KeyID == key {
    72  				counter--
    73  			}
    74  		}
    75  	}
    76  	return counter < releasesRoleWithSigs.Threshold, nil
    77  }
    78  
    79  // removeSingleSigner attempts to remove a single signer and returns whether signer removal happened.
    80  // The signer not being removed doesn't necessarily raise an error e.g. user choosing "No" when prompted for confirmation.
    81  func removeSingleSigner(ctx context.Context, dockerCLI command.Cli, repoName, signerName string, forceYes bool) (bool, error) {
    82  	imgRefAndAuth, err := trust.GetImageReferencesAndAuth(ctx, image.AuthResolver(dockerCLI), repoName)
    83  	if err != nil {
    84  		return false, err
    85  	}
    86  
    87  	signerDelegation := data.RoleName("targets/" + signerName)
    88  	if signerDelegation == releasesRoleTUFName {
    89  		return false, errors.Errorf("releases is a reserved keyword and cannot be removed")
    90  	}
    91  	notaryRepo, err := dockerCLI.NotaryClient(imgRefAndAuth, trust.ActionsPushAndPull)
    92  	if err != nil {
    93  		return false, trust.NotaryError(imgRefAndAuth.Reference().Name(), err)
    94  	}
    95  	delegationRoles, err := notaryRepo.GetDelegationRoles()
    96  	if err != nil {
    97  		return false, errors.Wrapf(err, "error retrieving signers for %s", repoName)
    98  	}
    99  	var role data.Role
   100  	for _, delRole := range delegationRoles {
   101  		if delRole.Name == signerDelegation {
   102  			role = delRole
   103  			break
   104  		}
   105  	}
   106  	if role.Name == "" {
   107  		return false, errors.Errorf("no signer %s for repository %s", signerName, repoName)
   108  	}
   109  	allRoles, err := notaryRepo.ListRoles()
   110  	if err != nil {
   111  		return false, err
   112  	}
   113  	if ok, err := isLastSignerForReleases(role, allRoles); ok && !forceYes {
   114  		removeSigner := command.PromptForConfirmation(os.Stdin, dockerCLI.Out(), fmt.Sprintf("The signer \"%s\" signed the last released version of %s. "+
   115  			"Removing this signer will make %s unpullable. "+
   116  			"Are you sure you want to continue?",
   117  			signerName, repoName, repoName,
   118  		))
   119  
   120  		if !removeSigner {
   121  			fmt.Fprintf(dockerCLI.Out(), "\nAborting action.\n")
   122  			return false, nil
   123  		}
   124  	} else if err != nil {
   125  		return false, err
   126  	}
   127  	if err = notaryRepo.RemoveDelegationKeys(releasesRoleTUFName, role.KeyIDs); err != nil {
   128  		return false, err
   129  	}
   130  	if err = notaryRepo.RemoveDelegationRole(signerDelegation); err != nil {
   131  		return false, err
   132  	}
   133  
   134  	if err = notaryRepo.Publish(); err != nil {
   135  		return false, err
   136  	}
   137  
   138  	fmt.Fprintf(dockerCLI.Out(), "Successfully removed %s from %s\n\n", signerName, repoName)
   139  
   140  	return true, nil
   141  }