feat: Add new gcloud commands, API clients, and third-party libraries across various services.

This commit is contained in:
2026-01-01 20:26:35 +01:00
parent 5e23cbece0
commit a19e592eb7
25221 changed files with 8324611 additions and 0 deletions

View File

@@ -0,0 +1,31 @@
# -*- coding: utf-8 -*- #
# Copyright 2021 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.
"""PrivateCA resource completers."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.command_lib.util import completers
class LocationsCompleter(completers.ListCommandCompleter):
"""The location completer."""
def __init__(self, **kwargs):
super(LocationsCompleter, self).__init__(
collection='privateca.projects.locations',
list_command='privateca locations list --uri',
**kwargs)

View File

@@ -0,0 +1,334 @@
# -*- coding: utf-8 -*- #
# Copyright 2023 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.
"""Helpers for create commands."""
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.privateca import base as privateca_base
from googlecloudsdk.api_lib.privateca import certificate_utils
from googlecloudsdk.api_lib.privateca import request_utils
from googlecloudsdk.calliope import exceptions
from googlecloudsdk.command_lib.privateca import flags
from googlecloudsdk.command_lib.privateca import resource_args
from googlecloudsdk.command_lib.util.args import labels_util
def _ParseCAResourceArgs(args):
"""Parses, validates and returns the resource args from the CLI.
Args:
args: The parsed arguments from the command-line.
Returns:
Tuple containing the Resource objects for (CA, source CA, issuer).
"""
resource_args.ValidateResourceIsCompleteIfSpecified(args, 'kms_key_version')
resource_args.ValidateResourceIsCompleteIfSpecified(args, 'issuer_pool')
resource_args.ValidateResourceIsCompleteIfSpecified(args, 'from_ca')
ca_ref = args.CONCEPTS.certificate_authority.Parse()
resource_args.ValidateResourceLocation(
ca_ref, 'CERTIFICATE_AUTHORITY', version='v1'
)
kms_key_version_ref = args.CONCEPTS.kms_key_version.Parse()
if (
kms_key_version_ref
and ca_ref.locationsId != kms_key_version_ref.locationsId
):
raise exceptions.InvalidArgumentException(
'--kms-key-version',
'KMS key must be in the same location as the Certificate Authority '
'({}).'.format(ca_ref.locationsId),
)
issuer_ref = (
args.CONCEPTS.issuer_pool.Parse()
if hasattr(args, 'issuer_pool')
else None
)
source_ca_ref = args.CONCEPTS.from_ca.Parse()
if (
source_ca_ref
and source_ca_ref.Parent().RelativeName()
!= ca_ref.Parent().RelativeName()
):
raise exceptions.InvalidArgumentException(
'--from-ca',
'The provided source CA must be a part of the same pool as the'
' specified CA to be created.',
)
return (ca_ref, source_ca_ref, issuer_ref)
def CreateCAFromArgs(args, is_subordinate):
"""Creates a GA CA object from CA create flags.
Args:
args: The parser that contains the flag values.
is_subordinate: If True, a subordinate CA is returned, otherwise a root CA.
Returns:
A tuple for the CA to create with (CA object, CA ref, issuer).
"""
client = privateca_base.GetClientInstance(api_version='v1')
messages = privateca_base.GetMessagesModule(api_version='v1')
ca_ref, source_ca_ref, issuer_ref = _ParseCAResourceArgs(args)
pool_ref = ca_ref.Parent()
source_ca = None
if source_ca_ref:
source_ca = client.projects_locations_caPools_certificateAuthorities.Get(
messages.PrivatecaProjectsLocationsCaPoolsCertificateAuthoritiesGetRequest(
name=source_ca_ref.RelativeName()
)
)
if not source_ca:
raise exceptions.InvalidArgumentException(
'--from-ca', 'The provided source CA could not be retrieved.'
)
ca_pool = client.projects_locations_caPools.Get(
messages.PrivatecaProjectsLocationsCaPoolsGetRequest(
name=pool_ref.RelativeName()
)
)
keyspec = flags.ParseKeySpec(args)
if (
ca_pool.tier == messages.CaPool.TierValueValuesEnum.DEVOPS
and keyspec.cloudKmsKeyVersion
):
raise exceptions.InvalidArgumentException(
'--kms-key-version',
'The DevOps tier does not support user-specified KMS keys.',
)
subject_config = messages.SubjectConfig(
subject=messages.Subject(), subjectAltName=messages.SubjectAltNames()
)
if args.IsSpecified('subject'):
subject_config.subject = flags.ParseSubject(args)
elif args.IsKnownAndSpecified('subject_file'):
subject_config.subject = flags.ParseSubjectFile(args)
elif source_ca:
subject_config.subject = source_ca.config.subjectConfig.subject
if flags.SanFlagsAreSpecified(args):
subject_config.subjectAltName = flags.ParseSanFlags(args)
elif source_ca:
subject_config.subjectAltName = (
source_ca.config.subjectConfig.subjectAltName
)
flags.ValidateSubjectConfig(subject_config)
# Populate x509 params to default.
x509_parameters = flags.ParseX509Parameters(args, is_ca_command=True)
if source_ca and not flags.X509ConfigFlagsAreSpecified(args):
x509_parameters = source_ca.config.x509Config
# Args.validity will be populated to default if not specified.
lifetime = flags.ParseValidityFlag(args)
if source_ca and not args.IsSpecified('validity'):
lifetime = source_ca.lifetime
labels = labels_util.ParseCreateArgs(
args, messages.CertificateAuthority.LabelsValue
)
ski = flags.ParseSubjectKeyId(args, messages)
# Parse user defined access URLs
user_defined_access_urls = flags.ParseUserDefinedAccessUrls(args, messages)
new_ca = messages.CertificateAuthority(
type=messages.CertificateAuthority.TypeValueValuesEnum.SUBORDINATE
if is_subordinate
else messages.CertificateAuthority.TypeValueValuesEnum.SELF_SIGNED,
lifetime=lifetime,
config=messages.CertificateConfig(
subjectConfig=subject_config,
x509Config=x509_parameters,
subjectKeyId=ski,
),
keySpec=keyspec,
gcsBucket=None,
userDefinedAccessUrls=user_defined_access_urls,
labels=labels,
)
return (new_ca, ca_ref, issuer_ref)
def HasEnabledCa(ca_list, messages):
"""Checks if there are any enabled CAs in the CA list."""
for ca in ca_list:
if ca.state == messages.CertificateAuthority.StateValueValuesEnum.ENABLED:
return True
return False
def _ValidateIssuingCa(ca_pool_name, issuing_ca_id, ca_list):
"""Checks that an issuing CA is in the CA Pool and has a valid state.
Args:
ca_pool_name: The resource name of the containing CA Pool.
issuing_ca_id: The CA ID of the CA to verify.
ca_list: The list of JSON CA objects in the CA pool to check from
Raises:
InvalidArgumentException on validation errors
"""
messages = privateca_base.GetMessagesModule(api_version='v1')
allowd_issuing_states = [
messages.CertificateAuthority.StateValueValuesEnum.ENABLED,
messages.CertificateAuthority.StateValueValuesEnum.STAGED,
]
issuing_ca = None
for ca in ca_list:
if 'certificateAuthorities/{}'.format(issuing_ca_id) in ca.name:
issuing_ca = ca
if not issuing_ca:
raise exceptions.InvalidArgumentException(
'--issuer-ca',
'The specified CA with ID [{}] was not found in CA Pool [{}]'.format(
issuing_ca_id, ca_pool_name
),
)
if issuing_ca.state not in allowd_issuing_states:
raise exceptions.InvalidArgumentException(
'--issuer-pool',
'The specified CA with ID [{}] in CA Pool [{}] is not ENABLED or'
' STAGED. Please choose a CA that has one of these states to issue the'
' CA certificate from.'.format(issuing_ca_id, ca_pool_name),
)
def ValidateIssuingPool(ca_pool_name, issuing_ca_id):
"""Checks that a CA Pool is valid to be issuing Pool for a subordinate.
Args:
ca_pool_name: The resource name of the issuing CA Pool.
issuing_ca_id: The optional CA ID in the CA Pool to validate.
Raises:
InvalidArgumentException if the CA Pool does not exist or is not enabled.
"""
try:
client = privateca_base.GetClientInstance(api_version='v1')
messages = privateca_base.GetMessagesModule(api_version='v1')
enabled_state = messages.CertificateAuthority.StateValueValuesEnum.ENABLED
ca_list_response = client.projects_locations_caPools_certificateAuthorities.List(
messages.PrivatecaProjectsLocationsCaPoolsCertificateAuthoritiesListRequest(
parent=ca_pool_name
)
)
ca_list = ca_list_response.certificateAuthorities
# If a specific CA is targeted, verify its properties
if issuing_ca_id:
_ValidateIssuingCa(ca_pool_name, issuing_ca_id, ca_list)
return
# Otherwise verify that there is an available CA to issue from
ca_states = [ca.state for ca in ca_list]
if enabled_state not in ca_states:
raise exceptions.InvalidArgumentException(
'--issuer-pool',
'The issuing CA Pool [{}] did not have any CAs in ENABLED state of'
' the {} CAs found. Please create or enable a CA and try again.'
.format(ca_pool_name, len(ca_list)),
)
except apitools_exceptions.HttpNotFoundError:
raise exceptions.InvalidArgumentException(
'--issuer-pool',
'The issuing CA Pool [{}] was not found. Please verify this information'
' is correct and try again.'.format(ca_pool_name),
)
def _CreateCertificateCreateRequest(issuer_pool_ref, csr, issuer_ca_id, new_ca):
"""Returns the certificate create request with the given settings.
Args:
issuer_pool_ref: The resource reference for the issuing CA pool.
csr: The Certificate Signing Request.
issuer_ca_id: The CA ID of the CA to sign the CSR, if specified.
new_ca: The CA object.
Returns:
A certificate create request.
"""
messages = privateca_base.GetMessagesModule(api_version='v1')
certificate_id = 'subordinate-{}'.format(certificate_utils.GenerateCertId())
issuer_pool_name = issuer_pool_ref.RelativeName()
certificate_name = '{}/certificates/{}'.format(
issuer_pool_name, certificate_id
)
lifetime = new_ca.lifetime
cert_request = (
messages.PrivatecaProjectsLocationsCaPoolsCertificatesCreateRequest(
certificateId=certificate_id,
parent=issuer_pool_name,
requestId=request_utils.GenerateRequestId(),
issuingCertificateAuthorityId=issuer_ca_id,
certificate=messages.Certificate(
name=certificate_name,
lifetime=lifetime,
pemCsr=csr,
),
)
)
if new_ca.config.subjectConfig.subject.rdnSequence:
cert_request.certificate.subjectMode = (
messages.Certificate.SubjectModeValueValuesEnum.RDN_SEQUENCE
)
return cert_request
def SignCsr(issuer_pool_ref, csr, issuer_ca_id, new_ca):
"""Issues a certificate under the given issuer with the given settings.
Args:
issuer_pool_ref: The resource reference for the issuing CA pool.
csr: The Certificate Signing Request.
issuer_ca_id: The CA ID of the CA to sign the CSR, if specified.
new_ca: The CA object.
Returns:
The certificate for the new CA.
"""
client = privateca_base.GetClientInstance(api_version='v1')
cert_request = _CreateCertificateCreateRequest(
issuer_pool_ref, csr, issuer_ca_id, new_ca
)
return client.projects_locations_caPools_certificates.Create(cert_request)

View File

@@ -0,0 +1,58 @@
# -*- coding: utf-8 -*- #
# Copyright 2020 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.
"""Helpers for raising exceptions."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.core import exceptions
class FileOutputError(exceptions.Error):
"""Error thrown for issues with writing to files."""
class InvalidCertificateAuthorityTypeError(exceptions.Error):
"""Error thrown for performing a command on the wrong CA type."""
class NoUpdateException(exceptions.Error):
"""Error thrown when an update command is run resulting in no updates."""
class UserAbortException(exceptions.Error):
"""Error thrown when an a user aborts the command."""
class InsufficientPermissionException(exceptions.Error):
"""Indicates that a user is missing required permissions for an operation."""
def __init__(self, resource, missing_permissions):
"""Create a new InsufficientPermissionException.
Args:
resource: str, The resource on which the user needs permissions.
missing_permissions: iterable, The missing permissions.
"""
super(InsufficientPermissionException, self).__init__(
'The current user does not have permissions for this operation. '
'Please ensure you have {} permissions on the {} and that '
'you are logged-in as the correct user and try again.'.format(
','.join(missing_permissions), resource))
class UnsupportedKmsKeyTypeException(exceptions.Error):
"""Indicates that a user is using an unsupported KMS key type."""

