# -*- coding: utf-8 -*- # # Copyright 2017 Google LLC. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Update a key.""" from __future__ import absolute_import from __future__ import division from __future__ import unicode_literals from apitools.base.py import exceptions as apitools_exceptions from googlecloudsdk.api_lib.cloudkms import base as cloudkms_base from googlecloudsdk.calliope import base from googlecloudsdk.command_lib.kms import exceptions as kms_exceptions from googlecloudsdk.command_lib.kms import flags from googlecloudsdk.command_lib.kms import maps from googlecloudsdk.command_lib.kms import resource_args from googlecloudsdk.command_lib.util.args import labels_util @base.UniverseCompatible @base.ReleaseTracks( base.ReleaseTrack.ALPHA, base.ReleaseTrack.BETA, base.ReleaseTrack.GA ) class Update(base.UpdateCommand): r"""Update a key. 1. Update the rotation schedule for the given key. Updates the rotation schedule for the given key. The schedule automatically creates a new primary version for the key according to `next-rotation-time` and `rotation-period` flags. Flag `next-rotation-time` must be in ISO 8601 or RFC3339 format, and `rotation-period` must be in the form INTEGER[UNIT], where units can be one of seconds (s), minutes (m), hours (h) or days (d). Key rotations performed manually via `update-primary-version` and the version `create` do not affect the stored `next-rotation-time`. 2. Remove the rotation schedule for the given key with `remove-rotation-schedule` flag. 3. Update/Remove the labels for the given key with `update-labels` and/or `remove-labels` flags. 4. Update the primary version for the given key with `primary-version` flag. 5. Update the Key Access Justifications policy for the given key with `allowed-access-reasons` flag to allow specified reasons. The key must be enrolled in Key Access Justifications to use this flag. 6. Remove the Key Access Justifications policy for the given key with `remove-key-access-justifications-policy` flag. The key must be enrolled in Key Access Justifications to use this flag. 7. Update the Key Access Justifications policy for the given key with `allowed_access_reasons` flag to allow zero access reasons. This effectively disables the key, because a policy is configured to reject all access reasons. The key must be enrolled in Key Access Justifications to use this flag. ## EXAMPLES The following command sets a 30 day rotation period for the key named `frodo` within the keyring `fellowship` and location `global` starting at the specified time: $ {command} frodo \ --location=global \ --keyring=fellowship \ --rotation-period=30d \ --next-rotation-time=2017-10-12T12:34:56.1234Z The following command removes the rotation schedule for the key named `frodo` within the keyring `fellowship` and location `global`: $ {command} frodo \ --location=global \ --keyring=fellowship \ --remove-rotation-schedule The following command updates the labels value for the key named `frodo` within the keyring `fellowship` and location `global`. If the label key does not exist at the time, it will be added: $ {command} frodo \ --location=global \ --keyring=fellowship \ --update-labels=k1=v1 The following command removes labels k1 and k2 from the key named `frodo` within the keyring `fellowship` and location `global`: $ {command} frodo \ --location=global \ --keyring=fellowship \ --remove-labels=k1,k2 The following command updates the primary version for the key named `frodo` within the keyring `fellowship` and location `global`: $ {command} frodo \ --location=global \ --keyring=fellowship \ --primary-version=1 The following command updates the default algorithm for the key named `frodo` within the keyring `fellowship` and location `global`, assuming the key originally has purpose 'asymmetric-encryption' and algorithm 'rsa-decrypt-oaep-2048-sha256': $ {command} frodo \ --location=global \ --keyring=fellowship \ --default-algorithm=rsa-decrypt-oaep-4096-sha256 The following command updates the Key Access Justifications policy for the key named `frodo` within the keyring ``fellowship'' and location ``global'' to allow only ``customer-initiated-access'' and ``google-initiated-system-operation'': $ {command} frodo \ --location=global \ --keyring=fellowship \ --allowed-access-reasons=customer-initiated-access,google-initiated-system-operation The following command removes the Key Access Justifications policy for the key named `frodo` within the keyring ``fellowship'' and location ``global'', which results in all access reasons being allowed: $ {command} frodo \ --location=global \ --keyring=fellowship \ --remove-key-access-justifications-policy The following command updates the Key Access Justifications policy for the key named `frodo` within the keyring ``fellowship'' and location ``global'' to allow only zero access reasons, effectively disabling the key: $ {command} frodo \ --location=global \ --keyring=fellowship \ --allowed-access-reasons= """ @staticmethod def Args(parser): resource_args.AddKmsKeyResourceArgForKMS(parser, True, 'key') flags.AddRotationPeriodFlag(parser) flags.AddNextRotationTimeFlag(parser) flags.AddRemoveRotationScheduleFlag(parser) flags.AddCryptoKeyPrimaryVersionFlag(parser, 'to make primary') labels_util.AddUpdateLabelsFlags(parser) flags.AddDefaultAlgorithmFlag(parser) flags.AddAllowedAccessReasonsFlag(parser) flags.AddRemoveKeyAccessJustificationsPolicyFlag(parser) def ProcessFlags(self, args): fields_to_update = [] labels_diff = labels_util.Diff.FromUpdateArgs(args) if labels_diff.MayHaveUpdates(): fields_to_update.append('labels') if args.remove_rotation_schedule: if args.rotation_period or args.next_rotation_time: raise kms_exceptions.ArgumentError( 'You cannot set and remove rotation schedule at the same time.') fields_to_update.append('rotationPeriod') fields_to_update.append('nextRotationTime') if args.rotation_period: fields_to_update.append('rotationPeriod') if args.next_rotation_time: fields_to_update.append('nextRotationTime') if args.default_algorithm: fields_to_update.append('versionTemplate.algorithm') if ( args.allowed_access_reasons is not None and args.remove_key_access_justifications_policy ): raise kms_exceptions.ArgumentError( 'You cannot set and remove a Key Access Justifications policy at the ' 'same time.' ) if ( args.allowed_access_reasons is not None or args.remove_key_access_justifications_policy ): fields_to_update.append('keyAccessJustificationsPolicy') # Raise an exception when no update field is specified. if not args.primary_version and not fields_to_update: raise kms_exceptions.UpdateError( 'At least one of --primary-version or --update-labels or ' '--remove-labels or --clear-labels or --rotation-period or ' '--next-rotation-time or --remove-rotation-schedule or ' '--default-algorithm or --allowed-access-reasons or ' '--remove-key-access-justifications-policy must be specified.' ) return fields_to_update def UpdatePrimaryVersion(self, args): client = cloudkms_base.GetClientInstance() messages = cloudkms_base.GetMessagesModule() crypto_key_ref = args.CONCEPTS.key.Parse() req = messages.CloudkmsProjectsLocationsKeyRingsCryptoKeysUpdatePrimaryVersionRequest( # pylint: disable=line-too-long name=crypto_key_ref.RelativeName(), updateCryptoKeyPrimaryVersionRequest=( messages.UpdateCryptoKeyPrimaryVersionRequest( cryptoKeyVersionId=args.primary_version))) try: response = client.projects_locations_keyRings_cryptoKeys.UpdatePrimaryVersion( # pylint: disable=line-too-long req) except apitools_exceptions.HttpError: return None return response def UpdateOthers(self, args, crypto_key, fields_to_update): client = cloudkms_base.GetClientInstance() messages = cloudkms_base.GetMessagesModule() crypto_key_ref = args.CONCEPTS.key.Parse() labels_update = labels_util.Diff.FromUpdateArgs(args).Apply( messages.CryptoKey.LabelsValue, crypto_key.labels) if labels_update.needs_update: new_labels = labels_update.labels else: new_labels = crypto_key.labels req = messages.CloudkmsProjectsLocationsKeyRingsCryptoKeysPatchRequest( name=crypto_key_ref.RelativeName(), cryptoKey=messages.CryptoKey( labels=new_labels)) req.updateMask = ','.join(fields_to_update) flags.SetNextRotationTime(args, req.cryptoKey) flags.SetRotationPeriod(args, req.cryptoKey) if args.default_algorithm: valid_algorithms = maps.VALID_ALGORITHMS_MAP[crypto_key.purpose] if args.default_algorithm not in valid_algorithms: raise kms_exceptions.UpdateError( 'Update failed: Algorithm {algorithm} is not valid. Here are the ' 'valid algorithm(s) for purpose {purpose}: {all_algorithms}'.format( algorithm=args.default_algorithm, purpose=crypto_key.purpose, all_algorithms=', '.join(valid_algorithms))) req.cryptoKey.versionTemplate = messages.CryptoKeyVersionTemplate( algorithm=maps.ALGORITHM_MAPPER.GetEnumForChoice( args.default_algorithm)) if not args.remove_key_access_justifications_policy: flags.SetKeyAccessJustificationsPolicy(args, req.cryptoKey) try: response = client.projects_locations_keyRings_cryptoKeys.Patch(req) except apitools_exceptions.HttpError: return None return response def HandleErrors(self, args, set_primary_version_succeeds, other_updates_succeed, fields_to_update): """Handles various errors that may occur during any update stage. Never returns without an exception. Args: args: Input arguments. set_primary_version_succeeds: True if the primary verion is updated successfully. other_updates_succeed: True if all other updates (besides primary verions) is updated successfully. fields_to_update: A list of fields to be updated. Raises: ToolException: An exception raised when there is error during any update stage. """ err = 'An Error occurred:' if not set_primary_version_succeeds: err += " Failed to update field 'primaryVersion'." elif args.primary_version: err += " Field 'primaryVersion' was updated." if not other_updates_succeed: err += " Failed to update field(s) '{}'.".format( "', '".join(fields_to_update)) elif fields_to_update: err += " Field(s) '{}' were updated.".format( "', '".join(fields_to_update)) raise kms_exceptions.UpdateError(err) def Run(self, args): """Updates the relevant fields (of a CryptoKey) from the flags.""" # Check the flags and raise an exception if any check fails. fields_to_update = self.ProcessFlags(args) # Try to get the cryptoKey and raise an exception if the key doesn't exist. client = cloudkms_base.GetClientInstance() messages = cloudkms_base.GetMessagesModule() crypto_key_ref = args.CONCEPTS.key.Parse() crypto_key = client.projects_locations_keyRings_cryptoKeys.Get( messages.CloudkmsProjectsLocationsKeyRingsCryptoKeysGetRequest( name=crypto_key_ref.RelativeName())) # Try to update the key's primary version. set_primary_version_succeeds = True if args.primary_version: response = self.UpdatePrimaryVersion(args) if response: crypto_key = response # If call succeeds, update the crypto_key. else: set_primary_version_succeeds = False # Try other updates. other_updates_succeed = True if fields_to_update: response = self.UpdateOthers(args, crypto_key, fields_to_update) if response: crypto_key = response # If call succeeds, update the crypto_key. else: other_updates_succeed = False if not set_primary_version_succeeds or not other_updates_succeed: self.HandleErrors(args, set_primary_version_succeeds, other_updates_succeed, fields_to_update) else: return crypto_key