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); } } }