View File

@@ -0,0 +1,30 @@
# -*- coding: utf-8 -*- #
# Copyright 2020 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.
"""Helpers for list filter parameter."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.core.resource import resource_expr_rewrite
# TODO(b/169417671) Support client side filtering along server side filtering.
class BackendFilterRewrite(resource_expr_rewrite.Backend):
"""Limit filter expressions to those supported by the PrivateCA API backend."""
def RewriteOperand(self, operand):
"""Always quote the operand as the Cloud Filter Library won't be able to parse as values all arbitrary strings."""
return self.QuoteOperand(operand, always=True)

View File

@@ -0,0 +1,134 @@
# -*- coding: utf-8 -*- #
# Copyright 2020 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.
"""Hooks for Privateca surface."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.api_lib.privateca import base
from googlecloudsdk.api_lib.privateca import request_utils
from googlecloudsdk.command_lib.privateca import resource_args
from googlecloudsdk.core.util import times
def CheckResponseSubordinateTypeHook(version='v1'):
"""Raises an exception if the response is not a subordinate ca."""
def CheckResponseSubordinateTypeHookVersioned(response, unused_args):
resource_args.CheckExpectedCAType(
base.GetMessagesModule(
api_version=version
).CertificateAuthority.TypeValueValuesEnum.SUBORDINATE,
response,
version=version,
)
return response
return CheckResponseSubordinateTypeHookVersioned
def CheckResponseRootTypeHook(version='v1'):
"""Raises an exception if the response is not a root ca."""
def CheckResponseRootTypeHookVersioned(response, unused_args):
resource_args.CheckExpectedCAType(
base.GetMessagesModule(
api_version=version
).CertificateAuthority.TypeValueValuesEnum.SELF_SIGNED,
response,
version=version,
)
return response
return CheckResponseRootTypeHookVersioned
def _CheckRequestTypeHook(resource_ref, expected_type, version='v1'):
"""Do a get on a CA resource and check its type against expected_type."""
client = base.GetClientInstance(api_version=version)
messages = base.GetMessagesModule(api_version=version)
certificate_authority = client.projects_locations_caPools_certificateAuthorities.Get(
messages.PrivatecaProjectsLocationsCaPoolsCertificateAuthoritiesGetRequest(
name=resource_ref.RelativeName()
)
)
resource_args.CheckExpectedCAType(expected_type, certificate_authority)
def CheckRequestRootTypeHook(version='v1'):
"""Raises an exception if the request is not for a root ca."""
def CheckRequestRootTypeHookVersioned(resource_ref, unused_args, request):
_CheckRequestTypeHook(
resource_ref,
base.GetMessagesModule(
api_version=version
).CertificateAuthority.TypeValueValuesEnum.SELF_SIGNED,
)
return request
return CheckRequestRootTypeHookVersioned
def CheckRequestSubordinateTypeHook(version='v1'):
"""Raises an exception if the request is not for a subordinate ca."""
def CheckRequestSubordinateTypeHookVersioned(
resource_ref, unused_args, request
):
_CheckRequestTypeHook(
resource_ref,
base.GetMessagesModule(
api_version=version
).CertificateAuthority.TypeValueValuesEnum.SUBORDINATE,
)
return request
return CheckRequestSubordinateTypeHookVersioned
def AddRequestIdHook(unused_ref, unused_args, request):
"""Fills a unique identifier for a request with a requestId field."""
request.requestId = request_utils.GenerateRequestId()
return request
def _ConvertProtoToIsoDuration(proto_duration_str):
"""Convert a given 'proto duration' string to an ISO8601 duration string."""
return times.FormatDuration(times.ParseDuration(proto_duration_str, True))
def ConvertCertificateLifetimeToIso8601(response, unused_args):
"""Converts certificate lifetimes from proto duration format to ISO8601."""
# These fields could be None if the user specifies a filter that omits them.
if response.lifetime:
response.lifetime = _ConvertProtoToIsoDuration(response.lifetime)
if (
response.certificateDescription
and response.certificateDescription.subjectDescription
and response.certificateDescription.subjectDescription.lifetime
):
response.certificateDescription.subjectDescription.lifetime = (
_ConvertProtoToIsoDuration(
response.certificateDescription.subjectDescription.lifetime
)
)
return response

View File

@@ -0,0 +1,88 @@
# -*- coding: utf-8 -*- #
# Copyright 2023 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.
"""Helpers for testing IAM permissions."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.api_lib.cloudkms import iam as kms_iam
from googlecloudsdk.api_lib.cloudresourcemanager import projects_api
from googlecloudsdk.api_lib.privateca import base as privateca_base
from googlecloudsdk.command_lib.privateca import exceptions
# Permissions needed on a KMS key for creating a CA.
_CA_CREATE_PERMISSIONS_ON_KEY = [
'cloudkms.cryptoKeys.setIamPolicy',
]
# Permissions needed on a project for creating a CA.
_CA_CREATE_PERMISSIONS_ON_PROJECT = [
'privateca.certificateAuthorities.create'
]
# Permissions needed on a CA Pool for issuing certificates.
_CERTIFICATE_CREATE_PERMISSIONS_ON_CA_POOL = ['privateca.certificates.create']
def _CheckAllPermissions(actual_permissions, expected_permissions, resource):
"""Raises an exception if the expected permissions are not all present."""
# IAM won't return more permissions than requested, so equality works here.
diff = set(expected_permissions) - set(actual_permissions)
if diff:
raise exceptions.InsufficientPermissionException(
resource=resource, missing_permissions=diff)
def CheckCreateCertificateAuthorityPermissions(project_ref, kms_key_ref=None):
"""Ensures that the current user has the required permissions to create a CA.
Args:
project_ref: The project where the new CA will be created.
kms_key_ref: optional, The KMS key that will be used by the CA.
Raises:
InsufficientPermissionException: If the user is missing permissions.
"""
_CheckAllPermissions(
projects_api.TestIamPermissions(
project_ref, _CA_CREATE_PERMISSIONS_ON_PROJECT).permissions,
_CA_CREATE_PERMISSIONS_ON_PROJECT, 'project')
if kms_key_ref:
_CheckAllPermissions(
kms_iam.TestCryptoKeyIamPermissions(
kms_key_ref, _CA_CREATE_PERMISSIONS_ON_KEY).permissions,
_CA_CREATE_PERMISSIONS_ON_KEY, 'KMS key')
def CheckCreateCertificatePermissions(issuing_ca_pool_ref):
"""Ensures that the current user can issue a certificate from the given Pool.
Args:
issuing_ca_pool_ref: The CA pool that will create the certificate.
Raises:
InsufficientPermissionException: If the user is missing permissions.
"""
client = privateca_base.GetClientInstance(api_version='v1')
messages = privateca_base.GetMessagesModule(api_version='v1')
test_response = client.projects_locations_caPools.TestIamPermissions(
messages.PrivatecaProjectsLocationsCaPoolsTestIamPermissionsRequest(
resource=issuing_ca_pool_ref.RelativeName(),
testIamPermissionsRequest=messages.TestIamPermissionsRequest(
permissions=_CERTIFICATE_CREATE_PERMISSIONS_ON_CA_POOL)))
_CheckAllPermissions(test_response.permissions,
_CERTIFICATE_CREATE_PERMISSIONS_ON_CA_POOL, 'issuing CA')

