1037 lines
36 KiB
Python
1037 lines
36 KiB
Python
# -*- 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.
|
|
"""Command line processing utilities for cloud access bindings."""
|
|
|
|
from __future__ import absolute_import
|
|
from __future__ import division
|
|
from __future__ import unicode_literals
|
|
|
|
import re
|
|
|
|
from apitools.base.py import encoding
|
|
from googlecloudsdk.api_lib.accesscontextmanager import util
|
|
from googlecloudsdk.calliope import exceptions as calliope_exceptions
|
|
from googlecloudsdk.command_lib.accesscontextmanager import common
|
|
from googlecloudsdk.core import exceptions as core_exceptions
|
|
from googlecloudsdk.core import properties
|
|
from googlecloudsdk.core import resources
|
|
from googlecloudsdk.core.util import iso_duration
|
|
from googlecloudsdk.core.util import times
|
|
|
|
|
|
def AddUpdateMask(ref, args, req):
|
|
"""Hook to add update mask."""
|
|
del ref
|
|
update_mask = []
|
|
if args.IsKnownAndSpecified('level'):
|
|
update_mask.append('access_levels')
|
|
if args.IsKnownAndSpecified('dry_run_level'):
|
|
update_mask.append('dry_run_access_levels')
|
|
if args.IsKnownAndSpecified('session_length'):
|
|
update_mask.append('session_settings')
|
|
if args.IsKnownAndSpecified('binding_file'):
|
|
update_mask.append('scoped_access_settings')
|
|
|
|
if not update_mask:
|
|
raise calliope_exceptions.MinimumArgumentException(
|
|
['--level', '--dry_run_level', '--session-length', '--binding-file']
|
|
)
|
|
|
|
req.updateMask = ','.join(update_mask)
|
|
return req
|
|
|
|
|
|
def AddUpdateMaskAlpha(ref, args, req):
|
|
"""Hook to add update mask in Alpha track."""
|
|
del ref
|
|
update_mask = []
|
|
if args.IsKnownAndSpecified('level'):
|
|
update_mask.append('access_levels')
|
|
if args.IsKnownAndSpecified('dry_run_level'):
|
|
update_mask.append('dry_run_access_levels')
|
|
if args.IsKnownAndSpecified(
|
|
'restricted_client_application_client_ids'
|
|
) or args.IsKnownAndSpecified('restricted_client_application_names'):
|
|
update_mask.append('restricted_client_applications')
|
|
if args.IsKnownAndSpecified('session_length'):
|
|
update_mask.append('session_settings')
|
|
if args.IsKnownAndSpecified('binding_file'):
|
|
update_mask.append('scoped_access_settings')
|
|
|
|
if not update_mask:
|
|
raise calliope_exceptions.MinimumArgumentException([
|
|
'--level',
|
|
'--dry_run_level',
|
|
'--restricted_client_application_names',
|
|
'--restricted_client_application_client_ids',
|
|
'--session-length',
|
|
'--binding-file',
|
|
])
|
|
|
|
req.updateMask = ','.join(update_mask)
|
|
return req
|
|
|
|
|
|
def ProcessOrganization(ref, args, req):
|
|
"""Hook to process organization input."""
|
|
del ref, args
|
|
if req.parent is not None:
|
|
return req
|
|
|
|
org = properties.VALUES.access_context_manager.organization.Get()
|
|
if org is None:
|
|
raise calliope_exceptions.RequiredArgumentException(
|
|
'--organization',
|
|
'The attribute can be set in the following ways: \n'
|
|
+ '- provide the argument `--organization` on the command line \n'
|
|
+ '- set the property `access_context_manager/organization`',
|
|
)
|
|
|
|
org_ref = resources.REGISTRY.Parse(
|
|
org, collection='accesscontextmanager.organizations'
|
|
)
|
|
req.parent = org_ref.RelativeName()
|
|
return req
|
|
|
|
|
|
def ProcessRestrictedClientApplicationsAlpha(unused_ref, args, req):
|
|
"""Hook to process restricted client applications input in Alpha track."""
|
|
del unused_ref
|
|
return _ProcessRestrictedClientApplications(args, req, version='v1alpha')
|
|
|
|
|
|
def _ProcessRestrictedClientApplications(args, req, version=None):
|
|
"""Process restricted client applications input for the given version."""
|
|
# Processing application client ids if available
|
|
if args.IsKnownAndSpecified('restricted_client_application_client_ids'):
|
|
client_ids = args.restricted_client_application_client_ids
|
|
restricted_client_application_refs = (
|
|
_MakeRestrictedClientApplicationsFromIdentifiers(
|
|
client_ids,
|
|
'restricted_client_application_client_ids',
|
|
version=version,
|
|
)
|
|
)
|
|
# req.gcpUserAccessBinding is None when no access levels are specified
|
|
# during update. Access Levels are optional when updating restricted client
|
|
# applications, but they are required when creating a new binding.
|
|
if req.gcpUserAccessBinding is None:
|
|
req.gcpUserAccessBinding = util.GetMessages(
|
|
version=version
|
|
).GcpUserAccessBinding()
|
|
for restricted_client_application_ref in restricted_client_application_refs:
|
|
req.gcpUserAccessBinding.restrictedClientApplications.append(
|
|
restricted_client_application_ref
|
|
)
|
|
# processing application names if available
|
|
if args.IsKnownAndSpecified('restricted_client_application_names'):
|
|
client_names = args.restricted_client_application_names
|
|
restricted_client_application_refs = (
|
|
_MakeRestrictedClientApplicationsFromIdentifiers(
|
|
client_names,
|
|
'restricted_client_application_names',
|
|
version=version,
|
|
)
|
|
)
|
|
# req.gcpUserAccessBinding is None when no access levels are specified
|
|
# during update. Access Levels are optional when updating restricted client
|
|
# applications, but they are required when creating a new binding.
|
|
if req.gcpUserAccessBinding is None:
|
|
req.gcpUserAccessBinding = util.GetMessages(
|
|
version=version
|
|
).GcpUserAccessBinding()
|
|
for restricted_client_application_ref in restricted_client_application_refs:
|
|
req.gcpUserAccessBinding.restrictedClientApplications.append(
|
|
restricted_client_application_ref
|
|
)
|
|
return req
|
|
|
|
|
|
def _MakeRestrictedClientApplicationsFromIdentifiers(
|
|
app_identifiers, arg_name, version=None
|
|
):
|
|
"""Parse restricted client applications and return their resource references."""
|
|
resource_refs = []
|
|
if app_identifiers is not None:
|
|
app_identifiers = [
|
|
# remove empty strings
|
|
identifier
|
|
for identifier in app_identifiers
|
|
if identifier
|
|
]
|
|
for app_identifier in app_identifiers:
|
|
if arg_name == 'restricted_client_application_client_ids':
|
|
try:
|
|
resource_refs.append(
|
|
util.GetMessages(version=version).Application(
|
|
clientId=app_identifier
|
|
)
|
|
)
|
|
except:
|
|
raise calliope_exceptions.InvalidArgumentException(
|
|
'--{}'.format('restricted_client_application_client_ids'),
|
|
'Unable to parse input. The input must be of type string[].',
|
|
)
|
|
elif arg_name == 'restricted_client_application_names':
|
|
try:
|
|
resource_refs.append(
|
|
util.GetMessages(version=version).Application(name=app_identifier)
|
|
)
|
|
except:
|
|
raise calliope_exceptions.InvalidArgumentException(
|
|
'--{}'.format('restricted_client_application_names'),
|
|
'Unable to parse input. The input must be of type string[].',
|
|
)
|
|
else:
|
|
raise calliope_exceptions.InvalidArgumentException(
|
|
'--{}'.format('arg_name'),
|
|
'The input is not valid for Restricted Client Applications.',
|
|
)
|
|
return resource_refs
|
|
|
|
|
|
def _ParseLevelRefs(req, param, is_dry_run):
|
|
"""Parse level strings and return their resource references."""
|
|
level_inputs = req.gcpUserAccessBinding.accessLevels
|
|
if is_dry_run:
|
|
level_inputs = req.gcpUserAccessBinding.dryRunAccessLevels
|
|
|
|
level_refs = []
|
|
level_inputs = [level_input for level_input in level_inputs if level_input]
|
|
if not level_inputs:
|
|
return level_refs
|
|
|
|
arg_name = '--dry_run_level' if is_dry_run else '--level'
|
|
|
|
for level_input in level_inputs:
|
|
try:
|
|
level_ref = resources.REGISTRY.Parse(
|
|
level_input,
|
|
params=param,
|
|
collection='accesscontextmanager.accessPolicies.accessLevels',
|
|
)
|
|
except:
|
|
raise calliope_exceptions.InvalidArgumentException(
|
|
'--{}'.format(arg_name),
|
|
'The input must be the full identifier for the access level, '
|
|
'such as `accessPolicies/123/accessLevels/abc`.',
|
|
)
|
|
level_refs.append(level_ref)
|
|
return level_refs
|
|
|
|
|
|
def ProcessLevels(ref, args, req):
|
|
"""Hook to format levels and validate all policies."""
|
|
del ref # Unused
|
|
policies_to_check = {}
|
|
|
|
param = {}
|
|
policy_ref = None
|
|
if args.IsKnownAndSpecified('policy'):
|
|
try:
|
|
policy_ref = resources.REGISTRY.Parse(
|
|
args.GetValue('policy'),
|
|
collection='accesscontextmanager.accessPolicies',
|
|
)
|
|
except:
|
|
raise calliope_exceptions.InvalidArgumentException(
|
|
'--policy',
|
|
'The input must be the full identifier for the access policy, '
|
|
'such as `123` or `accessPolicies/123.',
|
|
)
|
|
param = {'accessPoliciesId': policy_ref.Name()}
|
|
policies_to_check['--policy'] = policy_ref.RelativeName()
|
|
else:
|
|
del policy_ref
|
|
|
|
# Parse level and dry run level
|
|
level_refs = (
|
|
_ParseLevelRefs(req, param, is_dry_run=False)
|
|
if args.IsKnownAndSpecified('level')
|
|
else []
|
|
)
|
|
dry_run_level_refs = (
|
|
_ParseLevelRefs(req, param, is_dry_run=True)
|
|
if args.IsKnownAndSpecified('dry_run_level')
|
|
else []
|
|
)
|
|
|
|
# Validate all refs in each level ref belong to the same policy
|
|
level_parents = [x.Parent() for x in level_refs]
|
|
dry_run_level_parents = [x.Parent() for x in dry_run_level_refs]
|
|
if not all(x == level_parents[0] for x in level_parents):
|
|
raise ConflictPolicyException(['--level'])
|
|
if not all(x == dry_run_level_parents[0] for x in dry_run_level_parents):
|
|
raise ConflictPolicyException(['--dry-run-level'])
|
|
|
|
# Validate policies of level, dry run level and policy inputs are the same
|
|
if level_parents:
|
|
policies_to_check['--level'] = level_parents[0].RelativeName()
|
|
if dry_run_level_parents:
|
|
policies_to_check['--dry-run-level'] = dry_run_level_parents[
|
|
0
|
|
].RelativeName()
|
|
flags_to_complain = list(policies_to_check.keys())
|
|
flags_to_complain.sort() # Sort for test purpose.
|
|
policies_values = list(policies_to_check.values())
|
|
if not all(x == policies_values[0] for x in policies_values):
|
|
raise ConflictPolicyException(flags_to_complain)
|
|
|
|
# Set formatted level fields in the request
|
|
if level_refs:
|
|
req.gcpUserAccessBinding.accessLevels = [
|
|
x.RelativeName() for x in level_refs
|
|
]
|
|
if dry_run_level_refs:
|
|
req.gcpUserAccessBinding.dryRunAccessLevels = [
|
|
x.RelativeName() for x in dry_run_level_refs
|
|
]
|
|
return req
|
|
|
|
|
|
def ProcessSessionLength(string):
|
|
"""Process the session-length argument into an acceptable form for GCSL session settings."""
|
|
|
|
# If we receive the empty string then return a negative duration. This will
|
|
# signal to the request processor that sessionSettings should be cleared.
|
|
# This is primarily used for clearing bindings on calls to update, and is a
|
|
# no-op for calls to create.
|
|
|
|
duration = (
|
|
times.ParseDuration(string) if string else iso_duration.Duration(hours=-1)
|
|
)
|
|
|
|
# TODO(b/346781832)
|
|
if duration.total_seconds > iso_duration.Duration(days=1).total_seconds:
|
|
raise calliope_exceptions.InvalidArgumentException(
|
|
'--session-length',
|
|
'The session length cannot be greater than one day.',
|
|
)
|
|
# Format for Google protobuf Duration
|
|
return '{}s'.format(int(duration.total_seconds))
|
|
|
|
|
|
def ProcessSessionSettings(unused_ref, args, req):
|
|
"""Hook to process GCSL session settings.
|
|
|
|
When --session-length=0 make sure the sessionLengthEnabled is set to false.
|
|
|
|
Throw an error if --session-reauth-method or --use-oidc-max-age are set
|
|
without --session-length.
|
|
|
|
Args:
|
|
unused_ref: Unused
|
|
args: The command line arguments
|
|
req: The request object
|
|
|
|
Returns:
|
|
The modified request object.
|
|
|
|
Raises:
|
|
calliope_exceptions.InvalidArgumentException: If arguments are incorrectly
|
|
set.
|
|
"""
|
|
del unused_ref
|
|
if args.IsKnownAndSpecified('session_length'):
|
|
if args.IsKnownAndSpecified(
|
|
'restricted_client_application_client_ids'
|
|
) or args.IsKnownAndSpecified('restricted_client_application_names'):
|
|
raise calliope_exceptions.InvalidArgumentException(
|
|
'--session-length',
|
|
'Cannot set session length on restricted client applications. Use '
|
|
'scoped access settings.',
|
|
)
|
|
session_length = times.ParseDuration(
|
|
req.gcpUserAccessBinding.sessionSettings.sessionLength
|
|
).total_seconds
|
|
if session_length < 0: # Case where --session_length=''
|
|
req.gcpUserAccessBinding.sessionSettings = None
|
|
elif session_length == 0: # Case where we disable session
|
|
req.gcpUserAccessBinding.sessionSettings.sessionLengthEnabled = False
|
|
else: # Normal case
|
|
req.gcpUserAccessBinding.sessionSettings.sessionLengthEnabled = True
|
|
else:
|
|
if args.IsKnownAndSpecified('session_reauth_method'):
|
|
raise calliope_exceptions.InvalidArgumentException(
|
|
'--session_reauth_method',
|
|
'Cannot set --session_reauth_method without --session-length',
|
|
)
|
|
# Clear all default session settings from the request if --session-length is
|
|
# unspecified
|
|
req.gcpUserAccessBinding.sessionSettings = None
|
|
|
|
return req
|
|
|
|
|
|
def _CamelCase2SnakeCase(name):
|
|
s1 = re.compile('([a-z0-9])([A-Z])').sub(r'\1_\2', name)
|
|
return re.sub('_[A-Z]+', lambda m: m.group(0).lower(), s1)
|
|
|
|
|
|
def ProcessFilter(unused_ref, args, req):
|
|
"""Hook to process filter. Covert camel case to snake case."""
|
|
del unused_ref
|
|
if args.IsKnownAndSpecified('filter'):
|
|
# Only pass filter to handler if it contains principal
|
|
if 'principal' in args.filter:
|
|
filter_str = _CamelCase2SnakeCase(args.filter)
|
|
req.filter = filter_str
|
|
return req
|
|
|
|
|
|
class ConflictPolicyException(core_exceptions.Error):
|
|
"""For conflict policies from inputs."""
|
|
|
|
def __init__(self, parameter_names):
|
|
super(ConflictPolicyException, self).__init__(
|
|
'Invalid value for [{0}]: Ensure that the {0} resources are '
|
|
'all from the same policy.'.format(
|
|
', '.join(['{0}'.format(p) for p in parameter_names])
|
|
)
|
|
)
|
|
|
|
|
|
def _TryGetAccessLevelResources(
|
|
param, access_levels, field_name, error_message
|
|
):
|
|
"""Try to get the access level cloud resources that correspond to the `access levels`.
|
|
|
|
Args:
|
|
param: The parameters to pass to the resource registry
|
|
access_levels: The access levels to turn into cloud resources
|
|
field_name: The name of the field to use in the error message
|
|
error_message: The error message to use if the access levels cannot be
|
|
parsed
|
|
|
|
Returns:
|
|
The access level cloud resources that correspond to the `access levels`.
|
|
"""
|
|
access_level_resources = []
|
|
access_level_inputs = [
|
|
access_level for access_level in access_levels if access_level
|
|
]
|
|
|
|
for access_level_input in access_level_inputs:
|
|
try:
|
|
access_level_resources.append(
|
|
resources.REGISTRY.Parse(
|
|
access_level_input,
|
|
params=param,
|
|
collection='accesscontextmanager.accessPolicies.accessLevels',
|
|
)
|
|
)
|
|
except:
|
|
raise calliope_exceptions.InvalidArgumentException(
|
|
'--{}'.format(field_name),
|
|
error_message,
|
|
)
|
|
|
|
return access_level_resources
|
|
|
|
|
|
def _TryGetPolicyCloudResource(policy, field_name, error_message):
|
|
"""Try to get the policy cloud resource that corresponds to the `policy`.
|
|
|
|
Args:
|
|
policy: The policy to turn into a cloud resource
|
|
field_name: The name of the field to use in the error message
|
|
error_message: The error message to use if the policy cannot be parsed
|
|
|
|
Returns:
|
|
The policy cloud resource that corresponds to the `policy`.
|
|
"""
|
|
try:
|
|
return resources.REGISTRY.Parse(
|
|
policy,
|
|
collection='accesscontextmanager.accessPolicies',
|
|
)
|
|
except:
|
|
raise calliope_exceptions.InvalidArgumentException(
|
|
'--{}'.format(field_name), error_message
|
|
)
|
|
|
|
|
|
def _ProcessScopesInScopedAccessSettings(req):
|
|
"""Validates the scope in the scoped access settings."""
|
|
|
|
def _ValidateScopeInScopedAccessSettingsUniqueness(scoped_access_settings):
|
|
scopes = [str(x.scope) for x in scoped_access_settings]
|
|
if len(scopes) != len(set(scopes)):
|
|
raise calliope_exceptions.InvalidArgumentException(
|
|
'--binding-file',
|
|
'ScopedAccessSettings in the binding-file must be unique.',
|
|
)
|
|
|
|
def _IsClientScopeSet(client_scope):
|
|
if not client_scope:
|
|
return False
|
|
if not client_scope.restrictedClientApplication:
|
|
return False
|
|
restricted_client_application_dict = encoding.MessageToDict(
|
|
client_scope.restrictedClientApplication
|
|
)
|
|
if not restricted_client_application_dict:
|
|
return False
|
|
# Check for None or empty string
|
|
for key in restricted_client_application_dict.keys():
|
|
if not restricted_client_application_dict[key]:
|
|
return False
|
|
return True
|
|
|
|
def _ValidateScopeInScopedAccessSettingIsNotEmpty(scoped_access_setting):
|
|
if not scoped_access_setting.scope or not _IsClientScopeSet(
|
|
scoped_access_setting.scope.clientScope
|
|
):
|
|
raise calliope_exceptions.InvalidArgumentException(
|
|
'--binding-file',
|
|
'ScopedAccessSettings in the binding-file must have a scope.',
|
|
)
|
|
|
|
def _Start(req):
|
|
scoped_access_settings = req.gcpUserAccessBinding.scopedAccessSettings
|
|
_ValidateScopeInScopedAccessSettingsUniqueness(scoped_access_settings)
|
|
for scoped_access_setting in scoped_access_settings:
|
|
_ValidateScopeInScopedAccessSettingIsNotEmpty(scoped_access_setting)
|
|
|
|
_Start(req)
|
|
|
|
|
|
def _ProcessAccessSettingsInScopedAccessSettings(req):
|
|
"""Validates the access settings in the scoped access settings."""
|
|
|
|
def _IsAccessSettingsSet(access_settings):
|
|
if not access_settings:
|
|
return False
|
|
access_settings_dict = encoding.MessageToDict(access_settings)
|
|
if not access_settings_dict:
|
|
return False
|
|
# Check for None or empty arrays
|
|
for key in access_settings_dict.keys():
|
|
if not access_settings_dict[key]:
|
|
return False
|
|
return True
|
|
|
|
def _ValidateAccessSettingsInScopedAccessSettingAtLeastOneIsNotEmpty(
|
|
access_settings, dry_run_settings
|
|
):
|
|
if not _IsAccessSettingsSet(access_settings) and not _IsAccessSettingsSet(
|
|
dry_run_settings
|
|
):
|
|
raise calliope_exceptions.InvalidArgumentException(
|
|
'--binding-file',
|
|
'ScopedAccessSettings in the binding-file must have at least one of'
|
|
' activeSettings or dryRunSettings set.',
|
|
)
|
|
|
|
def _Start(req):
|
|
scoped_access_settings = req.gcpUserAccessBinding.scopedAccessSettings
|
|
for scoped_access_setting in scoped_access_settings:
|
|
_ValidateAccessSettingsInScopedAccessSettingAtLeastOneIsNotEmpty(
|
|
scoped_access_setting.activeSettings,
|
|
scoped_access_setting.dryRunSettings,
|
|
)
|
|
|
|
_Start(req)
|
|
|
|
|
|
def _ProcessAccessLevelsInScopedAccessSettings(args, req):
|
|
"""Process the access levels in the scoped access settings."""
|
|
|
|
def _ValidateBelongsToSamePolicy(
|
|
access_level_resources,
|
|
dry_run_access_level_resources,
|
|
policy_resource,
|
|
parameter_names,
|
|
):
|
|
"""Validate that the access levels and policy belong to the same policy."""
|
|
combined_access_level = (
|
|
access_level_resources + dry_run_access_level_resources
|
|
)
|
|
if combined_access_level:
|
|
# Check that all access levels are from the same policy
|
|
access_level_resources_parents = [
|
|
x.Parent() for x in combined_access_level
|
|
]
|
|
if not all(
|
|
x == access_level_resources_parents[0]
|
|
for x in access_level_resources_parents
|
|
):
|
|
raise ConflictPolicyException(parameter_names)
|
|
|
|
# Check that the policy is the same as the access levels
|
|
if (
|
|
policy_resource
|
|
and access_level_resources_parents
|
|
and (
|
|
policy_resource.RelativeName()
|
|
!= access_level_resources_parents[0].RelativeName()
|
|
)
|
|
):
|
|
raise ConflictPolicyException(['--policy'] + parameter_names)
|
|
|
|
def _ReplaceAccessLevelsInAccessSettingsWithRelativeNames(
|
|
access_settings, access_level_resources
|
|
):
|
|
"""Replace the access levels in the scoped access settings with relative names.
|
|
|
|
For example,
|
|
|
|
{
|
|
'activeSettings': {
|
|
'accessLevels': [
|
|
'accessPolicies/123/accessLevels/access_level_1'
|
|
]
|
|
}
|
|
}
|
|
|
|
is replaced with:
|
|
|
|
{
|
|
'activeSettings': {
|
|
'accessLevels': [
|
|
access_level_resources.RelativeName()
|
|
]
|
|
}
|
|
}
|
|
|
|
Args:
|
|
access_settings: The access settings to replace the access levels in.
|
|
access_level_resources: The access level resources to replace the access
|
|
levels with.
|
|
"""
|
|
# Set the relative names of the access levels in the request
|
|
if access_level_resources:
|
|
access_settings.accessLevels = [
|
|
x.RelativeName() for x in access_level_resources
|
|
]
|
|
|
|
def _GetAccessLevelResources(policy_resource, access_levels):
|
|
"""Get the access level resources from the scoped access settings.
|
|
|
|
Args:
|
|
policy_resource: The policy resource
|
|
access_levels: The access levels to turn into cloud resources. For
|
|
example, ['accessPolicies/123/accessLevels/access_level_1']
|
|
|
|
Returns:
|
|
The access level cloud resources that correspond to the `access levels`.
|
|
For example,
|
|
['https://accesscontextmanager.googleapis.com/v1/accessPolicies/123/accessLevels/access_level_1']
|
|
"""
|
|
param = (
|
|
{}
|
|
if not policy_resource
|
|
else {'accessPoliciesId': policy_resource.Name()}
|
|
)
|
|
# Obtain the access level resources
|
|
access_level_resources = []
|
|
if access_levels:
|
|
access_level_resources = _TryGetAccessLevelResources(
|
|
param,
|
|
access_levels,
|
|
'binding-file',
|
|
'Access levels in ScopedAccessSettings must contain the full'
|
|
' identifier. For example:'
|
|
' `accessPolicies/123/accessLevels/access_level_1',
|
|
)
|
|
return access_level_resources
|
|
|
|
def _Start(args, req):
|
|
policy_resource = None
|
|
if args.IsKnownAndSpecified('policy'):
|
|
# Obtain the policy resource
|
|
policy_resource = _TryGetPolicyCloudResource(
|
|
args.GetValue('policy'),
|
|
'policy',
|
|
'The input must be the full identifier for the access policy, '
|
|
'such as `123` or `accessPolicies/123.',
|
|
)
|
|
|
|
scoped_access_settings = req.gcpUserAccessBinding.scopedAccessSettings
|
|
access_level_resources_sample = []
|
|
dry_run_access_level_resources_sample = []
|
|
for scoped_access_setting in scoped_access_settings:
|
|
# Obtain the access level resources
|
|
access_level_resources = []
|
|
if (
|
|
scoped_access_setting.activeSettings
|
|
and scoped_access_setting.activeSettings.accessLevels
|
|
):
|
|
access_level_resources = _GetAccessLevelResources(
|
|
policy_resource, scoped_access_setting.activeSettings.accessLevels
|
|
)
|
|
access_level_resources_sample.append(access_level_resources[0])
|
|
|
|
# Obtain the dry run access level resources
|
|
dry_run_access_level_resources = []
|
|
if (
|
|
scoped_access_setting.dryRunSettings
|
|
and scoped_access_setting.dryRunSettings.accessLevels
|
|
):
|
|
dry_run_access_level_resources = _GetAccessLevelResources(
|
|
policy_resource,
|
|
scoped_access_setting.dryRunSettings.accessLevels,
|
|
)
|
|
dry_run_access_level_resources_sample.append(
|
|
dry_run_access_level_resources[0]
|
|
)
|
|
_ValidateBelongsToSamePolicy(
|
|
access_level_resources,
|
|
dry_run_access_level_resources,
|
|
policy_resource,
|
|
['--binding-file'],
|
|
)
|
|
_ReplaceAccessLevelsInAccessSettingsWithRelativeNames(
|
|
scoped_access_setting.activeSettings, access_level_resources
|
|
)
|
|
_ReplaceAccessLevelsInAccessSettingsWithRelativeNames(
|
|
scoped_access_setting.dryRunSettings, dry_run_access_level_resources
|
|
)
|
|
|
|
# Validate that all access levels in all scoped access settings belong to
|
|
# the same policy
|
|
_ValidateBelongsToSamePolicy(
|
|
access_level_resources_sample,
|
|
dry_run_access_level_resources_sample,
|
|
policy_resource,
|
|
['--binding-file'],
|
|
)
|
|
|
|
# Obtain the global access level resource for the first access level defined
|
|
# in the request
|
|
global_access_level_resources = []
|
|
if req.gcpUserAccessBinding.accessLevels:
|
|
try:
|
|
global_access_level_resources = _GetAccessLevelResources(
|
|
policy_resource, req.gcpUserAccessBinding.accessLevels
|
|
)
|
|
except calliope_exceptions.InvalidArgumentException:
|
|
# Ignore error because global access levels will be processed later
|
|
pass
|
|
if not global_access_level_resources:
|
|
try:
|
|
global_access_level_resources = _GetAccessLevelResources(
|
|
policy_resource, req.gcpUserAccessBinding.dryRunAccessLevels
|
|
)
|
|
except calliope_exceptions.InvalidArgumentException:
|
|
# Ignore error because global access levels will be processed later
|
|
pass
|
|
|
|
# Validated that scoped and global access levels belong to the same policy
|
|
_ValidateBelongsToSamePolicy(
|
|
access_level_resources_sample,
|
|
global_access_level_resources,
|
|
policy_resource,
|
|
['--binding-file', '--level', '--dry-run-level'],
|
|
)
|
|
|
|
_Start(args, req)
|
|
|
|
|
|
def _ProcessSessionSettingsInScopedAccessSettings(req):
|
|
"""Process the session settings in the scoped access settings."""
|
|
|
|
def _ValidateSessionSettings(session_settings):
|
|
if session_settings is None:
|
|
return
|
|
if session_settings.sessionLength is None:
|
|
raise calliope_exceptions.InvalidArgumentException(
|
|
'--binding-file',
|
|
'SessionSettings within ScopedAccessSettings must include a session'
|
|
'length.',
|
|
)
|
|
session_length = times.ParseDuration(
|
|
session_settings.sessionLength
|
|
).total_seconds
|
|
if session_length > iso_duration.Duration(days=1).total_seconds:
|
|
raise calliope_exceptions.InvalidArgumentException(
|
|
'--binding-file',
|
|
'SessionLength within ScopedAccessSettings must not be greater than'
|
|
' one day',
|
|
)
|
|
if session_length < 0:
|
|
raise calliope_exceptions.InvalidArgumentException(
|
|
'--binding-file',
|
|
'SessionLength within ScopedAccessSettings must not be less than '
|
|
'zero',
|
|
)
|
|
|
|
def _InferEmptySessionSettingsFields(session_settings):
|
|
# When sessionReauthMethod is absent, infer LOGIN
|
|
if session_settings.sessionReauthMethod is None:
|
|
v1_messages = util.GetMessages('v1')
|
|
if isinstance(session_settings, v1_messages.SessionSettings):
|
|
session_settings.sessionReauthMethod = (
|
|
v1_messages.SessionSettings.SessionReauthMethodValueValuesEnum.LOGIN
|
|
)
|
|
else:
|
|
session_settings.sessionReauthMethod = util.GetMessages(
|
|
'v1alpha'
|
|
).SessionSettings.SessionReauthMethodValueValuesEnum.LOGIN
|
|
# When sessionLengthEnabled is absent, infer True if SessionLength is
|
|
# greater than zero, otherwise infer false.
|
|
if session_settings.sessionLengthEnabled is None:
|
|
session_length = times.ParseDuration(
|
|
session_settings.sessionLength
|
|
).total_seconds
|
|
if session_length > 0:
|
|
session_settings.sessionLengthEnabled = True
|
|
else:
|
|
session_settings.sessionLengthEnabled = False
|
|
# When useOidcMaxAge is absent, infer False
|
|
if session_settings.useOidcMaxAge is None:
|
|
session_settings.useOidcMaxAge = False
|
|
|
|
def _Start(req):
|
|
scoped_access_settings = req.gcpUserAccessBinding.scopedAccessSettings
|
|
for s in scoped_access_settings:
|
|
if not s.activeSettings:
|
|
continue
|
|
session_settings = s.activeSettings.sessionSettings
|
|
if not session_settings:
|
|
continue
|
|
_ValidateSessionSettings(session_settings)
|
|
_InferEmptySessionSettingsFields(session_settings)
|
|
|
|
_Start(req)
|
|
|
|
|
|
def ProcessScopedAccessSettings(unused_ref, args, req):
|
|
"""Hook to process and validate scoped access settings from the request."""
|
|
|
|
def _ValidateRestrictedClientApplicationNamesAndClientIdsAreNotSpecified(
|
|
args,
|
|
):
|
|
legacy_prca_fields_specified = args.IsKnownAndSpecified(
|
|
'restricted_client_application_names'
|
|
) or args.IsKnownAndSpecified('restricted_client_application_client_ids')
|
|
if legacy_prca_fields_specified:
|
|
raise calliope_exceptions.InvalidArgumentException(
|
|
'--binding-file',
|
|
'The binding-file cannot be specified at the same time as'
|
|
' `--restricted-client-application-names` or'
|
|
' `--restricted-client-application-client-ids`.',
|
|
)
|
|
|
|
def _Start(unused_ref, args, req):
|
|
del unused_ref
|
|
if not args.IsKnownAndSpecified('binding_file'):
|
|
return req
|
|
|
|
_ValidateRestrictedClientApplicationNamesAndClientIdsAreNotSpecified(args)
|
|
_ProcessScopesInScopedAccessSettings(req)
|
|
_ProcessAccessSettingsInScopedAccessSettings(req)
|
|
_ProcessAccessLevelsInScopedAccessSettings(args, req)
|
|
_ProcessSessionSettingsInScopedAccessSettings(req)
|
|
|
|
return req
|
|
|
|
return _Start(unused_ref, args, req)
|
|
|
|
|
|
class InvalidFormatError(common.ParseFileError):
|
|
|
|
def __init__(self, path, reason):
|
|
super(InvalidFormatError, self).__init__(
|
|
path,
|
|
(
|
|
'Invalid format: {}\n\n'
|
|
' A binding-file is a YAML-formatted file'
|
|
' containing a single gcpUserAccessBinding.'
|
|
' For example:\n\n'
|
|
' scopedAccessSettings:\n'
|
|
' - scope:\n'
|
|
' clientScope:\n'
|
|
' restrictedClientApplication:\n'
|
|
' name: Cloud Console\n'
|
|
' activeSettings:\n'
|
|
' accessLevels:\n'
|
|
' - accessPolicies/123/accessLevels/access_level_1\n'
|
|
' dryRunSettings:\n'
|
|
' accessLevels:\n'
|
|
' - accessPolicies/123/accessLevels/dry_run_access_level_1\n'
|
|
' - scope:\n'
|
|
' clientScope:\n'
|
|
' restrictedClientApplication:\n'
|
|
' clientId: my_client_id.google.com\n'
|
|
' activeSettings:\n'
|
|
' accessLevels:\n'
|
|
' - accessPolicies/123/accessLevels/access_level_2\n'
|
|
' dryRunSetting:\n'
|
|
' accessLevels:\n'
|
|
' - accessPolicies/123/accessLevels/dry_run_access_level_2\n'
|
|
).format(
|
|
reason,
|
|
),
|
|
)
|
|
|
|
|
|
def ParseGcpUserAccessBindingFromBindingFile(api_version):
|
|
"""Parse a GcpUserAccessBinding from a YAML file.
|
|
|
|
Args:
|
|
api_version: str, the API version to use for parsing the messages
|
|
|
|
Returns:
|
|
A function that parses a GcpUserAccessBinding from a file.
|
|
"""
|
|
|
|
def _ValidateSingleGcpUserAccessBinding(bindings):
|
|
if len(bindings) > 1:
|
|
raise calliope_exceptions.InvalidArgumentException(
|
|
'--input-file',
|
|
'The input file contains more than one GcpUserAccessBinding. '
|
|
'Please specify only one GcpUserAccessBinding in the input file.',
|
|
)
|
|
|
|
def _ParseVersionedGcpUserAccessBindingFromBindingFile(path):
|
|
bindings = common.ParseAccessContextManagerMessagesFromYaml(
|
|
path, util.GetMessages(version=api_version).GcpUserAccessBinding, False
|
|
)
|
|
_ValidateSingleGcpUserAccessBinding(bindings)
|
|
GcpUserAccessBindingStructureValidator(path, bindings[0]).Validate()
|
|
return bindings[0]
|
|
|
|
return _ParseVersionedGcpUserAccessBindingFromBindingFile
|
|
|
|
|
|
class GcpUserAccessBindingStructureValidator:
|
|
"""Validates a GcpUserAccessBinding structure against unrecognized fields."""
|
|
|
|
def __init__(self, path, gcp_user_access_binding):
|
|
self.path = path
|
|
self.gcp_user_access_binding = gcp_user_access_binding
|
|
|
|
def Validate(self):
|
|
"""Validates the GcpUserAccessBinding structure."""
|
|
self._ValidateAllFieldsRecognizedForGcpUserAccessBinding(
|
|
self.gcp_user_access_binding
|
|
)
|
|
self._ValidateScopedAccessSettings(
|
|
self.gcp_user_access_binding.scopedAccessSettings
|
|
)
|
|
|
|
def _ValidateScopedAccessSettings(self, scoped_access_settings_list):
|
|
"""Validates the ScopedAccessSettings structure."""
|
|
if scoped_access_settings_list:
|
|
for i in range(len(scoped_access_settings_list)):
|
|
scoped_access_settings = scoped_access_settings_list[i]
|
|
self._ValidateAllFieldsRecognized(scoped_access_settings)
|
|
self._ValidateAccessScope(scoped_access_settings.scope)
|
|
self._ValidateAccessSettings(scoped_access_settings.activeSettings)
|
|
self._ValidateAccessSettings(scoped_access_settings.dryRunSettings)
|
|
|
|
def _ValidateAccessScope(self, access_scope):
|
|
"""Validates the AccessScope structure."""
|
|
if access_scope:
|
|
self._ValidateAllFieldsRecognized(access_scope)
|
|
self._ValidateClientScope(access_scope.clientScope)
|
|
|
|
def _ValidateClientScope(self, client_scope):
|
|
"""Validates the AccessScopeType structure."""
|
|
if client_scope:
|
|
self._ValidateAllFieldsRecognized(client_scope)
|
|
self._ValidateRestrictedClientApplication(
|
|
client_scope.restrictedClientApplication
|
|
)
|
|
|
|
def _ValidateRestrictedClientApplication(self, restricted_client_application):
|
|
"""Validates the RestrictedClientApplications."""
|
|
if restricted_client_application:
|
|
self._ValidateAllFieldsRecognized(restricted_client_application)
|
|
|
|
def _ValidateSessionSettings(self, session_settings):
|
|
"""Validate the SessionSettings."""
|
|
if session_settings:
|
|
self._ValidateAllFieldsRecognized(session_settings)
|
|
|
|
def _ValidateAccessSettings(self, access_settings):
|
|
"""Validates the AccessSettings structure."""
|
|
if access_settings:
|
|
self._ValidateAllFieldsRecognized(access_settings)
|
|
self._ValidateSessionSettings(access_settings.sessionSettings)
|
|
|
|
def _ValidateAllFieldsRecognizedForGcpUserAccessBinding(
|
|
self, gcp_user_access_binding
|
|
):
|
|
"""Validates that all fields in the GcpUserAccessBinding are recognized.
|
|
|
|
Note:Because ScopedAccessSettings is the only field supported in the
|
|
GcpUserAccessBinding, a custom validation is required.
|
|
|
|
Args:
|
|
gcp_user_access_binding: The GcpUserAccessBinding to validate
|
|
|
|
Raises:
|
|
InvalidFormatError: if the GcpUserAccessBinding contains unrecognized
|
|
fields
|
|
"""
|
|
valid_fields = ['scopedAccessSettings']
|
|
unrecognized_fields = set()
|
|
empty_list = []
|
|
if gcp_user_access_binding.accessLevels != empty_list:
|
|
unrecognized_fields.add('accessLevels')
|
|
if gcp_user_access_binding.dryRunAccessLevels != empty_list:
|
|
unrecognized_fields.add('dryRunAccessLevels')
|
|
if gcp_user_access_binding.groupKey is not None:
|
|
unrecognized_fields.add('groupKey')
|
|
if gcp_user_access_binding.name:
|
|
unrecognized_fields.add('name')
|
|
if (
|
|
hasattr(gcp_user_access_binding, 'principal')
|
|
and gcp_user_access_binding.principal is not None
|
|
):
|
|
unrecognized_fields.add('principal')
|
|
if gcp_user_access_binding.sessionSettings is not None:
|
|
unrecognized_fields.add('sessionSettings')
|
|
if gcp_user_access_binding.restrictedClientApplications:
|
|
unrecognized_fields.add('restrictedClientApplications')
|
|
if gcp_user_access_binding.all_unrecognized_fields():
|
|
unrecognized_fields.update(
|
|
gcp_user_access_binding.all_unrecognized_fields()
|
|
)
|
|
if unrecognized_fields:
|
|
raise InvalidFormatError(
|
|
self.path,
|
|
'"{}" contains unrecognized fields: [{}]. Valid fields are: [{}].'
|
|
.format(
|
|
type(self.gcp_user_access_binding).__name__,
|
|
', '.join(unrecognized_fields),
|
|
', '.join(valid_fields),
|
|
),
|
|
)
|
|
|
|
def _ValidateAllFieldsRecognized(self, message):
|
|
"""Validates that all fields in the message are recognized.
|
|
|
|
Args:
|
|
message: object to validate
|
|
|
|
Raises:
|
|
InvalidFormatError: if the message contains unrecognized fields
|
|
"""
|
|
if message.all_unrecognized_fields():
|
|
message_type = type(message)
|
|
valid_fields = [f.name for f in message_type.all_fields()]
|
|
raise InvalidFormatError(
|
|
self.path,
|
|
'"{}" contains unrecognized fields: [{}]. Valid fields are: [{}]'
|
|
.format(
|
|
message_type.__name__,
|
|
', '.join(message.all_unrecognized_fields()),
|
|
', '.join(valid_fields),
|
|
),
|
|
)
|