429 lines
13 KiB
Python
429 lines
13 KiB
Python
# -*- 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.
|
|
"""Command line processing utilities for access levels."""
|
|
|
|
from __future__ import absolute_import
|
|
from __future__ import division
|
|
from __future__ import unicode_literals
|
|
|
|
from apitools.base.py import encoding
|
|
|
|
from googlecloudsdk.api_lib.accesscontextmanager import util
|
|
from googlecloudsdk.api_lib.util import waiter
|
|
from googlecloudsdk.calliope import base
|
|
from googlecloudsdk.calliope.concepts import concepts
|
|
from googlecloudsdk.command_lib.accesscontextmanager import common
|
|
from googlecloudsdk.command_lib.accesscontextmanager import policies
|
|
from googlecloudsdk.command_lib.util.apis import arg_utils
|
|
from googlecloudsdk.command_lib.util.concepts import concept_parsers
|
|
from googlecloudsdk.core import exceptions
|
|
from googlecloudsdk.core import resources
|
|
from googlecloudsdk.core import yaml
|
|
import six
|
|
|
|
COLLECTION = 'accesscontextmanager.accessPolicies.accessLevels'
|
|
|
|
|
|
_INVALID_FORMAT_ERROR = """
|
|
Invalid format: {}
|
|
|
|
The valid fields for the YAML objects in this file type are [{}].
|
|
|
|
For an access level condition file, an example of the YAML-formatted list of conditions will look like:
|
|
|
|
- ipSubnetworks:
|
|
- 162.222.181.197/24
|
|
- 2001:db8::/48
|
|
- members:
|
|
- user:user@example.com
|
|
|
|
For access levels file, an example of the YAML-formatted list of access levels will look like:
|
|
|
|
- name: accessPolicies/my_policy/accessLevels/my_level
|
|
title: My Level
|
|
description: Level for foo.
|
|
basic:
|
|
combiningFunction: AND
|
|
conditions:
|
|
- ipSubnetworks:
|
|
- 192.168.100.14/24
|
|
- 2001:db8::/48
|
|
- members:
|
|
- user1:user1@example.com
|
|
"""
|
|
|
|
|
|
class ParseResponseError(exceptions.Error):
|
|
|
|
def __init__(self, reason):
|
|
super(ParseResponseError,
|
|
self).__init__('Issue parsing response: {}'.format(reason))
|
|
|
|
|
|
class ParseError(exceptions.Error):
|
|
|
|
def __init__(self, path, reason):
|
|
super(ParseError,
|
|
self).__init__('Issue parsing file [{}]: {}'.format(path, reason))
|
|
|
|
|
|
class InvalidFormatError(ParseError):
|
|
|
|
def __init__(self, path, reason, message_class):
|
|
valid_fields = [f.name for f in message_class.all_fields()]
|
|
super(InvalidFormatError,
|
|
self).__init__(path, (_INVALID_FORMAT_ERROR).format(
|
|
reason, ', '.join(valid_fields)))
|
|
|
|
|
|
def _LoadData(path):
|
|
try:
|
|
return yaml.load_path(path)
|
|
except yaml.FileLoadError as err:
|
|
raise ParseError(path, 'Problem loading file: {}'.format(err))
|
|
except yaml.YAMLParseError as err:
|
|
raise ParseError(path, 'Problem parsing data as YAML: {}'.format(err))
|
|
|
|
|
|
def _ValidateAllBasicConditionFieldsRecognized(path, conditions):
|
|
unrecognized_fields = set()
|
|
for condition in conditions:
|
|
if condition.all_unrecognized_fields():
|
|
unrecognized_fields.update(condition.all_unrecognized_fields())
|
|
if unrecognized_fields:
|
|
raise InvalidFormatError(
|
|
path,
|
|
'Unrecognized fields: [{}]'.format(', '.join(unrecognized_fields)),
|
|
type(conditions[0]))
|
|
|
|
|
|
def _ValidateAllCustomFieldsRecognized(path, expr):
|
|
if expr.all_unrecognized_fields():
|
|
raise InvalidFormatError(
|
|
path, 'Unrecognized fields: [{}]'.format(', '.join(
|
|
expr.all_unrecognized_fields())), type(expr))
|
|
|
|
|
|
def _ValidateAllLevelFieldsRecognized(path, levels):
|
|
unrecognized_fields = set()
|
|
for level in levels:
|
|
if level.all_unrecognized_fields():
|
|
unrecognized_fields.update(level.all_unrecognized_fields())
|
|
if unrecognized_fields:
|
|
raise InvalidFormatError(
|
|
path,
|
|
'Unrecognized fields: [{}]'.format(', '.join(unrecognized_fields)),
|
|
type(levels[0]))
|
|
|
|
|
|
def ParseReplaceAccessLevelsResponse(api_version):
|
|
"""Wrapper around ParseReplaceAccessLevelsResponse to accept api version."""
|
|
|
|
def VersionedParseReplaceAccessLevelsResponse(lro, unused_args):
|
|
"""Parse the Long Running Operation response of the ReplaceAccessLevels call.
|
|
|
|
Args:
|
|
lro: Long Running Operation response of ReplaceAccessLevels.
|
|
unused_args: not used.
|
|
|
|
Returns:
|
|
The replacement Access Levels created by the ReplaceAccessLevels call.
|
|
|
|
Raises:
|
|
ParseResponseError: if the response could not be parsed into the proper
|
|
object.
|
|
"""
|
|
client = util.GetClient(version=api_version)
|
|
operation_ref = resources.REGISTRY.Parse(
|
|
lro.name, collection='accesscontextmanager.operations')
|
|
poller = common.BulkAPIOperationPoller(client.accessPolicies_accessLevels,
|
|
client.operations, operation_ref)
|
|
|
|
return waiter.WaitFor(
|
|
poller, operation_ref,
|
|
'Waiting for Replace Access Levels operation [{}]'.format(
|
|
operation_ref.Name()))
|
|
|
|
return VersionedParseReplaceAccessLevelsResponse
|
|
|
|
|
|
def ParseBasicLevelConditions(api_version):
|
|
"""Wrapper around ParseCustomLevel to accept api version."""
|
|
|
|
def VersionedParseBasicLevelConditions(path):
|
|
"""Parse a YAML representation of basic level conditions.
|
|
|
|
Args:
|
|
path: str, path to file containing basic level conditions
|
|
|
|
Returns:
|
|
list of Condition objects.
|
|
|
|
Raises:
|
|
ParseError: if the file could not be read into the proper object
|
|
"""
|
|
|
|
data = yaml.load_path(path)
|
|
if not data:
|
|
raise ParseError(path, 'File is empty')
|
|
|
|
messages = util.GetMessages(version=api_version)
|
|
message_class = messages.Condition
|
|
try:
|
|
conditions = [encoding.DictToMessage(c, message_class) for c in data]
|
|
except Exception as err:
|
|
raise InvalidFormatError(path, six.text_type(err), message_class)
|
|
|
|
_ValidateAllBasicConditionFieldsRecognized(path, conditions)
|
|
|
|
return conditions
|
|
|
|
return VersionedParseBasicLevelConditions
|
|
|
|
|
|
def ParseCustomLevel(api_version):
|
|
"""Wrapper around ParseCustomLevel to accept api version."""
|
|
|
|
def VersionedParseCustomLevel(path):
|
|
"""Parse a YAML representation of custom level conditions.
|
|
|
|
Args:
|
|
path: str, path to file containing custom level expression
|
|
|
|
Returns:
|
|
string of CEL expression.
|
|
|
|
Raises:
|
|
ParseError: if the file could not be read into the proper object
|
|
"""
|
|
|
|
data = yaml.load_path(path)
|
|
if not data:
|
|
raise ParseError(path, 'File is empty')
|
|
|
|
messages = util.GetMessages(version=api_version)
|
|
message_class = messages.Expr
|
|
try:
|
|
expr = encoding.DictToMessage(data, message_class)
|
|
except Exception as err:
|
|
raise InvalidFormatError(path, six.text_type(err), message_class)
|
|
|
|
_ValidateAllCustomFieldsRecognized(path, expr)
|
|
return expr
|
|
|
|
return VersionedParseCustomLevel
|
|
|
|
|
|
def ParseAccessLevels(api_version):
|
|
"""Wrapper around ParseAccessLevels to accept api version."""
|
|
|
|
def VersionedParseAccessLevels(path):
|
|
"""Parse a YAML representation of a list of Access Levels with basic/custom level conditions.
|
|
|
|
Args:
|
|
path: str, path to file containing basic/custom access levels
|
|
|
|
Returns:
|
|
list of Access Level objects.
|
|
|
|
Raises:
|
|
ParseError: if the file could not be read into the proper object
|
|
"""
|
|
|
|
data = yaml.load_path(path)
|
|
if not data:
|
|
raise ParseError(path, 'File is empty')
|
|
|
|
messages = util.GetMessages(version=api_version)
|
|
message_class = messages.AccessLevel
|
|
try:
|
|
levels = [encoding.DictToMessage(c, message_class) for c in data]
|
|
except Exception as err:
|
|
raise InvalidFormatError(path, six.text_type(err), message_class)
|
|
|
|
_ValidateAllLevelFieldsRecognized(path, levels)
|
|
return levels
|
|
|
|
return VersionedParseAccessLevels
|
|
|
|
|
|
def ClearCombiningFunctionUnlessBasicSpecSet(ref, args, req=None):
|
|
"""Clear basic field (and default combine function) if spec not provided."""
|
|
del ref # unused
|
|
if req is None:
|
|
return req
|
|
|
|
if not args.IsSpecified('basic_level_spec'):
|
|
req.accessLevel.reset('basic')
|
|
|
|
return req
|
|
|
|
|
|
def GetAttributeConfig():
|
|
return concepts.ResourceParameterAttributeConfig(
|
|
name='level', help_text='The ID of the access level.')
|
|
|
|
|
|
def GetResourceSpec():
|
|
return concepts.ResourceSpec(
|
|
'accesscontextmanager.accessPolicies.accessLevels',
|
|
resource_name='level',
|
|
accessPoliciesId=policies.GetAttributeConfig(),
|
|
accessLevelsId=GetAttributeConfig())
|
|
|
|
|
|
def AddResourceArg(parser, verb):
|
|
"""Add a resource argument for an access level.
|
|
|
|
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'.
|
|
"""
|
|
concept_parsers.ConceptParser.ForResource(
|
|
'level',
|
|
GetResourceSpec(),
|
|
'The access level {}.'.format(verb),
|
|
required=True).AddToParser(parser)
|
|
|
|
|
|
def AddResourceFlagArg(parser, verb):
|
|
"""Add a resource argument for an access level.
|
|
|
|
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'.
|
|
"""
|
|
concept_parsers.ConceptParser.ForResource(
|
|
'--level',
|
|
GetResourceSpec(),
|
|
'The access level {}.'.format(verb),
|
|
required=True).AddToParser(parser)
|
|
|
|
|
|
def GetCombineFunctionEnumMapper(api_version=None):
|
|
return arg_utils.ChoiceEnumMapper(
|
|
'--combine-function',
|
|
util.GetMessages(
|
|
version=api_version).BasicLevel.CombiningFunctionValueValuesEnum,
|
|
custom_mappings={
|
|
'AND': 'and',
|
|
'OR': 'or'
|
|
},
|
|
required=False,
|
|
help_str='For a basic level, determines how conditions are combined.',
|
|
)
|
|
|
|
|
|
def AddLevelArgs(parser):
|
|
"""Add common args for level create/update commands."""
|
|
args = [
|
|
common.GetDescriptionArg('access level'),
|
|
common.GetTitleArg('access level'),
|
|
]
|
|
for arg in args:
|
|
arg.AddToParser(parser)
|
|
|
|
|
|
def AddBasicSpecArgs(parser, api_version):
|
|
"""Add args for basic spec (with no custom spec)."""
|
|
basic_level_help_text = (
|
|
'Path to a file containing a list of basic access level conditions.\n\n'
|
|
'An access level condition file is a YAML-formatted list of conditions, '
|
|
'which are YAML objects representing a Condition as described in the API '
|
|
'reference. For example:\n\n'
|
|
' ```\n'
|
|
' - ipSubnetworks:\n'
|
|
' - 162.222.181.197/24\n'
|
|
' - 2001:db8::/48\n'
|
|
' - members:\n'
|
|
' - user:user@example.com\n'
|
|
' ```')
|
|
basic_level_spec_arg = base.Argument(
|
|
'--basic-level-spec',
|
|
help=basic_level_help_text,
|
|
type=ParseBasicLevelConditions(api_version))
|
|
basic_level_combine_arg = GetCombineFunctionEnumMapper(
|
|
api_version=api_version).choice_arg
|
|
|
|
basic_level_spec_arg.AddToParser(parser)
|
|
basic_level_combine_arg.AddToParser(parser)
|
|
|
|
|
|
def AddBasicAndCustomSpecArgs(parser, api_version):
|
|
"""Add args for basic and custom specs (grouped together)."""
|
|
basic_level_help_text = (
|
|
'Path to a file containing a list of basic access level conditions.\n\n'
|
|
'An access level condition file is a YAML-formatted list of conditions,'
|
|
'which are YAML objects representing a Condition as described in the API '
|
|
'reference. For example:\n\n'
|
|
' ```\n'
|
|
' - ipSubnetworks:\n'
|
|
' - 162.222.181.197/24\n'
|
|
' - 2001:db8::/48\n'
|
|
' - members:\n'
|
|
' - user:user@example.com\n'
|
|
' ```')
|
|
custom_level_help_text = (
|
|
'Path to a file representing an expression for an access level.\n\n'
|
|
'The expression is in the Common Expression Langague (CEL) format.'
|
|
'For example:\n\n'
|
|
' ```\n'
|
|
' expression: "origin.region_code in [\'US\', \'CA\']"\n'
|
|
' ```')
|
|
|
|
basic_level_spec_arg = base.Argument(
|
|
'--basic-level-spec',
|
|
help=basic_level_help_text,
|
|
type=ParseBasicLevelConditions(api_version))
|
|
basic_level_combine_arg = GetCombineFunctionEnumMapper(
|
|
api_version=api_version).choice_arg
|
|
|
|
basic_level_spec_group = base.ArgumentGroup(help='Basic level specification.')
|
|
basic_level_spec_group.AddArgument(basic_level_spec_arg)
|
|
basic_level_spec_group.AddArgument(basic_level_combine_arg)
|
|
|
|
custom_level_spec_arg = base.Argument(
|
|
'--custom-level-spec',
|
|
help=custom_level_help_text,
|
|
type=ParseCustomLevel(api_version))
|
|
|
|
# Custom level spec group only consists of a single argument.
|
|
# This is done so help text between basic/custom specs is consistent.
|
|
custom_level_spec_group = base.ArgumentGroup(
|
|
help='Custom level specification.')
|
|
custom_level_spec_group.AddArgument(custom_level_spec_arg)
|
|
|
|
level_spec_group = base.ArgumentGroup(help='Level specification.', mutex=True)
|
|
|
|
level_spec_group.AddArgument(basic_level_spec_group)
|
|
level_spec_group.AddArgument(custom_level_spec_group)
|
|
|
|
level_spec_group.AddToParser(parser)
|
|
|
|
|
|
def AddLevelSpecArgs(parser, api_version=None, feature_mask=None):
|
|
"""Add arguments for in-file level specifications."""
|
|
if feature_mask is None:
|
|
feature_mask = {}
|
|
|
|
if feature_mask.get('custom_levels', False):
|
|
AddBasicAndCustomSpecArgs(parser, api_version)
|
|
else:
|
|
AddBasicSpecArgs(parser, api_version)
|