View File

@@ -0,0 +1,115 @@
# -*- coding: utf-8 -*- #
# Copyright 2020 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.
"""Key generation utilities."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import os
import sys
from googlecloudsdk.command_lib.privateca import exceptions
from googlecloudsdk.core import log
from googlecloudsdk.core.util import files
KEY_OUTPUT_WARNING = """A private key was exported to {}.
Possession of this key file could allow anybody to act as this certificate's
subject. Please make sure that you store this key file in a secure location at
all times, and ensure that only authorized users have access to it.
"""
def RSAKeyGen(key_size=2048):
"""Generates an RSA public-private key pair.
Args:
key_size: The size of the RSA key, in number of bytes. Defaults to 2048.
Returns:
A tuple with: (private_key, public_key) both serialized in PKCS1 as bytes.
"""
import_error_msg = ('Cannot load the Pyca cryptography library. Either the '
'library is not installed, or site packages are not '
'enabled for the Google Cloud SDK. Please consult Cloud '
'KMS documentation on adding Pyca to Google Cloud SDK '
'for further instructions.\n'
'https://cloud.google.com/kms/docs/crypto')
try:
# TODO(b/141249289): Move imports to the top of the file. In the
# meantime, until we're sure that all Private CA SDK users have the
# cryptography module available, let's not error out if we can't load the
# module unless we're actually going down this code path.
# pylint: disable=g-import-not-at-top
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.backends.openssl.backend import backend
except ImportError:
log.err.Print(import_error_msg)
sys.exit(1)
# The serialization modules have moved in cryptography version 3.4 and above.
# Try both the old and new locations to support both versions. See b/183521338
# for more context.
try:
# pylint: disable=g-import-not-at-top
from cryptography.hazmat.primitives.serialization.base import Encoding
from cryptography.hazmat.primitives.serialization.base import PrivateFormat
from cryptography.hazmat.primitives.serialization.base import PublicFormat
from cryptography.hazmat.primitives.serialization.base import NoEncryption
except ImportError:
try:
# pylint: disable=g-import-not-at-top
from cryptography.hazmat.primitives.serialization import Encoding
from cryptography.hazmat.primitives.serialization import PrivateFormat
from cryptography.hazmat.primitives.serialization import PublicFormat
from cryptography.hazmat.primitives.serialization import NoEncryption
except ImportError:
log.err.Print(import_error_msg)
sys.exit(1)
private_key = rsa.generate_private_key(
public_exponent=65537, key_size=key_size, backend=backend)
private_key_bytes = private_key.private_bytes(
Encoding.PEM,
PrivateFormat.TraditionalOpenSSL, # PKCS#1
NoEncryption())
public_key_bytes = private_key.public_key().public_bytes(
Encoding.PEM, PublicFormat.SubjectPublicKeyInfo)
return private_key_bytes, public_key_bytes
def ExportPrivateKey(private_key_output_file, private_key_bytes):
"""Export a private key to a filename, printing a warning to the user.
Args:
private_key_output_file: The path of the file to export to.
private_key_bytes: The content in byte format to export.
"""
try:
# Make sure this file is only accesible to the running user before writing.
files.PrivatizeFile(private_key_output_file)
files.WriteFileContents(private_key_output_file, private_key_bytes)
# Make file readable only by owner.
os.chmod(private_key_output_file, 0o400)
log.warning(KEY_OUTPUT_WARNING.format(private_key_output_file))
except (files.Error, OSError, IOError):
raise exceptions.FileOutputError(
"Error writing to private key output file named '{}'".format(
private_key_output_file))

View File

@@ -0,0 +1,97 @@
# -*- coding: utf-8 -*- #
# Copyright 2020 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.
"""General utilities using operations in Privateca commands."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from apitools.base.py import encoding
from googlecloudsdk.api_lib.privateca import base
from googlecloudsdk.api_lib.util import messages as messages_util
from googlecloudsdk.api_lib.util import waiter
from googlecloudsdk.core import exceptions
from googlecloudsdk.core import resources
class OperationError(exceptions.Error):
"""Exception for errors encountered from an operation."""
class OperationTimeoutError(OperationError):
"""Exception for when an operation times out."""
def GetOperationRef(operation):
"""Get a resource reference to a long running operation."""
return resources.REGISTRY.ParseRelativeName(
operation.name, 'privateca.projects.locations.operations'
)
def Await(operation, progress_message, api_version='v1'):
"""Waits for operation to complete while displaying in-progress indicator.
Args:
operation: The Operation resource.
progress_message: The message to display with the in-progress indicator.
api_version: The API version.
Returns:
The resource that is the result of the operation.
Raises:
OperationError: if the operation did not complete successfully
"""
if operation.done:
if operation.error:
raise OperationError(operation.error.message)
return operation.response
operation_ref = GetOperationRef(operation)
poller = waiter.CloudOperationPollerNoResources(
base.GetClientInstance(api_version).projects_locations_operations
)
try:
return waiter.WaitFor(poller, operation_ref, progress_message)
except waiter.TimeoutError:
raise OperationTimeoutError(
'Requested action timed out. Please run the describe command on your'
' resource to see if changes were successful, or try again in a few'
' minutes.'
)
def GetMessageFromResponse(response, message_type):
"""Returns a message from the ResponseValue.
Operations normally return a ResponseValue object in their response field that
is somewhat difficult to use. This functions returns the corresponding
message type to make it easier to parse the response.
Args:
response: The ResponseValue object that resulted from an Operation.
message_type: The type of the message that should be returned
Returns:
An instance of message_type with the values from the response filled in.
"""
message_dict = encoding.MessageToDict(response)
# '@type' is not needed and not present in messages.
if '@type' in message_dict:
del message_dict['@type']
return messages_util.DictToMessageWithErrorCheck(
message_dict, message_type, throw_on_unexpected_fields=False
)

