File: TrustedSignersCommand\TrustedSignersCommandRunner.cs
Web Access
Project: src\src\nuget-client\src\NuGet.Core\NuGet.Commands\NuGet.Commands.csproj (NuGet.Commands)
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

#nullable disable

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using NuGet.Common;
using NuGet.Configuration;
using NuGet.Packaging.Signing;
using static NuGet.Commands.TrustedSignersArgs;
using NuGet.Packaging;
using NuGet.Protocol;

namespace NuGet.Commands
{
    /// <summary>
    /// Command Runner used to run the business logic for nuget trusted-signers command
    /// </summary>
    public class TrustedSignersCommandRunner : ITrustedSignersCommandRunner
    {
        private const int SuccessCode = 0;

        private readonly ITrustedSignersProvider _trustedSignersProvider;
        private readonly IPackageSourceProvider _packageSourceProvider;

        public TrustedSignersCommandRunner(ITrustedSignersProvider trustedSignersProvider, IPackageSourceProvider packageSourceProvider)
        {
            _trustedSignersProvider = trustedSignersProvider ?? throw new ArgumentNullException(nameof(trustedSignersProvider));
            _packageSourceProvider = packageSourceProvider;
        }

        public async Task<int> ExecuteCommandAsync(TrustedSignersArgs trustedSignersArgs)
        {
            var logger = trustedSignersArgs.Logger ?? NullLogger.Instance;
            var actionsProvider = new TrustedSignerActionsProvider(_trustedSignersProvider, logger);

            switch (trustedSignersArgs.Action)
            {
                case TrustedSignersAction.List:
                    ValidateListArguments(trustedSignersArgs);

                    await ListAllTrustedSignersAsync(logger);

                    break;

                case TrustedSignersAction.Add:
                    ValidateNameExists(trustedSignersArgs.Name);

                    var isPackagePathProvided = !string.IsNullOrEmpty(trustedSignersArgs.PackagePath);
                    var isServiceIndexProvided = !string.IsNullOrEmpty(trustedSignersArgs.ServiceIndex);
                    var isFingerprintProvided = !string.IsNullOrEmpty(trustedSignersArgs.CertificateFingerprint);
                    var isAlgorithmProvided = !string.IsNullOrEmpty(trustedSignersArgs.FingerprintAlgorithm);

                    if (isPackagePathProvided)
                    {
                        if (isServiceIndexProvided || isFingerprintProvided || isAlgorithmProvided)
                        {
                            throw new CommandLineArgumentCombinationException(string.Format(CultureInfo.CurrentCulture, Strings.Error_CouldNotAdd, Strings.Error_InvalidCombinationOfArguments));
                        }

                        if (!(trustedSignersArgs.Repository ^ trustedSignersArgs.Author))
                        {
                            throw new CommandLineArgumentCombinationException(string.Format(CultureInfo.CurrentCulture, Strings.Error_NoSignatureTrustedForPackage, trustedSignersArgs.PackagePath));
                        }

                        var trustTarget = VerificationTarget.None;
                        if (trustedSignersArgs.Author)
                        {
                            if (!trustedSignersArgs.Repository && trustedSignersArgs.Owners != null && trustedSignersArgs.Owners.Any())
                            {
                                throw new CommandLineArgumentCombinationException(Strings.Error_CannotTrustOwnersForAuthor);
                            }

                            trustTarget |= VerificationTarget.Author;
                        }

                        if (trustedSignersArgs.Repository)
                        {
                            trustTarget |= VerificationTarget.Repository;
                        }

                        if (trustTarget == VerificationTarget.None)
                        {
                            trustTarget = VerificationTarget.Unknown;
                        }

                        var packagesToTrust = LocalFolderUtility.ResolvePackageFromPath(trustedSignersArgs.PackagePath);
                        LocalFolderUtility.EnsurePackageFileExists(trustedSignersArgs.PackagePath, packagesToTrust);

                        if (packagesToTrust.Count() > 1)
                        {
                            throw new ArgumentException(string.Format(CultureInfo.CurrentCulture,
                                Strings.Multiple_Nupkgs_Detected,
                                trustedSignersArgs.PackagePath));
                        }

                        foreach (var package in packagesToTrust)
                        {
                            using (var packageReader = new PackageArchiveReader(package))
                            {
                                await actionsProvider.AddTrustedSignerAsync(
                                    trustedSignersArgs.Name,
                                    packageReader,
                                    trustTarget,
                                    trustedSignersArgs.AllowUntrustedRoot,
                                    trustedSignersArgs.Owners,
                                    CancellationToken.None);
                            }
                        }

                        break;
                    }

                    if (isServiceIndexProvided)
                    {
                        if (isFingerprintProvided || isAlgorithmProvided || trustedSignersArgs.Author || trustedSignersArgs.Repository)
                        {
                            throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Strings.Error_CouldNotAdd, Strings.Error_InvalidCombinationOfArguments));
                        }

                        var serviceIndex = ValidateAndParseV3ServiceIndexUrl(trustedSignersArgs.ServiceIndex);

                        await actionsProvider.AddTrustedRepositoryAsync(
                            trustedSignersArgs.Name,
                            serviceIndex,
                            trustedSignersArgs.Owners,
                            CancellationToken.None);

                        break;
                    }

