private async Task PushPublicKeyToMetadataAsync(
            InstanceLocator instance,
            bool useInstanceKeySet,
            ManagedMetadataAuthorizedKey metadataKey,
            CancellationToken token)
        {
            try
            {
                if (useInstanceKeySet)
                {
                    await this.computeEngineAdapter.UpdateMetadataAsync(
                        instance,
                        metadata => MergeKeyIntoMetadata(metadata, metadataKey),
                        token)
                    .ConfigureAwait(false);
                }
                else
                {
                    await this.computeEngineAdapter.UpdateCommonInstanceMetadataAsync(
                        instance.ProjectId,
                        metadata => MergeKeyIntoMetadata(metadata, metadataKey),
                        token)
                    .ConfigureAwait(false);
                }
            }
            catch (GoogleApiException e) when(e.Error == null || e.Error.Code == 403)
            {
                ApplicationTraceSources.Default.TraceVerbose(
                    "Setting request payload metadata failed with 403: {0} ({1})",
                    e.Message,
                    e.Error?.Errors.EnsureNotNull().Select(er => er.Reason).FirstOrDefault());

                // Setting metadata failed due to lack of permissions. Note that
                // the Error object is not always populated, hence the OR filter.

                throw new SshKeyPushFailedException(
                          "You do not have sufficient permissions to publish an SSH key. " +
                          "You need the 'Service Account User' and " +
                          "'Compute Instance Admin' roles (or equivalent custom roles) " +
                          "to perform this action.",
                          HelpTopics.ManagingMetadataAuthorizedKeys);
            }
            catch (GoogleApiException e) when(e.IsBadRequest())
            {
                ApplicationTraceSources.Default.TraceVerbose(
                    "Setting request payload metadata failed with 400: {0} ({1})",
                    e.Message,
                    e.Error?.Errors.EnsureNotNull().Select(er => er.Reason).FirstOrDefault());

                // This slightly weirdly encoded error happens if the user has the necessary
                // permissions on the VM, but lacks ActAs permission on the associated
                // service account.

                throw new SshKeyPushFailedException(
                          "You do not have sufficient permissions to publish an SSH key. " +
                          "Because this VM instance uses a service account, you also need the " +
                          "'Service Account User' role.",
                          HelpTopics.ManagingMetadataAuthorizedKeys);
            }
        }
        public void WhenKeySerialized_ThenTimestampHasNoMilliseconds()
        {
            var key = new ManagedMetadataAuthorizedKey(
                "login",
                "ssh-rsa",
                "key",
                new ManagedKeyMetadata(
                    "*****@*****.**",
                    new DateTime(2020, 1, 1, 23, 59, 59, 123, DateTimeKind.Utc)));

            Assert.AreEqual(
                "login:ssh-rsa key google-ssh {\"userName\":\"[email protected]\",\"expireOn\":\"2020-01-01T23:59:59+0000\"}",
                key.ToString());
        }
        //---------------------------------------------------------------------
        // IPublicKeyService.
        //---------------------------------------------------------------------

        public async Task <AuthorizedKey> AuthorizeKeyAsync(
            InstanceLocator instance,
            ISshKey key,
            TimeSpan validity,
            string preferredPosixUsername,
            AuthorizeKeyMethods allowedMethods,
            CancellationToken token)
        {
            Utilities.ThrowIfNull(instance, nameof(key));
            Utilities.ThrowIfNull(key, nameof(key));

            using (ApplicationTraceSources.Default.TraceMethod().WithParameters(instance))
            {
                //
                // Query metadata for instance and project in parallel.
                //
                var instanceDetailsTask = this.computeEngineAdapter.GetInstanceAsync(
                    instance,
                    token)
                                          .ConfigureAwait(false);
                var projectDetailsTask = this.computeEngineAdapter.GetProjectAsync(
                    instance.ProjectId,
                    token)
                                         .ConfigureAwait(false);

                var instanceDetails = await instanceDetailsTask;
                var projectDetails  = await projectDetailsTask;

                var osLoginEnabled = IsFlagEnabled(
                    projectDetails,
                    instanceDetails,
                    EnableOsLoginFlag);

                ApplicationTraceSources.Default.TraceVerbose(
                    "OS Login status for {0}: {1}", instance, osLoginEnabled);

                if (osLoginEnabled)
                {
                    //
                    // If OS Login is enabled, it has to be used. Any metadata keys
                    // are ignored.
                    //
                    if (!allowedMethods.HasFlag(AuthorizeKeyMethods.Oslogin))
                    {
                        throw new InvalidOperationException(
                                  $"{instance} requires OS Login to beused");
                    }

                    if (IsFlagEnabled(projectDetails, instanceDetails, EnableOsLoginMultiFactorFlag))
                    {
                        throw new NotImplementedException(
                                  "OS Login 2-factor authentication is not supported");
                    }

                    //
                    // NB. It's cheaper to unconditionally push the key than
                    // to check for previous keys first.
                    //
                    return(await this.osLoginService.AuthorizeKeyAsync(
                               instance.ProjectId,
                               OsLoginSystemType.Linux,
                               key,
                               validity,
                               token)
                           .ConfigureAwait(false));
                }
                else
                {
                    var instanceMetadata = instanceDetails.Metadata;
                    var projectMetadata  = projectDetails.CommonInstanceMetadata;

                    //
                    // Check if there is a legacy SSH key. If there is one,
                    // other keys are ignored.
                    //
                    // NB. legacy SSH keys were instance-only, so checking
                    // the instance metadata is sufficient.
                    //
                    if (IsLegacySshKeyPresent(instanceMetadata))
                    {
                        throw new UnsupportedLegacySshKeyEncounteredException(
                                  $"Connecting to the VM instance {instance.Name} is not supported " +
                                  "because the instance uses legacy SSH keys in its metadata (sshKeys)",
                                  HelpTopics.ManagingMetadataAuthorizedKeys);
                    }

                    //
                    // There is no legacy key, so we're good to push a new key.
                    //
                    // Now figure out which username to use and where to push it.
                    //
                    var blockProjectSshKeys = IsFlagEnabled(
                        projectDetails,
                        instanceDetails,
                        BlockProjectSshKeysFlag);

                    bool useInstanceKeySet;
                    if (allowedMethods.HasFlag(AuthorizeKeyMethods.ProjectMetadata) &&
                        allowedMethods.HasFlag(AuthorizeKeyMethods.InstanceMetadata))
                    {
                        //
                        // Both allowed - use project metadata unless:
                        // - project keys are blocked
                        // - we do not have the permission to update project metadata.
                        //
                        var canUpdateProjectMetadata = await this.resourceManagerAdapter
                                                       .IsGrantedPermission(
                            instance.ProjectId,
                            Permissions.ComputeProjectsSetCommonInstanceMetadata,
                            token)
                                                       .ConfigureAwait(false);

                        useInstanceKeySet = blockProjectSshKeys || !canUpdateProjectMetadata;
                    }
                    else if (allowedMethods.HasFlag(AuthorizeKeyMethods.ProjectMetadata))
                    {
                        // Only project allowed.
                        if (blockProjectSshKeys)
                        {
                            throw new InvalidOperationException(
                                      $"Project {instance.ProjectId} does not allow project-level SSH keys");
                        }
                        else
                        {
                            useInstanceKeySet = false;
                        }
                    }
                    else if (allowedMethods.HasFlag(AuthorizeKeyMethods.InstanceMetadata))
                    {
                        // Only instance allowed.
                        useInstanceKeySet = true;
                    }
                    else
                    {
                        // Neither project nor instance allowed.
                        throw new ArgumentException(nameof(allowedMethods));
                    }

                    var profile = AuthorizedKey.ForMetadata(
                        key,
                        preferredPosixUsername,
                        useInstanceKeySet,
                        this.authorizationAdapter.Authorization);
                    Debug.Assert(profile.Username != null);

                    var metadataKey = new ManagedMetadataAuthorizedKey(
                        profile.Username,
                        key.Type,
                        key.PublicKeyString,
                        new ManagedKeyMetadata(
                            this.authorizationAdapter.Authorization.Email,
                            DateTime.UtcNow.Add(validity)));

                    var existingKeySet = MetadataAuthorizedKeySet.FromMetadata(
                        useInstanceKeySet
                            ? instanceMetadata
                            : projectMetadata);

                    if (existingKeySet
                        .RemoveExpiredKeys()
                        .Contains(metadataKey))
                    {
                        //
                        // The key is there already, so we are all set.
                        //
                        ApplicationTraceSources.Default.TraceVerbose(
                            "Existing SSH key found for {0}",
                            profile.Username);
                    }
                    else
                    {
                        //
                        // Key not known yet, so we have to push it to
                        // the metadata.
                        //
                        ApplicationTraceSources.Default.TraceVerbose(
                            "Pushing new SSH key for {0}",
                            profile.Username);

                        await PushPublicKeyToMetadataAsync(
                            instance,
                            useInstanceKeySet,
                            metadataKey,
                            token)
                        .ConfigureAwait(false);
                    }

                    return(profile);
                }
            }
        }