View File

@@ -0,0 +1,67 @@
# -*- coding: utf-8 -*- #
# Copyright 2020 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.
"""Helpers for dealing with the Private CA P4SA."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.api_lib.cloudkms import iam as kms_iam
from googlecloudsdk.api_lib.privateca import base as privateca_base
from googlecloudsdk.api_lib.services import serviceusage
from googlecloudsdk.api_lib.storage import storage_api
def GetOrCreate(project_ref):
"""Gets (or creates) the P4SA for Private CA in the given project.
If the P4SA does not exist for this project, it will be created. Otherwise,
the email address of the existing P4SA will be returned.
Args:
project_ref: resources.Resource reference to the project for the P4SA.
Returns:
Email address of the Private CA P4SA for the given project.
"""
service_name = privateca_base.GetServiceName()
response = serviceusage.GenerateServiceIdentity(project_ref.Name(),
service_name)
return response['email']
def AddResourceRoleBindings(p4sa_email, kms_key_ref=None, bucket_ref=None):
"""Adds the necessary P4SA role bindings on the given key and bucket.
Args:
p4sa_email: Email address of the P4SA for which to add role bindings. This
can come from a call to GetOrCreate().
kms_key_ref: optional, resources.Resource reference to the KMS key on which
to add a role binding.
bucket_ref: optional, storage_util.BucketReference to the GCS bucket on
which to add a role binding.
"""
principal = 'serviceAccount:{}'.format(p4sa_email)
if kms_key_ref:
kms_iam.AddPolicyBindingsToCryptoKey(
kms_key_ref, [(principal, 'roles/cloudkms.signerVerifier'),
(principal, 'roles/viewer')])
if bucket_ref:
client = storage_api.StorageClient()
client.AddIamPolicyBindings(
bucket_ref, [(principal, 'roles/storage.objectAdmin'),
(principal, 'roles/storage.legacyBucketReader')])

View File

@@ -0,0 +1,67 @@
# -*- coding: utf-8 -*- #
# Copyright 2020 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.
"""PEM utilities for Privateca commands."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
import re
from googlecloudsdk.calliope import exceptions
_PEM_CERT_RE = (
r'-----BEGIN CERTIFICATE-----\n(?:[a-zA-Z0-9+/=]+\r?\n)+-----END '
r'CERTIFICATE-----\s*')
_PEM_CHAIN_RE = re.compile(r'^({})+$'.format(_PEM_CERT_RE))
def ValidateAndParsePemChain(pem_chain):
"""Validates and parses a pem_chain string into a list of certs.
Args:
pem_chain: The string represting the pem_chain.
Returns:
A list of the certificates that make up the chain, in the same order
as the input.
Raises:
exceptions.InvalidArgumentException if the pem_chain is in an unexpected
format.
"""
if not re.match(_PEM_CHAIN_RE, pem_chain):
raise exceptions.InvalidArgumentException(
'pem-chain', 'The pem_chain you provided was in an unexpected format.')
certs = re.findall(_PEM_CERT_RE, pem_chain)
for i in range(len(certs)):
# Match service-generated certs, which always end with a single newline.
certs[i] = certs[i].strip() + '\n'
return certs
def PemChainForOutput(pem_chain):
"""Formats a pem chain for output with exactly 1 newline character between each cert.
Args:
pem_chain: The list of certificate strings to output
Returns:
The string value of all certificates appended together for output.
"""
stripped_pem_chain = [cert.strip() for cert in pem_chain]
return '\n'.join(stripped_pem_chain)

