Open
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
86 changes: 76 additions & 10 deletions sagemaker-core/src/sagemaker/core/helper/iam_role_resolver.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -306,6 +306,59 @@ def _resolve_caller_role_arn(
raise


def _config_path_for_role_type(role_type: str) -> Optional[str]:
"""Return the SageMaker config key path holding a default role ARN for a role type.

Returns None for role types the config schema has no dedicated role-ARN path
for, in which case there is no config default to consult.
"""
try:
from sagemaker.core.config.config_schema import (
TRAINING_JOB_ROLE_ARN_PATH,
FEATURE_GROUP_ROLE_ARN_PATH,
)
except Exception: # pragma: no cover - defensive against import/layout changes
return None
return {
"training": TRAINING_JOB_ROLE_ARN_PATH,
"feature_store": FEATURE_GROUP_ROLE_ARN_PATH,
}.get(role_type)


def _resolve_config_default_role(role_type: str, sagemaker_session=None) -> Optional[str]:
"""Return a default role ARN from the SageMaker intelligent-defaults config, if set.

This lets a caller whose own identity has no backing role (an IAM user or the
account root) configure a default execution role in the SageMaker config
(e.g. ``SageMaker.TrainingJob.RoleArn``) instead of being forced to pass
``role=`` on every call. Returns None when no config default is set, the role
type has no config path, or the config cannot be read — in every such case the
caller falls back to caller-identity resolution exactly as before.
"""
config_path = _config_path_for_role_type(role_type)
if not config_path:
return None
try:
from sagemaker.core.common_utils import resolve_value_from_config

config_role = resolve_value_from_config(
direct_input=None,
config_path=config_path,
sagemaker_session=sagemaker_session,
)
except Exception as e: # pragma: no cover - defensive; treat as "no config default"
logger.debug(
"Could not read a default role from the SageMaker config for '%s': %s",
role_type,
e,
)
return None
# Only trust a concrete string ARN/name; anything else means "not configured".
if isinstance(config_role, str) and config_role:
return config_role
return None


# ---------------------------------------------------------------------------
# Read-only permission / trust validation
# ---------------------------------------------------------------------------
Expand DownExpand Up@@ -530,9 +583,11 @@ def resolve_and_validate_role(
) -> str:
"""Resolve the role to use and validate it (read-only; does not mutate IAM).

Resolution:
Resolution (first match wins):
1. ``provided_role`` given → resolve it to an ARN (must exist).
2. Otherwise → resolve the caller's own identity role.
2. A default role set in the SageMaker config for this role type
(e.g. ``SageMaker.TrainingJob.RoleArn``) → resolve it to an ARN.
3. Otherwise → resolve the caller's own identity role.

The resolved role is then VALIDATED (read-only, via iam:SimulatePrincipalPolicy
+ trust inspection):
Expand DownExpand Up@@ -564,14 +619,25 @@ def resolve_and_validate_role(
if provided_role:
role_arn = _resolve_explicit_role(provided_role, sagemaker_session)
else:
sts_client = boto_session.client("sts")
caller_identity = sts_client.get_caller_identity()
caller_arn = caller_identity["Arn"]
account_id = caller_identity["Account"]
partition = _partition_from_arn(caller_arn)
role_arn = _resolve_caller_role_arn(iam_client, caller_arn, account_id, partition)
if not role_arn:
raise RoleValidationError(_build_validation_error_message(None, role_type))
# Prefer a default role configured in the SageMaker config for this role
# type (e.g. SageMaker.TrainingJob.RoleArn) before falling back to
# caller-identity inference. This is what lets an IAM-user or root caller
# (whose identity has no backing role) run without passing role= on every
# call, as long as they have configured a default execution role.
config_role = _resolve_config_default_role(role_type, sagemaker_session)
if config_role:
role_arn = _resolve_explicit_role(config_role, sagemaker_session)
else:
sts_client = boto_session.client("sts")
caller_identity = sts_client.get_caller_identity()
caller_arn = caller_identity["Arn"]
account_id = caller_identity["Account"]
partition = _partition_from_arn(caller_arn)
role_arn = _resolve_caller_role_arn(
iam_client, caller_arn, account_id, partition
)
if not role_arn:
raise RoleValidationError(_build_validation_error_message(None, role_type))

# Permission check (definitive denial blocks; unverifiable warns).
verdict, denied = _evaluate_permissions(iam_client, role_arn, role_type)
Expand Down
67 changes: 67 additions & 0 deletions sagemaker-core/tests/unit/helper/test_iam_role_resolver.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -351,6 +351,73 @@ def test_no_resolvable_caller_role_raises(self):
assert "No IAM role could be resolved" in str(exc.value)
mock_iam.create_role.assert_not_called()

def test_config_default_role_used_when_caller_is_iam_user(self):
"""An IAM user with a configured default training role uses it, not failing."""
role_arn = "arn:aws:iam::123456789012:role/ConfiguredRole"
mock_session, mock_iam, _ = _make_session(
"arn:aws:iam::123456789012:user/dev-user"
)
mock_iam.get_role.return_value = {
"Role": {"Arn": role_arn, "AssumeRolePolicyDocument": _trusted_doc()}
}
mock_iam.get_paginator.return_value = _paginator_allowing(["s3:GetObject"])

with patch(
"sagemaker.core.common_utils.resolve_value_from_config",
return_value=role_arn,
) as mock_cfg:
result = resolve_and_validate_role(
provided_role=None,
role_type="training",
sagemaker_session=mock_session,
)

assert result == role_arn
mock_cfg.assert_called_once()
# The config default is used instead of caller-identity inference.
mock_iam.create_role.assert_not_called()

def test_config_default_role_takes_precedence_over_caller_role(self):
"""A configured default role wins over the caller's own backing role."""
config_role = "arn:aws:iam::123456789012:role/ConfiguredRole"
mock_session, mock_iam, _ = _make_session(
"arn:aws:sts::123456789012:assumed-role/CallerRole/sess"
)
mock_iam.get_role.return_value = {
"Role": {"Arn": config_role, "AssumeRolePolicyDocument": _trusted_doc()}
}
mock_iam.get_paginator.return_value = _paginator_allowing(["s3:GetObject"])

with patch(
"sagemaker.core.common_utils.resolve_value_from_config",
return_value=config_role,
):
result = resolve_and_validate_role(
provided_role=None,
role_type="training",
sagemaker_session=mock_session,
)

assert result == config_role

def test_iam_user_without_config_default_still_raises(self):
"""No configured default + IAM-user caller still raises (behavior preserved)."""
mock_session, mock_iam, _ = _make_session(
"arn:aws:iam::123456789012:user/dev-user"
)
with patch(
"sagemaker.core.common_utils.resolve_value_from_config",
return_value=None,
):
with pytest.raises(RoleValidationError) as exc:
resolve_and_validate_role(
provided_role=None,
role_type="training",
sagemaker_session=mock_session,
)
assert "No IAM role could be resolved" in str(exc.value)
mock_iam.create_role.assert_not_called()

def test_invalid_role_type_raises(self):
"""Invalid role_type raises ValueError."""
with pytest.raises(ValueError, match="Invalid role_type"):
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Open
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
86 changes: 76 additions & 10 deletions sagemaker-core/src/sagemaker/core/helper/iam_role_resolver.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -306,6 +306,59 @@ def _resolve_caller_role_arn(
raise


def _config_path_for_role_type(role_type: str) -> Optional[str]:
"""Return the SageMaker config key path holding a default role ARN for a role type.

Returns None for role types the config schema has no dedicated role-ARN path
for, in which case there is no config default to consult.
"""
try:
from sagemaker.core.config.config_schema import (
TRAINING_JOB_ROLE_ARN_PATH,
FEATURE_GROUP_ROLE_ARN_PATH,
)
except Exception: # pragma: no cover - defensive against import/layout changes
return None
return {
"training": TRAINING_JOB_ROLE_ARN_PATH,
"feature_store": FEATURE_GROUP_ROLE_ARN_PATH,
}.get(role_type)


def _resolve_config_default_role(role_type: str, sagemaker_session=None) -> Optional[str]:
"""Return a default role ARN from the SageMaker intelligent-defaults config, if set.

This lets a caller whose own identity has no backing role (an IAM user or the
account root) configure a default execution role in the SageMaker config
(e.g. ``SageMaker.TrainingJob.RoleArn``) instead of being forced to pass
``role=`` on every call. Returns None when no config default is set, the role
type has no config path, or the config cannot be read — in every such case the
caller falls back to caller-identity resolution exactly as before.
"""
config_path = _config_path_for_role_type(role_type)
if not config_path:
return None
try:
from sagemaker.core.common_utils import resolve_value_from_config

config_role = resolve_value_from_config(
direct_input=None,
config_path=config_path,
sagemaker_session=sagemaker_session,
)
except Exception as e: # pragma: no cover - defensive; treat as "no config default"
logger.debug(
"Could not read a default role from the SageMaker config for '%s': %s",
role_type,
e,
)
return None
# Only trust a concrete string ARN/name; anything else means "not configured".
if isinstance(config_role, str) and config_role:
return config_role
return None


# ---------------------------------------------------------------------------
# Read-only permission / trust validation
# ---------------------------------------------------------------------------
Expand DownExpand Up@@ -530,9 +583,11 @@ def resolve_and_validate_role(
) -> str:
"""Resolve the role to use and validate it (read-only; does not mutate IAM).

Resolution:
Resolution (first match wins):
1. ``provided_role`` given → resolve it to an ARN (must exist).
2. Otherwise → resolve the caller's own identity role.
2. A default role set in the SageMaker config for this role type
(e.g. ``SageMaker.TrainingJob.RoleArn``) → resolve it to an ARN.
3. Otherwise → resolve the caller's own identity role.

The resolved role is then VALIDATED (read-only, via iam:SimulatePrincipalPolicy
+ trust inspection):
Expand DownExpand Up@@ -564,14 +619,25 @@ def resolve_and_validate_role(
if provided_role:
role_arn = _resolve_explicit_role(provided_role, sagemaker_session)
else:
sts_client = boto_session.client("sts")
caller_identity = sts_client.get_caller_identity()
caller_arn = caller_identity["Arn"]
account_id = caller_identity["Account"]
partition = _partition_from_arn(caller_arn)
role_arn = _resolve_caller_role_arn(iam_client, caller_arn, account_id, partition)
if not role_arn:
raise RoleValidationError(_build_validation_error_message(None, role_type))
# Prefer a default role configured in the SageMaker config for this role
# type (e.g. SageMaker.TrainingJob.RoleArn) before falling back to
# caller-identity inference. This is what lets an IAM-user or root caller
# (whose identity has no backing role) run without passing role= on every
# call, as long as they have configured a default execution role.
config_role = _resolve_config_default_role(role_type, sagemaker_session)
if config_role:
role_arn = _resolve_explicit_role(config_role, sagemaker_session)
else:
sts_client = boto_session.client("sts")
caller_identity = sts_client.get_caller_identity()
caller_arn = caller_identity["Arn"]
account_id = caller_identity["Account"]
partition = _partition_from_arn(caller_arn)
role_arn = _resolve_caller_role_arn(
iam_client, caller_arn, account_id, partition
)
if not role_arn:
raise RoleValidationError(_build_validation_error_message(None, role_type))

# Permission check (definitive denial blocks; unverifiable warns).
verdict, denied = _evaluate_permissions(iam_client, role_arn, role_type)
Expand Down
67 changes: 67 additions & 0 deletions sagemaker-core/tests/unit/helper/test_iam_role_resolver.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -351,6 +351,73 @@ def test_no_resolvable_caller_role_raises(self):
assert "No IAM role could be resolved" in str(exc.value)
mock_iam.create_role.assert_not_called()

def test_config_default_role_used_when_caller_is_iam_user(self):
"""An IAM user with a configured default training role uses it, not failing."""
role_arn = "arn:aws:iam::123456789012:role/ConfiguredRole"
mock_session, mock_iam, _ = _make_session(
"arn:aws:iam::123456789012:user/dev-user"
)
mock_iam.get_role.return_value = {
"Role": {"Arn": role_arn, "AssumeRolePolicyDocument": _trusted_doc()}
}
mock_iam.get_paginator.return_value = _paginator_allowing(["s3:GetObject"])

with patch(
"sagemaker.core.common_utils.resolve_value_from_config",
return_value=role_arn,
) as mock_cfg:
result = resolve_and_validate_role(
provided_role=None,
role_type="training",
sagemaker_session=mock_session,
)

assert result == role_arn
mock_cfg.assert_called_once()
# The config default is used instead of caller-identity inference.
mock_iam.create_role.assert_not_called()

def test_config_default_role_takes_precedence_over_caller_role(self):
"""A configured default role wins over the caller's own backing role."""
config_role = "arn:aws:iam::123456789012:role/ConfiguredRole"
mock_session, mock_iam, _ = _make_session(
"arn:aws:sts::123456789012:assumed-role/CallerRole/sess"
)
mock_iam.get_role.return_value = {
"Role": {"Arn": config_role, "AssumeRolePolicyDocument": _trusted_doc()}
}
mock_iam.get_paginator.return_value = _paginator_allowing(["s3:GetObject"])

with patch(
"sagemaker.core.common_utils.resolve_value_from_config",
return_value=config_role,
):
result = resolve_and_validate_role(
provided_role=None,
role_type="training",
sagemaker_session=mock_session,
)

assert result == config_role

def test_iam_user_without_config_default_still_raises(self):
"""No configured default + IAM-user caller still raises (behavior preserved)."""
mock_session, mock_iam, _ = _make_session(
"arn:aws:iam::123456789012:user/dev-user"
)
with patch(
"sagemaker.core.common_utils.resolve_value_from_config",
return_value=None,
):
with pytest.raises(RoleValidationError) as exc:
resolve_and_validate_role(
provided_role=None,
role_type="training",
sagemaker_session=mock_session,
)
assert "No IAM role could be resolved" in str(exc.value)
mock_iam.create_role.assert_not_called()

def test_invalid_role_type_raises(self):
"""Invalid role_type raises ValueError."""
with pytest.raises(ValueError, match="Invalid role_type"):
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Open
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
86 changes: 76 additions & 10 deletions sagemaker-core/src/sagemaker/core/helper/iam_role_resolver.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -306,6 +306,59 @@ def _resolve_caller_role_arn(
raise


def _config_path_for_role_type(role_type: str) -> Optional[str]:
"""Return the SageMaker config key path holding a default role ARN for a role type.

Returns None for role types the config schema has no dedicated role-ARN path
for, in which case there is no config default to consult.
"""
try:
from sagemaker.core.config.config_schema import (
TRAINING_JOB_ROLE_ARN_PATH,
FEATURE_GROUP_ROLE_ARN_PATH,
)
except Exception: # pragma: no cover - defensive against import/layout changes
return None
return {
"training": TRAINING_JOB_ROLE_ARN_PATH,
"feature_store": FEATURE_GROUP_ROLE_ARN_PATH,
}.get(role_type)


def _resolve_config_default_role(role_type: str, sagemaker_session=None) -> Optional[str]:
"""Return a default role ARN from the SageMaker intelligent-defaults config, if set.

This lets a caller whose own identity has no backing role (an IAM user or the
account root) configure a default execution role in the SageMaker config
(e.g. ``SageMaker.TrainingJob.RoleArn``) instead of being forced to pass
``role=`` on every call. Returns None when no config default is set, the role
type has no config path, or the config cannot be read — in every such case the
caller falls back to caller-identity resolution exactly as before.
"""
config_path = _config_path_for_role_type(role_type)
if not config_path:
return None
try:
from sagemaker.core.common_utils import resolve_value_from_config

config_role = resolve_value_from_config(
direct_input=None,
config_path=config_path,
sagemaker_session=sagemaker_session,
)
except Exception as e: # pragma: no cover - defensive; treat as "no config default"
logger.debug(
"Could not read a default role from the SageMaker config for '%s': %s",
role_type,
e,
)
return None
# Only trust a concrete string ARN/name; anything else means "not configured".
if isinstance(config_role, str) and config_role:
return config_role
return None


# ---------------------------------------------------------------------------
# Read-only permission / trust validation
# ---------------------------------------------------------------------------
Expand DownExpand Up@@ -530,9 +583,11 @@ def resolve_and_validate_role(
) -> str:
"""Resolve the role to use and validate it (read-only; does not mutate IAM).

Resolution:
Resolution (first match wins):
1. ``provided_role`` given → resolve it to an ARN (must exist).
2. Otherwise → resolve the caller's own identity role.
2. A default role set in the SageMaker config for this role type
(e.g. ``SageMaker.TrainingJob.RoleArn``) → resolve it to an ARN.
3. Otherwise → resolve the caller's own identity role.

The resolved role is then VALIDATED (read-only, via iam:SimulatePrincipalPolicy
+ trust inspection):
Expand DownExpand Up@@ -564,14 +619,25 @@ def resolve_and_validate_role(
if provided_role:
role_arn = _resolve_explicit_role(provided_role, sagemaker_session)
else:
sts_client = boto_session.client("sts")
caller_identity = sts_client.get_caller_identity()
caller_arn = caller_identity["Arn"]
account_id = caller_identity["Account"]
partition = _partition_from_arn(caller_arn)
role_arn = _resolve_caller_role_arn(iam_client, caller_arn, account_id, partition)
if not role_arn:
raise RoleValidationError(_build_validation_error_message(None, role_type))
# Prefer a default role configured in the SageMaker config for this role
# type (e.g. SageMaker.TrainingJob.RoleArn) before falling back to
# caller-identity inference. This is what lets an IAM-user or root caller
# (whose identity has no backing role) run without passing role= on every
# call, as long as they have configured a default execution role.
config_role = _resolve_config_default_role(role_type, sagemaker_session)
if config_role:
role_arn = _resolve_explicit_role(config_role, sagemaker_session)
else:
sts_client = boto_session.client("sts")
caller_identity = sts_client.get_caller_identity()
caller_arn = caller_identity["Arn"]
account_id = caller_identity["Account"]
partition = _partition_from_arn(caller_arn)
role_arn = _resolve_caller_role_arn(
iam_client, caller_arn, account_id, partition
)
if not role_arn:
raise RoleValidationError(_build_validation_error_message(None, role_type))

# Permission check (definitive denial blocks; unverifiable warns).
verdict, denied = _evaluate_permissions(iam_client, role_arn, role_type)
Expand Down
67 changes: 67 additions & 0 deletions sagemaker-core/tests/unit/helper/test_iam_role_resolver.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -351,6 +351,73 @@ def test_no_resolvable_caller_role_raises(self):
assert "No IAM role could be resolved" in str(exc.value)
mock_iam.create_role.assert_not_called()

def test_config_default_role_used_when_caller_is_iam_user(self):
"""An IAM user with a configured default training role uses it, not failing."""
role_arn = "arn:aws:iam::123456789012:role/ConfiguredRole"
mock_session, mock_iam, _ = _make_session(
"arn:aws:iam::123456789012:user/dev-user"
)
mock_iam.get_role.return_value = {
"Role": {"Arn": role_arn, "AssumeRolePolicyDocument": _trusted_doc()}
}
mock_iam.get_paginator.return_value = _paginator_allowing(["s3:GetObject"])

with patch(
"sagemaker.core.common_utils.resolve_value_from_config",
return_value=role_arn,
) as mock_cfg:
result = resolve_and_validate_role(
provided_role=None,
role_type="training",
sagemaker_session=mock_session,
)

assert result == role_arn
mock_cfg.assert_called_once()
# The config default is used instead of caller-identity inference.
mock_iam.create_role.assert_not_called()

def test_config_default_role_takes_precedence_over_caller_role(self):
"""A configured default role wins over the caller's own backing role."""
config_role = "arn:aws:iam::123456789012:role/ConfiguredRole"
mock_session, mock_iam, _ = _make_session(
"arn:aws:sts::123456789012:assumed-role/CallerRole/sess"
)
mock_iam.get_role.return_value = {
"Role": {"Arn": config_role, "AssumeRolePolicyDocument": _trusted_doc()}
}
mock_iam.get_paginator.return_value = _paginator_allowing(["s3:GetObject"])

with patch(
"sagemaker.core.common_utils.resolve_value_from_config",
return_value=config_role,
):
result = resolve_and_validate_role(
provided_role=None,
role_type="training",
sagemaker_session=mock_session,
)

assert result == config_role

def test_iam_user_without_config_default_still_raises(self):
"""No configured default + IAM-user caller still raises (behavior preserved)."""
mock_session, mock_iam, _ = _make_session(
"arn:aws:iam::123456789012:user/dev-user"
)
with patch(
"sagemaker.core.common_utils.resolve_value_from_config",
return_value=None,
):
with pytest.raises(RoleValidationError) as exc:
resolve_and_validate_role(
provided_role=None,
role_type="training",
sagemaker_session=mock_session,
)
assert "No IAM role could be resolved" in str(exc.value)
mock_iam.create_role.assert_not_called()

def test_invalid_role_type_raises(self):
"""Invalid role_type raises ValueError."""
with pytest.raises(ValueError, match="Invalid role_type"):
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Open
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
86 changes: 76 additions & 10 deletions sagemaker-core/src/sagemaker/core/helper/iam_role_resolver.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -306,6 +306,59 @@ def _resolve_caller_role_arn(
raise


def _config_path_for_role_type(role_type: str) -> Optional[str]:
"""Return the SageMaker config key path holding a default role ARN for a role type.

Returns None for role types the config schema has no dedicated role-ARN path
for, in which case there is no config default to consult.
"""
try:
from sagemaker.core.config.config_schema import (
TRAINING_JOB_ROLE_ARN_PATH,
FEATURE_GROUP_ROLE_ARN_PATH,
)
except Exception: # pragma: no cover - defensive against import/layout changes
return None
return {
"training": TRAINING_JOB_ROLE_ARN_PATH,
"feature_store": FEATURE_GROUP_ROLE_ARN_PATH,
}.get(role_type)


def _resolve_config_default_role(role_type: str, sagemaker_session=None) -> Optional[str]:
"""Return a default role ARN from the SageMaker intelligent-defaults config, if set.

This lets a caller whose own identity has no backing role (an IAM user or the
account root) configure a default execution role in the SageMaker config
(e.g. ``SageMaker.TrainingJob.RoleArn``) instead of being forced to pass
``role=`` on every call. Returns None when no config default is set, the role
type has no config path, or the config cannot be read — in every such case the
caller falls back to caller-identity resolution exactly as before.
"""
config_path = _config_path_for_role_type(role_type)
if not config_path:
return None
try:
from sagemaker.core.common_utils import resolve_value_from_config

config_role = resolve_value_from_config(
direct_input=None,
config_path=config_path,
sagemaker_session=sagemaker_session,
)
except Exception as e: # pragma: no cover - defensive; treat as "no config default"
logger.debug(
"Could not read a default role from the SageMaker config for '%s': %s",
role_type,
e,
)
return None
# Only trust a concrete string ARN/name; anything else means "not configured".
if isinstance(config_role, str) and config_role:
return config_role
return None


# ---------------------------------------------------------------------------
# Read-only permission / trust validation
# ---------------------------------------------------------------------------
Expand DownExpand Up@@ -530,9 +583,11 @@ def resolve_and_validate_role(
) -> str:
"""Resolve the role to use and validate it (read-only; does not mutate IAM).

Resolution:
Resolution (first match wins):
1. ``provided_role`` given → resolve it to an ARN (must exist).
2. Otherwise → resolve the caller's own identity role.
2. A default role set in the SageMaker config for this role type
(e.g. ``SageMaker.TrainingJob.RoleArn``) → resolve it to an ARN.
3. Otherwise → resolve the caller's own identity role.

The resolved role is then VALIDATED (read-only, via iam:SimulatePrincipalPolicy
+ trust inspection):
Expand DownExpand Up@@ -564,14 +619,25 @@ def resolve_and_validate_role(
if provided_role:
role_arn = _resolve_explicit_role(provided_role, sagemaker_session)
else:
sts_client = boto_session.client("sts")
caller_identity = sts_client.get_caller_identity()
caller_arn = caller_identity["Arn"]
account_id = caller_identity["Account"]
partition = _partition_from_arn(caller_arn)
role_arn = _resolve_caller_role_arn(iam_client, caller_arn, account_id, partition)
if not role_arn:
raise RoleValidationError(_build_validation_error_message(None, role_type))
# Prefer a default role configured in the SageMaker config for this role
# type (e.g. SageMaker.TrainingJob.RoleArn) before falling back to
# caller-identity inference. This is what lets an IAM-user or root caller
# (whose identity has no backing role) run without passing role= on every
# call, as long as they have configured a default execution role.
config_role = _resolve_config_default_role(role_type, sagemaker_session)
if config_role:
role_arn = _resolve_explicit_role(config_role, sagemaker_session)
else:
sts_client = boto_session.client("sts")
caller_identity = sts_client.get_caller_identity()
caller_arn = caller_identity["Arn"]
account_id = caller_identity["Account"]
partition = _partition_from_arn(caller_arn)
role_arn = _resolve_caller_role_arn(
iam_client, caller_arn, account_id, partition
)
if not role_arn:
raise RoleValidationError(_build_validation_error_message(None, role_type))

# Permission check (definitive denial blocks; unverifiable warns).
verdict, denied = _evaluate_permissions(iam_client, role_arn, role_type)
Expand Down
67 changes: 67 additions & 0 deletions sagemaker-core/tests/unit/helper/test_iam_role_resolver.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -351,6 +351,73 @@ def test_no_resolvable_caller_role_raises(self):
assert "No IAM role could be resolved" in str(exc.value)
mock_iam.create_role.assert_not_called()

def test_config_default_role_used_when_caller_is_iam_user(self):
"""An IAM user with a configured default training role uses it, not failing."""
role_arn = "arn:aws:iam::123456789012:role/ConfiguredRole"
mock_session, mock_iam, _ = _make_session(
"arn:aws:iam::123456789012:user/dev-user"
)
mock_iam.get_role.return_value = {
"Role": {"Arn": role_arn, "AssumeRolePolicyDocument": _trusted_doc()}
}
mock_iam.get_paginator.return_value = _paginator_allowing(["s3:GetObject"])

with patch(
"sagemaker.core.common_utils.resolve_value_from_config",
return_value=role_arn,
) as mock_cfg:
result = resolve_and_validate_role(
provided_role=None,
role_type="training",
sagemaker_session=mock_session,
)

assert result == role_arn
mock_cfg.assert_called_once()
# The config default is used instead of caller-identity inference.
mock_iam.create_role.assert_not_called()

def test_config_default_role_takes_precedence_over_caller_role(self):
"""A configured default role wins over the caller's own backing role."""
config_role = "arn:aws:iam::123456789012:role/ConfiguredRole"
mock_session, mock_iam, _ = _make_session(
"arn:aws:sts::123456789012:assumed-role/CallerRole/sess"
)
mock_iam.get_role.return_value = {
"Role": {"Arn": config_role, "AssumeRolePolicyDocument": _trusted_doc()}
}
mock_iam.get_paginator.return_value = _paginator_allowing(["s3:GetObject"])

with patch(
"sagemaker.core.common_utils.resolve_value_from_config",
return_value=config_role,
):
result = resolve_and_validate_role(
provided_role=None,
role_type="training",
sagemaker_session=mock_session,
)

assert result == config_role

def test_iam_user_without_config_default_still_raises(self):
"""No configured default + IAM-user caller still raises (behavior preserved)."""
mock_session, mock_iam, _ = _make_session(
"arn:aws:iam::123456789012:user/dev-user"
)
with patch(
"sagemaker.core.common_utils.resolve_value_from_config",
return_value=None,
):
with pytest.raises(RoleValidationError) as exc:
resolve_and_validate_role(
provided_role=None,
role_type="training",
sagemaker_session=mock_session,
)
assert "No IAM role could be resolved" in str(exc.value)
mock_iam.create_role.assert_not_called()

def test_invalid_role_type_raises(self):
"""Invalid role_type raises ValueError."""
with pytest.raises(ValueError, match="Invalid role_type"):
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Open
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
86 changes: 76 additions & 10 deletions sagemaker-core/src/sagemaker/core/helper/iam_role_resolver.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -306,6 +306,59 @@ def _resolve_caller_role_arn(
raise


def _config_path_for_role_type(role_type: str) -> Optional[str]:
"""Return the SageMaker config key path holding a default role ARN for a role type.

Returns None for role types the config schema has no dedicated role-ARN path
for, in which case there is no config default to consult.
"""
try:
from sagemaker.core.config.config_schema import (
TRAINING_JOB_ROLE_ARN_PATH,
FEATURE_GROUP_ROLE_ARN_PATH,
)
except Exception: # pragma: no cover - defensive against import/layout changes
return None
return {
"training": TRAINING_JOB_ROLE_ARN_PATH,
"feature_store": FEATURE_GROUP_ROLE_ARN_PATH,
}.get(role_type)


def _resolve_config_default_role(role_type: str, sagemaker_session=None) -> Optional[str]:
"""Return a default role ARN from the SageMaker intelligent-defaults config, if set.

This lets a caller whose own identity has no backing role (an IAM user or the
account root) configure a default execution role in the SageMaker config
(e.g. ``SageMaker.TrainingJob.RoleArn``) instead of being forced to pass
``role=`` on every call. Returns None when no config default is set, the role
type has no config path, or the config cannot be read — in every such case the
caller falls back to caller-identity resolution exactly as before.
"""
config_path = _config_path_for_role_type(role_type)
if not config_path:
return None
try:
from sagemaker.core.common_utils import resolve_value_from_config

config_role = resolve_value_from_config(
direct_input=None,
config_path=config_path,
sagemaker_session=sagemaker_session,
)
except Exception as e: # pragma: no cover - defensive; treat as "no config default"
logger.debug(
"Could not read a default role from the SageMaker config for '%s': %s",
role_type,
e,
)
return None
# Only trust a concrete string ARN/name; anything else means "not configured".
if isinstance(config_role, str) and config_role:
return config_role
return None


# ---------------------------------------------------------------------------
# Read-only permission / trust validation
# ---------------------------------------------------------------------------
Expand DownExpand Up@@ -530,9 +583,11 @@ def resolve_and_validate_role(
) -> str:
"""Resolve the role to use and validate it (read-only; does not mutate IAM).

Resolution:
Resolution (first match wins):
1. ``provided_role`` given → resolve it to an ARN (must exist).
2. Otherwise → resolve the caller's own identity role.
2. A default role set in the SageMaker config for this role type
(e.g. ``SageMaker.TrainingJob.RoleArn``) → resolve it to an ARN.
3. Otherwise → resolve the caller's own identity role.

The resolved role is then VALIDATED (read-only, via iam:SimulatePrincipalPolicy
+ trust inspection):
Expand DownExpand Up@@ -564,14 +619,25 @@ def resolve_and_validate_role(
if provided_role:
role_arn = _resolve_explicit_role(provided_role, sagemaker_session)
else:
sts_client = boto_session.client("sts")
caller_identity = sts_client.get_caller_identity()
caller_arn = caller_identity["Arn"]
account_id = caller_identity["Account"]
partition = _partition_from_arn(caller_arn)
role_arn = _resolve_caller_role_arn(iam_client, caller_arn, account_id, partition)
if not role_arn:
raise RoleValidationError(_build_validation_error_message(None, role_type))
# Prefer a default role configured in the SageMaker config for this role
# type (e.g. SageMaker.TrainingJob.RoleArn) before falling back to
# caller-identity inference. This is what lets an IAM-user or root caller
# (whose identity has no backing role) run without passing role= on every
# call, as long as they have configured a default execution role.
config_role = _resolve_config_default_role(role_type, sagemaker_session)
if config_role:
role_arn = _resolve_explicit_role(config_role, sagemaker_session)
else:
sts_client = boto_session.client("sts")
caller_identity = sts_client.get_caller_identity()
caller_arn = caller_identity["Arn"]
account_id = caller_identity["Account"]
partition = _partition_from_arn(caller_arn)
role_arn = _resolve_caller_role_arn(
iam_client, caller_arn, account_id, partition
)
if not role_arn:
raise RoleValidationError(_build_validation_error_message(None, role_type))

# Permission check (definitive denial blocks; unverifiable warns).
verdict, denied = _evaluate_permissions(iam_client, role_arn, role_type)
Expand Down
67 changes: 67 additions & 0 deletions sagemaker-core/tests/unit/helper/test_iam_role_resolver.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -351,6 +351,73 @@ def test_no_resolvable_caller_role_raises(self):
assert "No IAM role could be resolved" in str(exc.value)
mock_iam.create_role.assert_not_called()

def test_config_default_role_used_when_caller_is_iam_user(self):
"""An IAM user with a configured default training role uses it, not failing."""
role_arn = "arn:aws:iam::123456789012:role/ConfiguredRole"
mock_session, mock_iam, _ = _make_session(
"arn:aws:iam::123456789012:user/dev-user"
)
mock_iam.get_role.return_value = {
"Role": {"Arn": role_arn, "AssumeRolePolicyDocument": _trusted_doc()}
}
mock_iam.get_paginator.return_value = _paginator_allowing(["s3:GetObject"])

with patch(
"sagemaker.core.common_utils.resolve_value_from_config",
return_value=role_arn,
) as mock_cfg:
result = resolve_and_validate_role(
provided_role=None,
role_type="training",
sagemaker_session=mock_session,
)

assert result == role_arn
mock_cfg.assert_called_once()
# The config default is used instead of caller-identity inference.
mock_iam.create_role.assert_not_called()

def test_config_default_role_takes_precedence_over_caller_role(self):
"""A configured default role wins over the caller's own backing role."""
config_role = "arn:aws:iam::123456789012:role/ConfiguredRole"
mock_session, mock_iam, _ = _make_session(
"arn:aws:sts::123456789012:assumed-role/CallerRole/sess"
)
mock_iam.get_role.return_value = {
"Role": {"Arn": config_role, "AssumeRolePolicyDocument": _trusted_doc()}
}
mock_iam.get_paginator.return_value = _paginator_allowing(["s3:GetObject"])

with patch(
"sagemaker.core.common_utils.resolve_value_from_config",
return_value=config_role,
):
result = resolve_and_validate_role(
provided_role=None,
role_type="training",
sagemaker_session=mock_session,
)

assert result == config_role

def test_iam_user_without_config_default_still_raises(self):
"""No configured default + IAM-user caller still raises (behavior preserved)."""
mock_session, mock_iam, _ = _make_session(
"arn:aws:iam::123456789012:user/dev-user"
)
with patch(
"sagemaker.core.common_utils.resolve_value_from_config",
return_value=None,
):
with pytest.raises(RoleValidationError) as exc:
resolve_and_validate_role(
provided_role=None,
role_type="training",
sagemaker_session=mock_session,
)
assert "No IAM role could be resolved" in str(exc.value)
mock_iam.create_role.assert_not_called()

def test_invalid_role_type_raises(self):
"""Invalid role_type raises ValueError."""
with pytest.raises(ValueError, match="Invalid role_type"):
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Open
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
86 changes: 76 additions & 10 deletions sagemaker-core/src/sagemaker/core/helper/iam_role_resolver.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -306,6 +306,59 @@ def _resolve_caller_role_arn(
raise


def _config_path_for_role_type(role_type: str) -> Optional[str]:
"""Return the SageMaker config key path holding a default role ARN for a role type.

Returns None for role types the config schema has no dedicated role-ARN path
for, in which case there is no config default to consult.
"""
try:
from sagemaker.core.config.config_schema import (
TRAINING_JOB_ROLE_ARN_PATH,
FEATURE_GROUP_ROLE_ARN_PATH,
)
except Exception: # pragma: no cover - defensive against import/layout changes
return None
return {
"training": TRAINING_JOB_ROLE_ARN_PATH,
"feature_store": FEATURE_GROUP_ROLE_ARN_PATH,
}.get(role_type)


def _resolve_config_default_role(role_type: str, sagemaker_session=None) -> Optional[str]:
"""Return a default role ARN from the SageMaker intelligent-defaults config, if set.

This lets a caller whose own identity has no backing role (an IAM user or the
account root) configure a default execution role in the SageMaker config
(e.g. ``SageMaker.TrainingJob.RoleArn``) instead of being forced to pass
``role=`` on every call. Returns None when no config default is set, the role
type has no config path, or the config cannot be read — in every such case the
caller falls back to caller-identity resolution exactly as before.
"""
config_path = _config_path_for_role_type(role_type)
if not config_path:
return None
try:
from sagemaker.core.common_utils import resolve_value_from_config

config_role = resolve_value_from_config(
direct_input=None,
config_path=config_path,
sagemaker_session=sagemaker_session,
)
except Exception as e: # pragma: no cover - defensive; treat as "no config default"
logger.debug(
"Could not read a default role from the SageMaker config for '%s': %s",
role_type,
e,
)
return None
# Only trust a concrete string ARN/name; anything else means "not configured".
if isinstance(config_role, str) and config_role:
return config_role
return None


# ---------------------------------------------------------------------------
# Read-only permission / trust validation
# ---------------------------------------------------------------------------
Expand DownExpand Up@@ -530,9 +583,11 @@ def resolve_and_validate_role(
) -> str:
"""Resolve the role to use and validate it (read-only; does not mutate IAM).

Resolution:
Resolution (first match wins):
1. ``provided_role`` given → resolve it to an ARN (must exist).
2. Otherwise → resolve the caller's own identity role.
2. A default role set in the SageMaker config for this role type
(e.g. ``SageMaker.TrainingJob.RoleArn``) → resolve it to an ARN.
3. Otherwise → resolve the caller's own identity role.

The resolved role is then VALIDATED (read-only, via iam:SimulatePrincipalPolicy
+ trust inspection):
Expand DownExpand Up@@ -564,14 +619,25 @@ def resolve_and_validate_role(
if provided_role:
role_arn = _resolve_explicit_role(provided_role, sagemaker_session)
else:
sts_client = boto_session.client("sts")
caller_identity = sts_client.get_caller_identity()
caller_arn = caller_identity["Arn"]
account_id = caller_identity["Account"]
partition = _partition_from_arn(caller_arn)
role_arn = _resolve_caller_role_arn(iam_client, caller_arn, account_id, partition)
if not role_arn:
raise RoleValidationError(_build_validation_error_message(None, role_type))
# Prefer a default role configured in the SageMaker config for this role
# type (e.g. SageMaker.TrainingJob.RoleArn) before falling back to
# caller-identity inference. This is what lets an IAM-user or root caller
# (whose identity has no backing role) run without passing role= on every
# call, as long as they have configured a default execution role.
config_role = _resolve_config_default_role(role_type, sagemaker_session)
if config_role:
role_arn = _resolve_explicit_role(config_role, sagemaker_session)
else:
sts_client = boto_session.client("sts")
caller_identity = sts_client.get_caller_identity()
caller_arn = caller_identity["Arn"]
account_id = caller_identity["Account"]
partition = _partition_from_arn(caller_arn)
role_arn = _resolve_caller_role_arn(
iam_client, caller_arn, account_id, partition
)
if not role_arn:
raise RoleValidationError(_build_validation_error_message(None, role_type))

# Permission check (definitive denial blocks; unverifiable warns).
verdict, denied = _evaluate_permissions(iam_client, role_arn, role_type)
Expand Down
67 changes: 67 additions & 0 deletions sagemaker-core/tests/unit/helper/test_iam_role_resolver.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -351,6 +351,73 @@ def test_no_resolvable_caller_role_raises(self):
assert "No IAM role could be resolved" in str(exc.value)
mock_iam.create_role.assert_not_called()

def test_config_default_role_used_when_caller_is_iam_user(self):
"""An IAM user with a configured default training role uses it, not failing."""
role_arn = "arn:aws:iam::123456789012:role/ConfiguredRole"
mock_session, mock_iam, _ = _make_session(
"arn:aws:iam::123456789012:user/dev-user"
)
mock_iam.get_role.return_value = {
"Role": {"Arn": role_arn, "AssumeRolePolicyDocument": _trusted_doc()}
}
mock_iam.get_paginator.return_value = _paginator_allowing(["s3:GetObject"])

with patch(
"sagemaker.core.common_utils.resolve_value_from_config",
return_value=role_arn,
) as mock_cfg:
result = resolve_and_validate_role(
provided_role=None,
role_type="training",
sagemaker_session=mock_session,
)

assert result == role_arn
mock_cfg.assert_called_once()
# The config default is used instead of caller-identity inference.
mock_iam.create_role.assert_not_called()

def test_config_default_role_takes_precedence_over_caller_role(self):
"""A configured default role wins over the caller's own backing role."""
config_role = "arn:aws:iam::123456789012:role/ConfiguredRole"
mock_session, mock_iam, _ = _make_session(
"arn:aws:sts::123456789012:assumed-role/CallerRole/sess"
)
mock_iam.get_role.return_value = {
"Role": {"Arn": config_role, "AssumeRolePolicyDocument": _trusted_doc()}
}
mock_iam.get_paginator.return_value = _paginator_allowing(["s3:GetObject"])

with patch(
"sagemaker.core.common_utils.resolve_value_from_config",
return_value=config_role,
):
result = resolve_and_validate_role(
provided_role=None,
role_type="training",
sagemaker_session=mock_session,
)

assert result == config_role

def test_iam_user_without_config_default_still_raises(self):
"""No configured default + IAM-user caller still raises (behavior preserved)."""
mock_session, mock_iam, _ = _make_session(
"arn:aws:iam::123456789012:user/dev-user"
)
with patch(
"sagemaker.core.common_utils.resolve_value_from_config",
return_value=None,
):
with pytest.raises(RoleValidationError) as exc:
resolve_and_validate_role(
provided_role=None,
role_type="training",
sagemaker_session=mock_session,
)
assert "No IAM role could be resolved" in str(exc.value)
mock_iam.create_role.assert_not_called()

def test_invalid_role_type_raises(self):
"""Invalid role_type raises ValueError."""
with pytest.raises(ValueError, match="Invalid role_type"):
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Open
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
86 changes: 76 additions & 10 deletions sagemaker-core/src/sagemaker/core/helper/iam_role_resolver.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -306,6 +306,59 @@ def _resolve_caller_role_arn(
raise


def _config_path_for_role_type(role_type: str) -> Optional[str]:
"""Return the SageMaker config key path holding a default role ARN for a role type.

Returns None for role types the config schema has no dedicated role-ARN path
for, in which case there is no config default to consult.
"""
try:
from sagemaker.core.config.config_schema import (
TRAINING_JOB_ROLE_ARN_PATH,
FEATURE_GROUP_ROLE_ARN_PATH,
)
except Exception: # pragma: no cover - defensive against import/layout changes
return None
return {
"training": TRAINING_JOB_ROLE_ARN_PATH,
"feature_store": FEATURE_GROUP_ROLE_ARN_PATH,
}.get(role_type)


def _resolve_config_default_role(role_type: str, sagemaker_session=None) -> Optional[str]:
"""Return a default role ARN from the SageMaker intelligent-defaults config, if set.

This lets a caller whose own identity has no backing role (an IAM user or the
account root) configure a default execution role in the SageMaker config
(e.g. ``SageMaker.TrainingJob.RoleArn``) instead of being forced to pass
``role=`` on every call. Returns None when no config default is set, the role
type has no config path, or the config cannot be read — in every such case the
caller falls back to caller-identity resolution exactly as before.
"""
config_path = _config_path_for_role_type(role_type)
if not config_path:
return None
try:
from sagemaker.core.common_utils import resolve_value_from_config

config_role = resolve_value_from_config(
direct_input=None,
config_path=config_path,
sagemaker_session=sagemaker_session,
)
except Exception as e: # pragma: no cover - defensive; treat as "no config default"
logger.debug(
"Could not read a default role from the SageMaker config for '%s': %s",
role_type,
e,
)
return None
# Only trust a concrete string ARN/name; anything else means "not configured".
if isinstance(config_role, str) and config_role:
return config_role
return None


# ---------------------------------------------------------------------------
# Read-only permission / trust validation
# ---------------------------------------------------------------------------
Expand DownExpand Up@@ -530,9 +583,11 @@ def resolve_and_validate_role(
) -> str:
"""Resolve the role to use and validate it (read-only; does not mutate IAM).

Resolution:
Resolution (first match wins):
1. ``provided_role`` given → resolve it to an ARN (must exist).
2. Otherwise → resolve the caller's own identity role.
2. A default role set in the SageMaker config for this role type
(e.g. ``SageMaker.TrainingJob.RoleArn``) → resolve it to an ARN.
3. Otherwise → resolve the caller's own identity role.

The resolved role is then VALIDATED (read-only, via iam:SimulatePrincipalPolicy
+ trust inspection):
Expand DownExpand Up@@ -564,14 +619,25 @@ def resolve_and_validate_role(
if provided_role:
role_arn = _resolve_explicit_role(provided_role, sagemaker_session)
else:
sts_client = boto_session.client("sts")
caller_identity = sts_client.get_caller_identity()
caller_arn = caller_identity["Arn"]
account_id = caller_identity["Account"]
partition = _partition_from_arn(caller_arn)
role_arn = _resolve_caller_role_arn(iam_client, caller_arn, account_id, partition)
if not role_arn:
raise RoleValidationError(_build_validation_error_message(None, role_type))
# Prefer a default role configured in the SageMaker config for this role
# type (e.g. SageMaker.TrainingJob.RoleArn) before falling back to
# caller-identity inference. This is what lets an IAM-user or root caller
# (whose identity has no backing role) run without passing role= on every
# call, as long as they have configured a default execution role.
config_role = _resolve_config_default_role(role_type, sagemaker_session)
if config_role:
role_arn = _resolve_explicit_role(config_role, sagemaker_session)
else:
sts_client = boto_session.client("sts")
caller_identity = sts_client.get_caller_identity()
caller_arn = caller_identity["Arn"]
account_id = caller_identity["Account"]
partition = _partition_from_arn(caller_arn)
role_arn = _resolve_caller_role_arn(
iam_client, caller_arn, account_id, partition
)
if not role_arn:
raise RoleValidationError(_build_validation_error_message(None, role_type))

# Permission check (definitive denial blocks; unverifiable warns).
verdict, denied = _evaluate_permissions(iam_client, role_arn, role_type)
Expand Down
67 changes: 67 additions & 0 deletions sagemaker-core/tests/unit/helper/test_iam_role_resolver.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -351,6 +351,73 @@ def test_no_resolvable_caller_role_raises(self):
assert "No IAM role could be resolved" in str(exc.value)
mock_iam.create_role.assert_not_called()

def test_config_default_role_used_when_caller_is_iam_user(self):
"""An IAM user with a configured default training role uses it, not failing."""
role_arn = "arn:aws:iam::123456789012:role/ConfiguredRole"
mock_session, mock_iam, _ = _make_session(
"arn:aws:iam::123456789012:user/dev-user"
)
mock_iam.get_role.return_value = {
"Role": {"Arn": role_arn, "AssumeRolePolicyDocument": _trusted_doc()}
}
mock_iam.get_paginator.return_value = _paginator_allowing(["s3:GetObject"])

with patch(
"sagemaker.core.common_utils.resolve_value_from_config",
return_value=role_arn,
) as mock_cfg:
result = resolve_and_validate_role(
provided_role=None,
role_type="training",
sagemaker_session=mock_session,
)

assert result == role_arn
mock_cfg.assert_called_once()
# The config default is used instead of caller-identity inference.
mock_iam.create_role.assert_not_called()

def test_config_default_role_takes_precedence_over_caller_role(self):
"""A configured default role wins over the caller's own backing role."""
config_role = "arn:aws:iam::123456789012:role/ConfiguredRole"
mock_session, mock_iam, _ = _make_session(
"arn:aws:sts::123456789012:assumed-role/CallerRole/sess"
)
mock_iam.get_role.return_value = {
"Role": {"Arn": config_role, "AssumeRolePolicyDocument": _trusted_doc()}
}
mock_iam.get_paginator.return_value = _paginator_allowing(["s3:GetObject"])

with patch(
"sagemaker.core.common_utils.resolve_value_from_config",
return_value=config_role,
):
result = resolve_and_validate_role(
provided_role=None,
role_type="training",
sagemaker_session=mock_session,
)

assert result == config_role

def test_iam_user_without_config_default_still_raises(self):
"""No configured default + IAM-user caller still raises (behavior preserved)."""
mock_session, mock_iam, _ = _make_session(
"arn:aws:iam::123456789012:user/dev-user"
)
with patch(
"sagemaker.core.common_utils.resolve_value_from_config",
return_value=None,
):
with pytest.raises(RoleValidationError) as exc:
resolve_and_validate_role(
provided_role=None,
role_type="training",
sagemaker_session=mock_session,
)
assert "No IAM role could be resolved" in str(exc.value)
mock_iam.create_role.assert_not_called()

def test_invalid_role_type_raises(self):
"""Invalid role_type raises ValueError."""
with pytest.raises(ValueError, match="Invalid role_type"):
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Open
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
86 changes: 76 additions & 10 deletions sagemaker-core/src/sagemaker/core/helper/iam_role_resolver.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -306,6 +306,59 @@ def _resolve_caller_role_arn(
raise


def _config_path_for_role_type(role_type: str) -> Optional[str]:
"""Return the SageMaker config key path holding a default role ARN for a role type.

Returns None for role types the config schema has no dedicated role-ARN path
for, in which case there is no config default to consult.
"""
try:
from sagemaker.core.config.config_schema import (
TRAINING_JOB_ROLE_ARN_PATH,
FEATURE_GROUP_ROLE_ARN_PATH,
)
except Exception: # pragma: no cover - defensive against import/layout changes
return None
return {
"training": TRAINING_JOB_ROLE_ARN_PATH,
"feature_store": FEATURE_GROUP_ROLE_ARN_PATH,
}.get(role_type)


def _resolve_config_default_role(role_type: str, sagemaker_session=None) -> Optional[str]:
"""Return a default role ARN from the SageMaker intelligent-defaults config, if set.

This lets a caller whose own identity has no backing role (an IAM user or the
account root) configure a default execution role in the SageMaker config
(e.g. ``SageMaker.TrainingJob.RoleArn``) instead of being forced to pass
``role=`` on every call. Returns None when no config default is set, the role
type has no config path, or the config cannot be read — in every such case the
caller falls back to caller-identity resolution exactly as before.
"""
config_path = _config_path_for_role_type(role_type)
if not config_path:
return None
try:
from sagemaker.core.common_utils import resolve_value_from_config

config_role = resolve_value_from_config(
direct_input=None,
config_path=config_path,
sagemaker_session=sagemaker_session,
)
except Exception as e: # pragma: no cover - defensive; treat as "no config default"
logger.debug(
"Could not read a default role from the SageMaker config for '%s': %s",
role_type,
e,
)
return None
# Only trust a concrete string ARN/name; anything else means "not configured".
if isinstance(config_role, str) and config_role:
return config_role
return None


# ---------------------------------------------------------------------------
# Read-only permission / trust validation
# ---------------------------------------------------------------------------
Expand DownExpand Up@@ -530,9 +583,11 @@ def resolve_and_validate_role(
) -> str:
"""Resolve the role to use and validate it (read-only; does not mutate IAM).

Resolution:
Resolution (first match wins):
1. ``provided_role`` given → resolve it to an ARN (must exist).
2. Otherwise → resolve the caller's own identity role.
2. A default role set in the SageMaker config for this role type
(e.g. ``SageMaker.TrainingJob.RoleArn``) → resolve it to an ARN.
3. Otherwise → resolve the caller's own identity role.

The resolved role is then VALIDATED (read-only, via iam:SimulatePrincipalPolicy
+ trust inspection):
Expand DownExpand Up@@ -564,14 +619,25 @@ def resolve_and_validate_role(
if provided_role:
role_arn = _resolve_explicit_role(provided_role, sagemaker_session)
else:
sts_client = boto_session.client("sts")
caller_identity = sts_client.get_caller_identity()
caller_arn = caller_identity["Arn"]
account_id = caller_identity["Account"]
partition = _partition_from_arn(caller_arn)
role_arn = _resolve_caller_role_arn(iam_client, caller_arn, account_id, partition)
if not role_arn:
raise RoleValidationError(_build_validation_error_message(None, role_type))
# Prefer a default role configured in the SageMaker config for this role
# type (e.g. SageMaker.TrainingJob.RoleArn) before falling back to
# caller-identity inference. This is what lets an IAM-user or root caller
# (whose identity has no backing role) run without passing role= on every
# call, as long as they have configured a default execution role.
config_role = _resolve_config_default_role(role_type, sagemaker_session)
if config_role:
role_arn = _resolve_explicit_role(config_role, sagemaker_session)
else:
sts_client = boto_session.client("sts")
caller_identity = sts_client.get_caller_identity()
caller_arn = caller_identity["Arn"]
account_id = caller_identity["Account"]
partition = _partition_from_arn(caller_arn)
role_arn = _resolve_caller_role_arn(
iam_client, caller_arn, account_id, partition
)
if not role_arn:
raise RoleValidationError(_build_validation_error_message(None, role_type))

# Permission check (definitive denial blocks; unverifiable warns).
verdict, denied = _evaluate_permissions(iam_client, role_arn, role_type)
Expand Down
67 changes: 67 additions & 0 deletions sagemaker-core/tests/unit/helper/test_iam_role_resolver.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -351,6 +351,73 @@ def test_no_resolvable_caller_role_raises(self):
assert "No IAM role could be resolved" in str(exc.value)
mock_iam.create_role.assert_not_called()

def test_config_default_role_used_when_caller_is_iam_user(self):
"""An IAM user with a configured default training role uses it, not failing."""
role_arn = "arn:aws:iam::123456789012:role/ConfiguredRole"
mock_session, mock_iam, _ = _make_session(
"arn:aws:iam::123456789012:user/dev-user"
)
mock_iam.get_role.return_value = {
"Role": {"Arn": role_arn, "AssumeRolePolicyDocument": _trusted_doc()}
}
mock_iam.get_paginator.return_value = _paginator_allowing(["s3:GetObject"])

with patch(
"sagemaker.core.common_utils.resolve_value_from_config",
return_value=role_arn,
) as mock_cfg:
result = resolve_and_validate_role(
provided_role=None,
role_type="training",
sagemaker_session=mock_session,
)

assert result == role_arn
mock_cfg.assert_called_once()
# The config default is used instead of caller-identity inference.
mock_iam.create_role.assert_not_called()

def test_config_default_role_takes_precedence_over_caller_role(self):
"""A configured default role wins over the caller's own backing role."""
config_role = "arn:aws:iam::123456789012:role/ConfiguredRole"
mock_session, mock_iam, _ = _make_session(
"arn:aws:sts::123456789012:assumed-role/CallerRole/sess"
)
mock_iam.get_role.return_value = {
"Role": {"Arn": config_role, "AssumeRolePolicyDocument": _trusted_doc()}
}
mock_iam.get_paginator.return_value = _paginator_allowing(["s3:GetObject"])

with patch(
"sagemaker.core.common_utils.resolve_value_from_config",
return_value=config_role,
):
result = resolve_and_validate_role(
provided_role=None,
role_type="training",
sagemaker_session=mock_session,
)

assert result == config_role

def test_iam_user_without_config_default_still_raises(self):
"""No configured default + IAM-user caller still raises (behavior preserved)."""
mock_session, mock_iam, _ = _make_session(
"arn:aws:iam::123456789012:user/dev-user"
)
with patch(
"sagemaker.core.common_utils.resolve_value_from_config",
return_value=None,
):
with pytest.raises(RoleValidationError) as exc:
resolve_and_validate_role(
provided_role=None,
role_type="training",
sagemaker_session=mock_session,
)
assert "No IAM role could be resolved" in str(exc.value)
mock_iam.create_role.assert_not_called()

def test_invalid_role_type_raises(self):
"""Invalid role_type raises ValueError."""
with pytest.raises(ValueError, match="Invalid role_type"):
Expand Down
Loading