                    if (isFingerprintProvided)
                    {
                        if (trustedSignersArgs.Owners != null && trustedSignersArgs.Owners.Any())
                        {
                            throw new ArgumentException(Strings.Error_CannotTrustOwnersForAuthor);
                        }

                        var hashAlgorithm = ValidateAndParseFingerprintAlgorithm(trustedSignersArgs.FingerprintAlgorithm);

                        actionsProvider.AddOrUpdateTrustedSigner(
                            trustedSignersArgs.Name,
                            trustedSignersArgs.CertificateFingerprint,
                            hashAlgorithm,
                            trustedSignersArgs.AllowUntrustedRoot);

                        break;
                    }

                    if (isAlgorithmProvided || trustedSignersArgs.Author || trustedSignersArgs.Repository)
                    {
                        throw new CommandLineArgumentCombinationException(string.Format(CultureInfo.CurrentCulture, Strings.Error_CouldNotAdd, Strings.Error_InvalidCombinationOfArguments));
                    }

                    if (_packageSourceProvider == null)
                    {
                        throw new ArgumentException(Strings.Error_NoSourcesInformation);
                    }

                    var packageSource = _packageSourceProvider.GetPackageSourceByName(trustedSignersArgs.Name);
                    if (packageSource == null || string.IsNullOrEmpty(packageSource.Source))
                    {
                        throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Strings.Error_UnavailableSource, trustedSignersArgs.Name));
                    }

                    var sourceServiceIndex = ValidateAndParseV3ServiceIndexUrl(packageSource.Source);

                    await actionsProvider.AddTrustedRepositoryAsync(
                        trustedSignersArgs.Name,
                        sourceServiceIndex,
                        trustedSignersArgs.Owners,
                        CancellationToken.None);


                    break;

                case TrustedSignersAction.Remove:
                    ValidateRemoveArguments(trustedSignersArgs);

                    await RemoveTrustedSignerAsync(trustedSignersArgs.Name, logger);

                    break;

                case TrustedSignersAction.Sync:
                    ValidateSyncArguments(trustedSignersArgs);

                    await actionsProvider.SyncTrustedRepositoryAsync(trustedSignersArgs.Name, CancellationToken.None);

                    break;
            }

            return SuccessCode;
        }


        private async Task ListAllTrustedSignersAsync(ILogger logger)
        {
            var trustedSigners = _trustedSignersProvider.GetTrustedSigners();
            if (!trustedSigners.Any())
            {
                await logger.LogAsync(LogLevel.Minimal, Strings.NoTrustedSigners);
                return;
            }

            var trustedSignersLogs = new List<LogMessage>();

            await logger.LogAsync(LogLevel.Minimal, Strings.RegsiteredTrustedSigners);
            await logger.LogAsync(LogLevel.Minimal, Environment.NewLine);

            for (var i = 0; i < trustedSigners.Count; i++)
            {
                var item = trustedSigners[i];

                var trustedSignerBuilder = new StringBuilder();

                var index = $" {i + 1}.".PadRight(6);
                var defaultIndentation = string.Empty.PadRight(6);

                trustedSignerBuilder.AppendLine(index + string.Format(CultureInfo.CurrentCulture, Strings.TrustedSignerLogTitle, item.Name, item.ElementName));

                if (item is RepositoryItem repoItem)
                {
                    trustedSignerBuilder.AppendLine(defaultIndentation + string.Format(CultureInfo.CurrentCulture, Strings.TrustedSignerLogServiceIndex, repoItem.ServiceIndex));

                    if (repoItem.Owners != null && repoItem.Owners.Any())
                    {
                        trustedSignerBuilder.AppendLine(defaultIndentation + string.Format(CultureInfo.CurrentCulture, Strings.TrustedSignerLogOwners, string.Join("; ", repoItem.Owners)));
                    }
                }

                trustedSignerBuilder.AppendLine(defaultIndentation + Strings.TrustedSignerLogCertificates);

                foreach (var cert in item.Certificates)
                {
                    var extraIndentation = string.Empty.PadRight(2);

                    var summaryAllowUntrustedRoot = (cert.AllowUntrustedRoot) ? Strings.TrustedSignerLogCertificateSummaryAllowUntrustedRoot : Strings.TrustedSignerLogCertificateSummaryUnallowUntrustedRoot;
                    trustedSignerBuilder.AppendLine(defaultIndentation + extraIndentation + string.Format(CultureInfo.CurrentCulture, summaryAllowUntrustedRoot, cert.HashAlgorithm.ToString(), cert.Fingerprint));
                }

                trustedSignersLogs.Add(new LogMessage(LogLevel.Minimal, trustedSignerBuilder.ToString()));
            }

            await logger.LogMessagesAsync(trustedSignersLogs);
        }

        private async Task RemoveTrustedSignerAsync(string name, ILogger logger)
        {
            var trustedSigners = _trustedSignersProvider.GetTrustedSigners().Where(item => string.Equals(item.Name, name, StringComparison.OrdinalIgnoreCase));
            if (!trustedSigners.Any())
            {
                await logger.LogAsync(LogLevel.Minimal, string.Format(CultureInfo.CurrentCulture, Strings.NoTrustedSignersMatching, name));
                return;
            }

            _trustedSignersProvider.Remove(trustedSigners.ToList());

            await logger.LogAsync(LogLevel.Minimal, string.Format(CultureInfo.CurrentCulture, Strings.SuccessfullyRemovedTrustedSigner, name));
        }

        private void ValidateListArguments(TrustedSignersArgs args)
        {
            var isNameProvided = !string.IsNullOrEmpty(args.Name);
            var isPackagePathProvided = !string.IsNullOrEmpty(args.PackagePath);
            var isServiceIndexProvided = !string.IsNullOrEmpty(args.ServiceIndex);
            var isFingerprintProvided = !string.IsNullOrEmpty(args.CertificateFingerprint);
            var isAlgorithmProvided = !string.IsNullOrEmpty(args.FingerprintAlgorithm);
            var areOwnersProvided = args.Owners != null && args.Owners.Any();
            var isUntrustedRootProvided = args.AllowUntrustedRoot;
            var isAuthorProvided = args.Author;
            var isRepositoryProvided = args.Repository;

            if (isNameProvided || isPackagePathProvided || isServiceIndexProvided ||
                isFingerprintProvided || isAlgorithmProvided || areOwnersProvided ||
                isUntrustedRootProvided || isAuthorProvided || isRepositoryProvided)
            {
                throw new CommandLineArgumentCombinationException(string.Format(CultureInfo.CurrentCulture, Strings.Error_CouldNotList, Strings.Error_InvalidCombinationOfArguments));
            }
        }

        private void ValidateRemoveArguments(TrustedSignersArgs args)
        {
            ValidateNameExists(args.Name);

            var isPackagePathProvided = !string.IsNullOrEmpty(args.PackagePath);
            var isServiceIndexProvided = !string.IsNullOrEmpty(args.ServiceIndex);
            var isFingerprintProvided = !string.IsNullOrEmpty(args.CertificateFingerprint);
            var isAlgorithmProvided = !string.IsNullOrEmpty(args.FingerprintAlgorithm);
            var areOwnersProvided = args.Owners != null && args.Owners.Any();
            var isUntrustedRootProvided = args.AllowUntrustedRoot;
            var isAuthorProvided = args.Author;
            var isRepositoryProvided = args.Repository;

            if (isPackagePathProvided || isServiceIndexProvided ||
                isFingerprintProvided || isAlgorithmProvided || areOwnersProvided ||
                isUntrustedRootProvided || isAuthorProvided || isRepositoryProvided)
            {
                throw new CommandLineArgumentCombinationException(string.Format(CultureInfo.CurrentCulture, Strings.Error_CouldNotRemove, Strings.Error_InvalidCombinationOfArguments));
            }
        }

        private void ValidateSyncArguments(TrustedSignersArgs args)
        {
            ValidateNameExists(args.Name);

            var isPackagePathProvided = !string.IsNullOrEmpty(args.PackagePath);
            var isServiceIndexProvided = !string.IsNullOrEmpty(args.ServiceIndex);
            var isFingerprintProvided = !string.IsNullOrEmpty(args.CertificateFingerprint);
            var isAlgorithmProvided = !string.IsNullOrEmpty(args.FingerprintAlgorithm);
            var areOwnersProvided = args.Owners != null && args.Owners.Any();
            var isUntrustedRootProvided = args.AllowUntrustedRoot;
            var isAuthorProvided = args.Author;
            var isRepositoryProvided = args.Repository;

            if (isPackagePathProvided || isServiceIndexProvided ||
                isFingerprintProvided || isAlgorithmProvided || areOwnersProvided ||
                isUntrustedRootProvided || isAuthorProvided || isRepositoryProvided)
            {
                throw new CommandLineArgumentCombinationException(string.Format(CultureInfo.CurrentCulture, Strings.Error_CouldNotSync, Strings.Error_InvalidCombinationOfArguments));
            }
        }

        private void ValidateNameExists(string name)
        {
            if (string.IsNullOrEmpty(name))
            {
                throw new CommandLineArgumentCombinationException(string.Format(CultureInfo.CurrentCulture, Strings.Error_PropertyCannotBeNullOrEmpty, nameof(name)));
            }
        }

        private HashAlgorithmName ValidateAndParseFingerprintAlgorithm(string algorithm)
        {
            if (string.IsNullOrEmpty(algorithm))
            {
                return HashAlgorithmName.SHA256;
            }

            var hashAlgorithm = CryptoHashUtility.GetHashAlgorithmName(algorithm);

            if (hashAlgorithm == HashAlgorithmName.Unknown || !SigningSpecifications.V1.AllowedHashAlgorithms.Contains(hashAlgorithm))
            {
                throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Strings.Error_NotSupportedHashAlgorithm, algorithm));
            }

            return hashAlgorithm;
        }

        private Uri ValidateAndParseV3ServiceIndexUrl(string serviceIndex)
        {
            var validUri = Uri.TryCreate(serviceIndex, UriKind.Absolute, out var serviceIndexUri);
            if (!validUri || !string.Equals(serviceIndexUri.Scheme, "https", StringComparison.OrdinalIgnoreCase))
            {
                throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Strings.Error_ServiceIndexShouldBeHttps, serviceIndex));
            }

            return serviceIndexUri;
        }
    }
}