View File

@@ -0,0 +1,243 @@
# -*- coding: utf-8 -*- #
# Copyright 2020 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.
"""Helpers for building x509 parameters using a preset profile."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.api_lib.privateca import base as privateca_base
from googlecloudsdk.api_lib.util import messages as messages_util
from googlecloudsdk.calliope import exceptions
_LEAF_CLIENT_TLS = {
'caOptions': {
'isCa': False
},
'keyUsage': {
'extendedKeyUsage': {
'clientAuth': True
},
'baseKeyUsage': {
'digitalSignature': True,
'keyEncipherment': True
}
}
}
_LEAF_CODE_SIGNING = {
'caOptions': {
'isCa': False
},
'keyUsage': {
'extendedKeyUsage': {
'codeSigning': True
},
'baseKeyUsage': {
'digitalSignature': True,
'contentCommitment': True
}
}
}
_LEAF_MTLS = {
'caOptions': {
'isCa': False
},
'keyUsage': {
'extendedKeyUsage': {
'serverAuth': True,
'clientAuth': True
},
'baseKeyUsage': {
'digitalSignature': True,
'keyEncipherment': True
}
}
}
_LEAF_SERVER_TLS = {
'caOptions': {
'isCa': False
},
'keyUsage': {
'extendedKeyUsage': {
'serverAuth': True
},
'baseKeyUsage': {
'digitalSignature': True,
'keyEncipherment': True
}
}
}
_LEAF_SMIME = {
'caOptions': {
'isCa': False
},
'keyUsage': {
'extendedKeyUsage': {
'emailProtection': True
},
'baseKeyUsage': {
'digitalSignature': True,
'contentCommitment': True
}
}
}
_ROOT_UNCONSTRAINED = {
'caOptions': {
'isCa': True
},
'keyUsage': {
'baseKeyUsage': {
'certSign': True,
'crlSign': True
}
}
}
_SUBORDINATE_CLIENT_TLS_PATHLEN_0 = {
'caOptions': {
'isCa': True,
'maxIssuerPathLength': 0
},
'keyUsage': {
'extendedKeyUsage': {
'clientAuth': True
},
'baseKeyUsage': {
'certSign': True,
'crlSign': True
}
}
}
_SUBORDINATE_CODE_SIGNING_PATHLEN_0 = {
'caOptions': {
'isCa': True,
'maxIssuerPathLength': 0
},
'keyUsage': {
'extendedKeyUsage': {
'codeSigning': True
},
'baseKeyUsage': {
'certSign': True,
'crlSign': True
}
}
}
_SUBORDINATE_MTLS_PATHLEN_0 = {
'caOptions': {
'isCa': True,
'maxIssuerPathLength': 0
},
'keyUsage': {
'extendedKeyUsage': {
'serverAuth': True,
'clientAuth': True
},
'baseKeyUsage': {
'certSign': True,
'crlSign': True
}
}
}
_SUBORDINATE_SERVER_TLS_PATHLEN_0 = {
'caOptions': {
'isCa': True,
'maxIssuerPathLength': 0
},
'keyUsage': {
'extendedKeyUsage': {
'serverAuth': True
},
'baseKeyUsage': {
'certSign': True,
'crlSign': True
}
}
}
_SUBORDINATE_SMIME_PATHLEN_0 = {
'caOptions': {
'isCa': True,
'maxIssuerPathLength': 0
},
'keyUsage': {
'extendedKeyUsage': {
'emailProtection': True
},
'baseKeyUsage': {
'certSign': True,
'crlSign': True
}
}
}
_SUBORDINATE_UNCONSTRAINED_PATHLEN_0 = {
'caOptions': {
'isCa': True,
'maxIssuerPathLength': 0
},
'keyUsage': {
'baseKeyUsage': {
'certSign': True,
'crlSign': True
}
}
}
_PRESET_PROFILES = {
'leaf_client_tls': _LEAF_CLIENT_TLS,
'leaf_code_signing': _LEAF_CODE_SIGNING,
'leaf_mtls': _LEAF_MTLS,
'leaf_server_tls': _LEAF_SERVER_TLS,
'leaf_smime': _LEAF_SMIME,
'root_unconstrained': _ROOT_UNCONSTRAINED,
'subordinate_client_tls_pathlen_0': _SUBORDINATE_CLIENT_TLS_PATHLEN_0,
'subordinate_code_signing_pathlen_0': _SUBORDINATE_CODE_SIGNING_PATHLEN_0,
'subordinate_mtls_pathlen_0': _SUBORDINATE_MTLS_PATHLEN_0,
'subordinate_server_tls_pathlen_0': _SUBORDINATE_SERVER_TLS_PATHLEN_0,
'subordinate_smime_pathlen_0': _SUBORDINATE_SMIME_PATHLEN_0,
'subordinate_unconstrained_pathlen_0': _SUBORDINATE_UNCONSTRAINED_PATHLEN_0
}
def GetPresetProfileOptions():
"""Returns the possible string options for the use-preset-profile flag."""
return sorted(_PRESET_PROFILES.keys())
def GetPresetX509Parameters(profile_name):
"""Parses the profile name string into the corresponding API X509Parameters.
Args:
profile_name: The preset profile name.
Returns:
An X509Parameters object.
"""
if profile_name not in _PRESET_PROFILES:
raise exceptions.InvalidArgumentException(
'--use-preset-profile',
'The preset profile that was specified does not exist.')
messages = privateca_base.GetMessagesModule('v1')
return messages_util.DictToMessageWithErrorCheck(
_PRESET_PROFILES[profile_name], messages.X509Parameters)

View File

@@ -0,0 +1,340 @@
# -*- coding: utf-8 -*- #
# Copyright 2020 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.
"""Helpers for parsing resource arguments."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.api_lib.privateca import base
from googlecloudsdk.api_lib.privateca import locations
from googlecloudsdk.calliope import exceptions
from googlecloudsdk.calliope.concepts import concepts
from googlecloudsdk.calliope.concepts import deps
from googlecloudsdk.calliope.concepts import handlers
from googlecloudsdk.calliope.concepts import util
from googlecloudsdk.command_lib.kms import resource_args as kms_args
from googlecloudsdk.command_lib.privateca import completers as privateca_completers
from googlecloudsdk.command_lib.privateca import exceptions as privateca_exceptions
from googlecloudsdk.command_lib.util.concepts import concept_parsers
from googlecloudsdk.core import properties
import six
# Flag fallthroughs that rely on properties.
LOCATION_PROPERTY_FALLTHROUGH = deps.PropertyFallthrough(
properties.VALUES.privateca.location
)
PROJECT_PROPERTY_FALLTHROUGH = deps.PropertyFallthrough(
properties.VALUES.core.project
)
def CertificateTemplateAttributeConfig():
# TODO(b/186143764): GA Autocompleters
return concepts.ResourceParameterAttributeConfig(name='certificate template')
def CaPoolAttributeConfig(display_name='pool', fallthroughs=None):
# TODO(b/186143764): GA Autocompleters
return concepts.ResourceParameterAttributeConfig(
name=display_name,
help_text='The parent CA Pool of the {resource}.',
fallthroughs=fallthroughs or [],
)
def CertAttributeConfig(fallthroughs=None):
# TODO(b/186143764): GA Autocompleters
# Certificate is always an anchor attribute so help_text is unused.
return concepts.ResourceParameterAttributeConfig(
name='certificate', fallthroughs=fallthroughs or []
)
def CertAuthorityAttributeConfig(
arg_name='certificate_authority', fallthroughs=None
):
fallthroughs = fallthroughs or []
return concepts.ResourceParameterAttributeConfig(
name=arg_name,
help_text='The issuing certificate authority of the {resource}.',
fallthroughs=fallthroughs,
)
def LocationAttributeConfig(arg_name='location', fallthroughs=None):
fallthroughs = fallthroughs or [LOCATION_PROPERTY_FALLTHROUGH]
return concepts.ResourceParameterAttributeConfig(
name=arg_name,
help_text='The location of the {resource}.',
completer=privateca_completers.LocationsCompleter,
fallthroughs=fallthroughs,
)
def ProjectAttributeConfig(arg_name='project', fallthroughs=None):
"""DO NOT USE THIS for most flags.
This config is only useful when you want to provide an explicit project
fallthrough. For most cases, prefer concepts.DEFAULT_PROJECT_ATTRIBUTE_CONFIG.
Args:
arg_name: Name of the flag used to specify this attribute. Defaults to
'project'.
fallthroughs: List of deps.Fallthrough objects to provide project values.
Returns:
A concepts.ResourceParameterAttributeConfig for a project.
"""
return concepts.ResourceParameterAttributeConfig(
name=arg_name,
help_text='The project containing the {resource}.',
fallthroughs=fallthroughs or [],
)
def CreateKmsKeyVersionResourceSpec():
"""Creates a resource spec for a KMS CryptoKeyVersion.
Defaults to the location and project of the CA, specified through flags or
properties.
Returns:
A concepts.ResourceSpec for a CryptoKeyVersion.
"""
return concepts.ResourceSpec(
'cloudkms.projects.locations.keyRings.cryptoKeys.cryptoKeyVersions',
resource_name='key version',
cryptoKeyVersionsId=kms_args.KeyVersionAttributeConfig(kms_prefix=True),
cryptoKeysId=kms_args.KeyAttributeConfig(kms_prefix=True),
keyRingsId=kms_args.KeyringAttributeConfig(kms_prefix=True),
locationsId=LocationAttributeConfig(
'kms-location',
[deps.ArgFallthrough('location'), LOCATION_PROPERTY_FALLTHROUGH],
),
projectsId=ProjectAttributeConfig(
'kms-project',
[deps.ArgFallthrough('project'), PROJECT_PROPERTY_FALLTHROUGH],
),
)
def CreateCertAuthorityResourceSpec(
display_name,
certificate_authority_attribute='certificate_authority',
location_attribute='location',
location_fallthroughs=None,
pool_id_fallthroughs=None,
ca_id_fallthroughs=None,
):
# TODO(b/186143764): GA Autocompleters
return concepts.ResourceSpec(
'privateca.projects.locations.caPools.certificateAuthorities',
api_version='v1',
# This will be formatted and used as {resource} in the help text.
resource_name=display_name,
certificateAuthoritiesId=CertAuthorityAttributeConfig(
certificate_authority_attribute, fallthroughs=ca_id_fallthroughs
),
caPoolsId=CaPoolAttributeConfig(fallthroughs=pool_id_fallthroughs),
locationsId=LocationAttributeConfig(
location_attribute, fallthroughs=location_fallthroughs
),
projectsId=concepts.DEFAULT_PROJECT_ATTRIBUTE_CONFIG,
disable_auto_completers=True,
)
def CreateCaPoolResourceSpec(
display_name,
location_attribute='location',
pool_id_fallthroughs=None,
location_fallthroughs=None,
):
# TODO(b/186143764): GA Autocompleters
return concepts.ResourceSpec(
'privateca.projects.locations.caPools',
api_version='v1',
# This will be formatted and used as {resource} in the help text.
resource_name=display_name,
caPoolsId=CaPoolAttributeConfig(fallthroughs=pool_id_fallthroughs),
locationsId=LocationAttributeConfig(
location_attribute, fallthroughs=location_fallthroughs
),
projectsId=concepts.DEFAULT_PROJECT_ATTRIBUTE_CONFIG,
disable_auto_completers=True,
)
def CreateCertResourceSpec(display_name, id_fallthroughs=None):
return concepts.ResourceSpec(
'privateca.projects.locations.caPools.certificates',
# This will be formatted and used as {resource} in the help text.
api_version='v1',
resource_name=display_name,
certificatesId=CertAttributeConfig(fallthroughs=id_fallthroughs or []),
caPoolsId=CaPoolAttributeConfig('issuer-pool'),
locationsId=LocationAttributeConfig('issuer-location'),
projectsId=concepts.DEFAULT_PROJECT_ATTRIBUTE_CONFIG,
disable_auto_completers=False,
)
def CreateCertificateTemplateResourceSpec(display_name):
# TODO(b/186143764): GA Autocompleters
return concepts.ResourceSpec(
'privateca.projects.locations.certificateTemplates',
api_version='v1',
# This will be formatted and used as {resource} in the help text.
resource_name=display_name,
certificateTemplatesId=CertificateTemplateAttributeConfig(),
locationsId=LocationAttributeConfig(),
projectsId=concepts.DEFAULT_PROJECT_ATTRIBUTE_CONFIG,
disable_auto_completers=True,
)
def AddCertAuthorityPositionalResourceArg(parser, verb):
"""Add a positional resource argument for a GA Certificate Authority.
NOTE: Must be used only if it's the only resource arg in the command.
Args:
parser: the parser for the command.
verb: str, the verb to describe the resource, such as 'to update'.
"""
arg_name = 'CERTIFICATE_AUTHORITY'
concept_parsers.ConceptParser.ForResource(
arg_name,
CreateCertAuthorityResourceSpec(arg_name),
'The certificate authority {}.'.format(verb),
required=True,
).AddToParser(parser)
def AddCaPoolPositionalResourceArg(parser, verb):
"""Add a positional resource argument for a CA Pool.
NOTE: Must be used only if it's the only resource arg in the command.
Args:
parser: the parser for the command.
verb: str, the verb to describe the resource, such as 'to update'.
"""
arg_name = 'CA_POOL'
concept_parsers.ConceptParser.ForResource(
arg_name,
CreateCaPoolResourceSpec(arg_name),
'The ca pool {}.'.format(verb),
required=True,
).AddToParser(parser)
def AddCertPositionalResourceArg(parser, verb):
"""Add a positional resource argument for a GA Certificate.
NOTE: Must be used only if it's the only resource arg in the command.
Args:
parser: the parser for the command.
verb: str, the verb to describe the resource, such as 'to update'.
"""
arg_name = 'CERTIFICATE'
concept_parsers.ConceptParser.ForResource(
arg_name,
CreateCertResourceSpec(arg_name),
'The certificate {}.'.format(verb),
required=True,
).AddToParser(parser)
def AddCertificateTemplatePositionalResourceArg(parser, verb):
"""Add a positional resource argument for a certificate template.
NOTE: Must be used only if it's the only resource arg in the command.
Args:
parser: the parser for the command.
verb: str, the verb to describe the resource, such as 'to update'.
"""
arg_name = 'CERTIFICATE_TEMPLATE'
concept_parsers.ConceptParser.ForResource(
arg_name,
CreateCertificateTemplateResourceSpec(arg_name),
'The template {}.'.format(verb),
required=True,
).AddToParser(parser)
# Resource validation.
def ValidateResourceLocation(resource_ref, arg_name, version='v1'):
"""Raises an exception if the given resource is in an unsupported location."""
supported_locations = locations.GetSupportedLocations(version=version)
if resource_ref.locationsId not in supported_locations:
raise exceptions.InvalidArgumentException(
arg_name,
'Resource is in an unsupported location. Supported locations are: {}.'
.format(', '.join(sorted(supported_locations))),
)
def CheckExpectedCAType(expected_type, ca, version='v1'):
"""Raises an exception if the Certificate Authority type is not expected_type.
Args:
expected_type: The expected type.
ca: The ca object to check.
version: The version of the API to check against.
"""
ca_type_enum = base.GetMessagesModule(
api_version=version
).CertificateAuthority.TypeValueValuesEnum
if expected_type == ca_type_enum.SUBORDINATE and ca.type != expected_type:
raise privateca_exceptions.InvalidCertificateAuthorityTypeError(
'Cannot perform subordinates command on Root CA. Please use the'
' `privateca roots` command group instead.'
)
elif expected_type == ca_type_enum.SELF_SIGNED and ca.type != expected_type:
raise privateca_exceptions.InvalidCertificateAuthorityTypeError(
'Cannot perform roots command on Subordinate CA. Please use the'
' `privateca subordinates` command group instead.'
)
def ValidateResourceIsCompleteIfSpecified(args, resource_arg_name):
"""Raises a ParseError if the given resource_arg_name is partially specified."""
if not hasattr(args.CONCEPTS, resource_arg_name):
return
concept_info = args.CONCEPTS.ArgNameToConceptInfo(resource_arg_name)
associated_args = [
util.NamespaceFormat(arg)
for arg in concept_info.attribute_to_args_map.values()
]
# If none of the relevant args are specified, we're good.
if not [arg for arg in associated_args if args.IsSpecified(arg)]:
return
try:
# Re-parse this concept, but treat it as required even if it originally
# wasn't. This will trigger a meaningful user error if it's underspecified.
concept_info.ClearCache()
concept_info.allow_empty = False
concept_info.Parse(args)
except concepts.InitializationError as e:
raise handlers.ParseError(resource_arg_name, six.text_type(e))

