Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ All notable changes to this project will be documented in this file.

The format is based on `Keep a Changelog <http://keepachangelog.com/>`__.

3.81.1 - 2026-05-05
-------------------
Added
~~~~~
* Support for large generic v4 and v5 unit shapes in the Generative AI service in Python SDK



3.81.0 - 2026-04-28
-------------------
Added
Expand Down Expand Up @@ -108,6 +116,7 @@ Changed
* ``oci batch batch-task-profile create --min-memory-in-gbs --min-ocpus``



3.80.0 - 2026-04-21
-------------------
Added
Expand Down
4 changes: 2 additions & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Jinja2>=3.1.5,<4.0.0; python_version >= '3.7'
jmespath>=0.10.0,<=1.0.1
ndg-httpsclient==0.4.2
mock==2.0.0
oci==2.173.0
oci==2.173.1
packaging>=22.0,<25.0; python_version > '3.8'
packaging==20.2; python_version <= '3.8'
pluggy==0.13.0
Expand Down Expand Up @@ -55,4 +55,4 @@ setuptools==68.0.0; python_version == '3.7'
setuptools==59.6.0; python_version == '3.6'
# this is required because of python 3.6 requests dependency version bound
urllib3==2.6.3; python_version >= '3.10'
urllib3==1.26.20; python_version < '3.10'
urllib3==1.26.20; python_version < '3.10'
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ def open_relative(*path):
readme = f.read()

