//--------------------------------------------------------------------- // 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); } } }
//--------------------------------------------------------------------- // IOsLoginService. //--------------------------------------------------------------------- public async Task <AuthorizedKey> AuthorizeKeyAsync( string projectId, OsLoginSystemType os, ISshKey key, TimeSpan validity, CancellationToken token) { Utilities.ThrowIfNullOrEmpty(projectId, nameof(projectId)); Utilities.ThrowIfNull(key, nameof(key)); if (os != OsLoginSystemType.Linux) { throw new ArgumentException(nameof(os)); } if (validity.TotalSeconds <= 0) { throw new ArgumentException(nameof(validity)); } using (ApplicationTraceSources.Default.TraceMethod().WithParameters(projectId)) { // // If OS Login is enabled for a project, we have to use // the Posix username from the OS Login login profile. // // Note that the Posix account managed by OS login can // differ based on the project that we're trying to access. // Therefore, make sure to specify the project when // importing the key. // // OS Login auto-generates a username for us. Again, this // username might differ based on project/organization. // // // Import the key for the given project. // var loginProfile = await this.adapter.ImportSshPublicKeyAsync( projectId, key, validity, token) .ConfigureAwait(false); // // Although rare, there could be multiple POSIX accounts. // var account = loginProfile.PosixAccounts .EnsureNotNull() .FirstOrDefault(a => a.Primary == true && a.OperatingSystemType == "LINUX"); if (account == null) { // // This is strange, the account should have been created. // throw new OsLoginSshKeyImportFailedException( "Imported SSH key to OSLogin, but no POSIX account was created", HelpTopics.TroubleshootingOsLogin); } return(AuthorizedKey.ForOsLoginAccount(key, account)); } }