View File

@@ -0,0 +1,83 @@
project:
name: project
collection: privateca.projects
attributes:
- &project
parameter_name: projectsId
attribute_name: project
help: The project ID.
property: core/project
disable_auto_completers: true
location:
name: location
collection: privateca.projects.locations
attributes:
- *project
- &location
parameter_name: locationsId
attribute_name: location
help: The location of the {resource}.
property: privateca/location
disable_auto_completers: true
certificate_template:
name: Certificate Template
collection: privateca.projects.locations.certificateTemplates
request_id_field: certificate_template.name
attributes:
- *project
- *location
- &certificate_template
parameter_name: certificateTemplatesId
attribute_name: certificate_template
help: The ID of the certificate template.
disable_auto_completers: true
ca_pool:
name: CA Pool
collection: privateca.projects.locations.caPools
request_id_field: caPool.name
attributes:
- *project
- *location
- &ca_pool
parameter_name: caPoolsId
attribute_name: pool
help: The ID of the CA Pool.
disable_auto_completers: true
cert_authority:
name: CERTIFICATE_AUTHORITY
collection: privateca.projects.locations.caPools.certificateAuthorities
request_id_field: certificateAuthority.name
attributes:
- *project
- *location
- *ca_pool
- &cert_authority
parameter_name: certificateAuthoritiesId
attribute_name: certificate_authority
help: The ID of the certificate authority.
disable_auto_completers: true
cert:
name: CERTIFICATE
collection: privateca.projects.locations.caPools.certificates
request_id_field: certificate.name
attributes:
- *project
- &cert_issuer_location
parameter_name: locationsId
attribute_name: issuer-location
help: The location of the {resource}.
property: privateca/location
- &issuer_pool
parameter_name: caPoolsId
attribute_name: issuer-pool
help: The ID of the issuing CA Pool.
- &cert
parameter_name: certificatesId
attribute_name: certificate
help: The ID of the certificate.
disable_auto_completers: true