requires = [
'oci==2.173.0',
'oci==2.173.1',
'arrow>=1.0.0,<2.0.0',
'certifi>=2025.1.31,<2026.0.0',
'click==8.0.4',
Expand Down
34 changes: 33 additions & 1 deletion src/oci_cli/cli_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,36 @@
import tempfile
import zipfile
import datetime
import re

CONFIG_KEY_FILE_SUFFIX = "_file"
TOKEN_FILE_SUFFIX = "_token"

ZIP_FILE_FORMAT = 'zip'

PROFILE_NAME_PATTERN = re.compile(r'^[A-Za-z0-9._\-@]+$')


def validate_profile_name(profile_name):
"""
Validates that the profile name contains only allowed characters:
[A-Za-z0-9._-@]+ and does not include path traversal strings such as /, \\, .., or .
Raises a click.UsageError (prints a clear message and aborts) if invalid.
"""
if profile_name is None or not isinstance(profile_name, str) or not profile_name:
raise click.UsageError("Profile name must be a non-empty string.")

if not PROFILE_NAME_PATTERN.match(profile_name):
raise click.UsageError(
"Invalid profile name '{}'. Profile names may only contain letters, numbers, '.', '_', '-', or '@'."
.format(profile_name)
)
# Protect against "." or ".." (these are still allowed by the pattern)
if profile_name in (".", ".."):
raise click.UsageError(
"Invalid profile name '{}': Profile name cannot be '.' or '..'.".format(profile_name)
)


@cli.group('session', help="""Session commands for CLI""")
@cli_util.help_option_group
Expand All @@ -53,6 +77,11 @@ def authenticate(ctx, region, tenancy_name, profile_name, config_location, use_p
region = ctx.obj['region']
if region is None:
region = cli_setup.prompt_for_region()

# Validate profile name
if profile_name:
validate_profile_name(profile_name)

persist_only_public_key = False
if no_browser:
if int(session_expiration_in_minutes) > int(cli_constants.OCI_CLI_UPST_TOKEN_MAX_TTL):
Expand Down Expand Up @@ -343,6 +372,9 @@ def import_session(ctx, session_archive, force):
archived_profile_name = archived_profiles[profile_no]
archived_profile = archived_config[archived_profile_name]

# Validate the profile name being imported
validate_profile_name(archived_profile_name)

if 'security_token_file' not in archived_profile:
click.echo('ERROR: Cannot import non token based profile (profile must contain value for security_token_file).', file=sys.stderr)
sys.exit(1)
Expand All @@ -352,7 +384,7 @@ def import_session(ctx, session_archive, force):

while archived_profile_name in current_profiles and not force:
archived_profile_name = click.prompt("Config already contains a profile with the same name as the archived profile: {}. Provide an alternative name for the imported profile".format(archived_profile_name))

validate_profile_name(archived_profile_name)
imported_resources_dir = os.path.join(cli_setup.DEFAULT_TOKEN_DIRECTORY, archived_profile_name)
# The session-archive could be a malicious file where a profile name like "../../<some_path>" can cause
# arbitrary files to be written outside the expected location. Need to prevent it
Expand Down
76 changes: 51 additions & 25 deletions src/oci_cli/cli_setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.backends import default_backend

PROFILE_NAME_PATTERN = re.compile(r'^[A-Za-z0-9._\-@]+$')

generate_oci_config_instructions = """
This command provides a walkthrough of creating a valid CLI config file.

Expand Down Expand Up @@ -776,6 +778,25 @@ def validate_region(region):
return region


def validate_profile_name(profile_name):
"""
Validates that the profile name contains only allowed characters:
[A-Za-z0-9._-@]+ and does not include path traversal strings such as /, \\, .., or .
Raises a click.UsageError (prints a clear message and aborts) if invalid.
"""
if profile_name is None or not isinstance(profile_name, str) or not profile_name:
raise click.UsageError("Profile name must be a non-empty string.")

if not PROFILE_NAME_PATTERN.match(profile_name):
raise click.UsageError(
"Invalid profile name '{}'. Profile names may only contain letters, numbers, '.', '_', '-', or '@'.".format(profile_name)
)
if profile_name in (".", ".."):
raise click.UsageError(
"Invalid profile name '{}': Profile name cannot be '.' or '..'.".format(profile_name)
)


def is_valid_region_index(region):

return region.isdigit() and len(REGIONS) >= int(region) >= 1
Expand Down Expand Up @@ -823,28 +844,6 @@ def validate_resource_name(name):
return name


def validate_profile_name(value, config_parser, overwrite=False, makeUpper=True):
if not value:
click.echo('Cannot specify blank profile name')
return None

sections = [section.upper() for section in config_parser.sections()]
if config_parser['DEFAULT']:
sections.append('DEFAULT')

if makeUpper:
value = value.upper()

if value in sections and not overwrite:
click.echo('Profile {section} already exists in config. Cannot specify a profile that conflicts with any existing profile(s): {sections}'.format(
section=value,
sections=', '.join(sections)
))
value = None

return value


def prompt_for_config_location():
config_location = os.path.abspath(click.prompt('Enter a location for your config', default=os.path.join(DEFAULT_DIRECTORY, 'config'), value_proc=process_config_filename))
if os.path.exists(config_location):
Expand All @@ -854,7 +853,22 @@ def prompt_for_config_location():
if click.confirm('Config file: {} already exists. Do you want add a profile here? (If no, you will be prompted to overwrite the file)'.format(config_location), default=True):
profile_name = None
while not profile_name:
profile_name = click.prompt('Enter the name of the profile you would like to create', value_proc=lambda value: validate_profile_name(value, config_parser))
entered_name = click.prompt('Enter the name of the profile you would like to create')
# Validates for allowed characters/etc
validate_profile_name(entered_name)
# Now check for collision
sections = [section.upper() for section in config_parser.sections()]
if config_parser['DEFAULT']:
sections.append('DEFAULT')
entered_upper = entered_name.upper()
if entered_upper in sections:
click.echo('Profile {section} already exists in config. Cannot specify a profile that conflicts with any existing profile(s): {sections}'.format(
section=entered_upper,
sections=', '.join(sections)
))
profile_name = None
else:
profile_name = entered_name

return (config_location, profile_name)
except configparser.Error as e:
Expand All @@ -881,8 +895,20 @@ def prompt_session_for_profile():
if not os.path.exists(DEFAULT_CONFIG_LOCATION):
return (DEFAULT_CONFIG_LOCATION, DEFAULT_PROFILE_NAME)
config_parser.read(DEFAULT_CONFIG_LOCATION)
profile_name = click.prompt('Enter the name of the profile you would like to create',
value_proc=lambda value: validate_profile_name(value, config_parser, True, False))
entered_name = click.prompt('Enter the name of the profile you would like to create')
# Validate format
validate_profile_name(entered_name)
sections = [section.upper() for section in config_parser.sections()]
if config_parser['DEFAULT']:
sections.append('DEFAULT')
entered_upper = entered_name.upper()
if entered_upper in sections:
click.echo(
'Profile {section} already exists in config. Cannot specify a profile that conflicts with any existing profile(s): {sections}'.format(
section=entered_upper,
sections=', '.join(sections)
))
profile_name = entered_name
except configparser.Error as e:
pass

Expand Down
2 changes: 1 addition & 1 deletion src/oci_cli/service_mapping.py
Original file line number Diff line number Diff line change
Expand Up @@ -564,7 +564,7 @@
"Databases"
],
"occ": [
"occ",
"oci_control_center",
"OCI Control Center",
"Others"
],
Expand Down
2 changes: 1 addition & 1 deletion src/oci_cli/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
# Copyright (c) 2016, 2026, Oracle and/or its affiliates. All rights reserved.
# This software is dual-licensed to you under the Universal Permissive License (UPL) 1.0 as shown at https://oss.oracle.com/licenses/upl or Apache License 2.0 as shown at http://www.apache.org/licenses/LICENSE-2.0. You may choose either license.

__version__ = '3.81.0'
__version__ = '3.81.1'