View File

@@ -0,0 +1,30 @@
# -*- coding: utf-8 -*- #
# Copyright 2020 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.
"""Helpers for processing API responses."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.core import log
def GetFieldAndLogUnreachable(message, attribute):
"""Response callback to log unreachable while generating fields of the message."""
if message.unreachable:
log.warning(
'The following locations were fully or partially unreachable: {}.'
.format(', '.join(message.unreachable)))
return getattr(message, attribute)

View File

@@ -0,0 +1,64 @@
# -*- coding: utf-8 -*- #
# Copyright 2020 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.
"""Helpers for dealing with storage buckets."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.api_lib.storage import storage_api
from googlecloudsdk.api_lib.storage import storage_util
from googlecloudsdk.calliope import exceptions
from googlecloudsdk.core import log
def _BucketAllowsPublicObjectReads(bucket):
return any([acl.entity.lower() == 'allusers' and acl.role.lower() == 'reader'
for acl in bucket.defaultObjectAcl])
def ValidateBucketForCertificateAuthority(bucket_name):
"""Validates that a user-specified bucket can be used with a Private CA.
Args:
bucket_name: The name of the GCS bucket to validate.
Returns:
A BucketReference wrapping the given bucket name.
Raises:
InvalidArgumentException: when the given bucket can't be used with a CA.
"""
messages = storage_util.GetMessages()
client = storage_api.StorageClient(messages=messages)
try:
bucket = client.GetBucket(
bucket_name,
messages.StorageBucketsGetRequest.ProjectionValueValuesEnum.full)
if not _BucketAllowsPublicObjectReads(bucket):
# Show a warning but don't fail, since this could be intentional.
log.warning(
'The specified bucket does not publicly expose new objects by '
'default, so some clients may not be able to access the CA '
'certificate or CRLs. For more details, see '
'https://cloud.google.com/storage/docs/access-control/making-data-public'
)
return storage_util.BucketReference(bucket_name)
except storage_api.BucketNotFoundError:
raise exceptions.InvalidArgumentException(
'gcs-bucket', 'The given bucket does not exist.')

View File

@@ -0,0 +1,59 @@
# -*- coding: utf-8 -*- #
# Copyright 2020 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.
"""Helpers for dealing with text."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from dateutil import tz
from googlecloudsdk.core.util import times
def SnakeCaseToCamelCase(name):
words = name.split('_')
return words[0].lower() + ''.join(
[w[0].upper() + w[1:].lower() for w in words[1:]])
def ToSnakeCaseDict(dictionary):
"""Recursively convert all keys in nested dictionaries to snakeCase."""
new_dict = {}
for key, val in dictionary.items():
snaked_key = SnakeCaseToCamelCase(key)
if isinstance(val, dict):
new_dict[snaked_key] = ToSnakeCaseDict(val)
else:
new_dict[snaked_key] = val
return new_dict
def TransformNotBeforeTime(subject_description):
"""Use this function in a display transform to truncate anything smaller than minutes from ISO8601 timestamp."""
if subject_description and 'notBeforeTime' in subject_description:
return times.ParseDateTime(
subject_description.get('notBeforeTime')).astimezone(
tz.tzutc()).strftime('%Y-%m-%dT%H:%MZ')
return ''
def TransformNotAfterTime(subject_description):
"""Use this function in a display transform to truncate anything smaller than minutes from ISO8601 timestamp."""
if subject_description and 'notAfterTime' in subject_description:
return times.ParseDateTime(
subject_description.get('notAfterTime')).astimezone(
tz.tzutc()).strftime('%Y-%m-%dT%H:%MZ')
return ''

View File

@@ -0,0 +1,147 @@
# -*- coding: utf-8 -*- #
# Copyright 2023 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.
"""Helpers for update commands in GA."""
from __future__ import absolute_import
from __future__ import division
from __future__ import unicode_literals
from googlecloudsdk.api_lib.privateca import base as privateca_base
from googlecloudsdk.calliope import exceptions
from googlecloudsdk.command_lib.privateca import exceptions as privateca_exceptions
from googlecloudsdk.command_lib.privateca import flags
from googlecloudsdk.command_lib.privateca import pem_utils
from googlecloudsdk.command_lib.util.args import labels_util
from googlecloudsdk.core.util import files
def _ParsePemChainFromFile(pem_chain_file):
"""Parses a pem chain from a file.
Args:
pem_chain_file: file containing the pem_chain.
Returns:
The string list of certs in the chain.
"""
try:
pem_chain_input = files.ReadFileContents(pem_chain_file)
return pem_utils.ValidateAndParsePemChain(pem_chain_input)
except (files.Error, OSError, IOError):
raise exceptions.InvalidArgumentException(
'pem-chain',
"Could not read provided PEM chain file '{}'.".format(pem_chain_file),
)
def UpdateCAFromArgs(args, current_labels):
"""Creates a CA object and update mask from CA update flags.
Requires that args has 'pem-chain' and update labels flags registered.
Args:
args: The parser that contains the flag values.
current_labels: The current set of labels for the CA.
Returns:
A tuple with the CA object to update with and the list of strings
representing the update mask, respectively.
"""
messages = privateca_base.GetMessagesModule(api_version='v1')
ca_to_update = messages.CertificateAuthority()
update_mask = []
if args.IsKnownAndSpecified('pem_chain'):
ca_to_update.subordinateConfig = messages.SubordinateConfig(
pemIssuerChain=messages.SubordinateConfigChain(
pemCertificates=_ParsePemChainFromFile(args.pem_chain)
)
)
update_mask.append('subordinate_config')
labels_diff = labels_util.Diff.FromUpdateArgs(args)
labels_update = labels_diff.Apply(
messages.CertificateAuthority.LabelsValue, current_labels
)
if labels_update.needs_update:
ca_to_update.labels = labels_update.labels
update_mask.append('labels')
if not update_mask:
raise privateca_exceptions.NoUpdateExceptions(
'No updates found for the requested CA.'
)
return ca_to_update, update_mask
def UpdateCaPoolFromArgs(args, current_labels):
"""Creates a CA pool object and update mask from CA pool update flags.
Requires that args has 'publish-crl', 'publish-ca-cert', 'encryption-key' and
update labels flags registered.
Args:
args: The parser that contains the flag values.
current_labels: The current set of labels for the CA pool.
Returns:
A tuple with the CA pool object to update with and the list of strings
representing the update mask, respectively.
"""
messages = privateca_base.GetMessagesModule('v1')
pool_to_update = messages.CaPool()
update_mask = []
if (
args.IsSpecified('publish_crl')
or args.IsSpecified('publish_ca_cert')
or args.IsSpecified('publishing_encoding_format')
):
pool_to_update.publishingOptions = messages.PublishingOptions()
if args.IsSpecified('publish_crl'):
pool_to_update.publishingOptions.publishCrl = args.publish_crl
update_mask.append('publishing_options.publish_crl')
if args.IsSpecified('publish_ca_cert'):
pool_to_update.publishingOptions.publishCaCert = args.publish_ca_cert
update_mask.append('publishing_options.publish_ca_cert')
if args.IsSpecified('publishing_encoding_format'):
pool_to_update.publishingOptions.encodingFormat = (
flags.ParseEncodingFormatFlag(args)
)
update_mask.append('publishing_options.encoding_format')
if args.IsSpecified('encryption_key'):
pool_to_update.encryptionSpec = messages.EncryptionSpec(
cloudKmsKey=args.encryption_key
)
update_mask.append('encryption_spec')
labels_diff = labels_util.Diff.FromUpdateArgs(args)
labels_update = labels_diff.Apply(messages.CaPool.LabelsValue, current_labels)
if labels_update.needs_update:
pool_to_update.labels = labels_update.labels
update_mask.append('labels')
if args.IsSpecified('issuance_policy'):
pool_to_update.issuancePolicy = flags.ParseIssuancePolicy(args)
update_mask.append('issuance_policy')
if not update_mask:
raise privateca_exceptions.NoUpdateExceptions(
'No updates found for the requested CA pool.'
)
return pool_to_update, update_mask