mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-10-25 21:04:19 +00:00
674 lines
26 KiB
Python
674 lines
26 KiB
Python
import base64
|
|
import json
|
|
from unittest import mock
|
|
|
|
import pytest
|
|
|
|
from prowler.providers.okta.exceptions.exceptions import (
|
|
OktaEnvironmentVariableError,
|
|
OktaInsufficientPermissionsError,
|
|
OktaInvalidCredentialsError,
|
|
OktaInvalidOrgDomainError,
|
|
OktaInvalidProviderIdError,
|
|
OktaPrivateKeyFileError,
|
|
OktaSetUpIdentityError,
|
|
)
|
|
from prowler.providers.okta.models import OktaIdentityInfo, OktaSession
|
|
from prowler.providers.okta.okta_provider import DEFAULT_SCOPES, OktaProvider
|
|
from tests.providers.okta.okta_fixtures import (
|
|
OKTA_CLIENT_ID,
|
|
OKTA_ORG_DOMAIN,
|
|
OKTA_PRIVATE_KEY,
|
|
)
|
|
|
|
|
|
def _make_jwt(payload: dict) -> str:
|
|
"""Build an unsigned JWT carrying the given payload dict.
|
|
|
|
The signature segment is irrelevant — `_decode_token_scopes` reads
|
|
the payload without verification.
|
|
"""
|
|
|
|
def _b64u(data: bytes) -> str:
|
|
return base64.urlsafe_b64encode(data).rstrip(b"=").decode()
|
|
|
|
header = _b64u(json.dumps({"alg": "none"}).encode())
|
|
body = _b64u(json.dumps(payload).encode())
|
|
return f"{header}.{body}.sig"
|
|
|
|
|
|
class Test_OktaProvider_decode_token_scopes:
|
|
def test_returns_scopes_from_list_scp_claim(self):
|
|
token = _make_jwt({"scp": ["okta.policies.read", "okta.brands.read"]})
|
|
assert OktaProvider._decode_token_scopes(token) == [
|
|
"okta.policies.read",
|
|
"okta.brands.read",
|
|
]
|
|
|
|
def test_returns_scopes_from_space_separated_scp_string(self):
|
|
token = _make_jwt({"scp": "okta.policies.read okta.brands.read"})
|
|
assert OktaProvider._decode_token_scopes(token) == [
|
|
"okta.policies.read",
|
|
"okta.brands.read",
|
|
]
|
|
|
|
def test_returns_empty_list_when_token_is_none(self):
|
|
assert OktaProvider._decode_token_scopes(None) == []
|
|
|
|
def test_returns_empty_list_when_token_is_empty_string(self):
|
|
assert OktaProvider._decode_token_scopes("") == []
|
|
|
|
def test_returns_empty_list_when_scp_claim_missing(self):
|
|
token = _make_jwt({"sub": "client-id"})
|
|
assert OktaProvider._decode_token_scopes(token) == []
|
|
|
|
def test_returns_empty_list_when_token_is_malformed(self):
|
|
assert OktaProvider._decode_token_scopes("not.a.jwt-with-bad-base64!!") == []
|
|
|
|
def test_returns_empty_list_when_payload_is_not_json(self):
|
|
bad = base64.urlsafe_b64encode(b"not json").rstrip(b"=").decode()
|
|
assert OktaProvider._decode_token_scopes(f"hdr.{bad}.sig") == []
|
|
|
|
|
|
@pytest.fixture
|
|
def _clear_okta_env(monkeypatch):
|
|
for var in (
|
|
"OKTA_ORG_DOMAIN",
|
|
"OKTA_CLIENT_ID",
|
|
"OKTA_PRIVATE_KEY",
|
|
"OKTA_PRIVATE_KEY_FILE",
|
|
"OKTA_SCOPES",
|
|
):
|
|
monkeypatch.delenv(var, raising=False)
|
|
|
|
|
|
class Test_OktaProvider_validate_arguments:
|
|
def test_missing_all_three_raises_combined(self, _clear_okta_env):
|
|
with pytest.raises(OktaEnvironmentVariableError) as exc:
|
|
OktaProvider.validate_arguments()
|
|
msg = str(exc.value)
|
|
assert "OKTA_ORG_DOMAIN" in msg
|
|
assert "OKTA_CLIENT_ID" in msg
|
|
assert "OKTA_PRIVATE_KEY" in msg
|
|
|
|
def test_only_org_domain_missing(self, _clear_okta_env, tmp_path):
|
|
key_file = tmp_path / "key.pem"
|
|
key_file.write_text(OKTA_PRIVATE_KEY)
|
|
with pytest.raises(OktaEnvironmentVariableError) as exc:
|
|
OktaProvider.validate_arguments(
|
|
okta_client_id=OKTA_CLIENT_ID,
|
|
okta_private_key_file=str(key_file),
|
|
)
|
|
assert "OKTA_ORG_DOMAIN" in str(exc.value)
|
|
|
|
def test_accepts_private_key_content_in_place_of_file(self, _clear_okta_env):
|
|
OktaProvider.validate_arguments(
|
|
okta_org_domain=OKTA_ORG_DOMAIN,
|
|
okta_client_id=OKTA_CLIENT_ID,
|
|
okta_private_key=OKTA_PRIVATE_KEY,
|
|
)
|
|
|
|
def test_all_present_via_args(self, _clear_okta_env, tmp_path):
|
|
key_file = tmp_path / "key.pem"
|
|
key_file.write_text(OKTA_PRIVATE_KEY)
|
|
OktaProvider.validate_arguments(
|
|
okta_org_domain=OKTA_ORG_DOMAIN,
|
|
okta_client_id=OKTA_CLIENT_ID,
|
|
okta_private_key_file=str(key_file),
|
|
)
|
|
|
|
def test_all_present_via_env(self, monkeypatch, tmp_path):
|
|
key_file = tmp_path / "key.pem"
|
|
key_file.write_text(OKTA_PRIVATE_KEY)
|
|
monkeypatch.setenv("OKTA_ORG_DOMAIN", OKTA_ORG_DOMAIN)
|
|
monkeypatch.setenv("OKTA_CLIENT_ID", OKTA_CLIENT_ID)
|
|
monkeypatch.setenv("OKTA_PRIVATE_KEY_FILE", str(key_file))
|
|
OktaProvider.validate_arguments()
|
|
|
|
|
|
class Test_OktaProvider_setup_session:
|
|
def test_rejects_domain_with_scheme(self, _clear_okta_env, tmp_path):
|
|
key_file = tmp_path / "key.pem"
|
|
key_file.write_text(OKTA_PRIVATE_KEY)
|
|
with pytest.raises(OktaInvalidOrgDomainError):
|
|
OktaProvider.setup_session(
|
|
org_domain="https://acme.okta.com",
|
|
client_id=OKTA_CLIENT_ID,
|
|
private_key_file=str(key_file),
|
|
)
|
|
|
|
def test_rejects_domain_with_trailing_slash(self, _clear_okta_env, tmp_path):
|
|
key_file = tmp_path / "key.pem"
|
|
key_file.write_text(OKTA_PRIVATE_KEY)
|
|
with pytest.raises(OktaInvalidOrgDomainError):
|
|
OktaProvider.setup_session(
|
|
org_domain="acme.okta.com/",
|
|
client_id=OKTA_CLIENT_ID,
|
|
private_key_file=str(key_file),
|
|
)
|
|
|
|
def test_rejects_non_okta_tld(self, _clear_okta_env, tmp_path):
|
|
key_file = tmp_path / "key.pem"
|
|
key_file.write_text(OKTA_PRIVATE_KEY)
|
|
with pytest.raises(OktaInvalidOrgDomainError):
|
|
OktaProvider.setup_session(
|
|
org_domain="login.example.com",
|
|
client_id=OKTA_CLIENT_ID,
|
|
private_key_file=str(key_file),
|
|
)
|
|
|
|
def test_accepts_all_okta_managed_tlds(self, _clear_okta_env, tmp_path):
|
|
# Mirrors the domain whitelist used by the Okta SDK
|
|
# (okta.config.config_validator) so that gov/mil tenants — exactly the
|
|
# audience most likely to care about the DISA STIG check — are not
|
|
# turned away at provider init.
|
|
key_file = tmp_path / "key.pem"
|
|
key_file.write_text(OKTA_PRIVATE_KEY)
|
|
for domain in (
|
|
"acme.oktapreview.com",
|
|
"acme.okta-emea.com",
|
|
"acme.okta-gov.com",
|
|
"acme.okta.mil",
|
|
"acme.okta-miltest.com",
|
|
"acme.trex-govcloud.com",
|
|
):
|
|
session = OktaProvider.setup_session(
|
|
org_domain=domain,
|
|
client_id=OKTA_CLIENT_ID,
|
|
private_key_file=str(key_file),
|
|
)
|
|
assert session.org_domain == domain
|
|
|
|
def test_unreadable_private_key_file_raises(self, _clear_okta_env):
|
|
with pytest.raises(OktaPrivateKeyFileError):
|
|
OktaProvider.setup_session(
|
|
org_domain=OKTA_ORG_DOMAIN,
|
|
client_id=OKTA_CLIENT_ID,
|
|
private_key_file="/nonexistent/path.pem",
|
|
)
|
|
|
|
def test_happy_path_uses_default_scopes(self, _clear_okta_env, tmp_path):
|
|
key_file = tmp_path / "key.pem"
|
|
key_file.write_text(OKTA_PRIVATE_KEY)
|
|
session = OktaProvider.setup_session(
|
|
org_domain=OKTA_ORG_DOMAIN,
|
|
client_id=OKTA_CLIENT_ID,
|
|
private_key_file=str(key_file),
|
|
)
|
|
assert session.org_domain == OKTA_ORG_DOMAIN
|
|
assert session.client_id == OKTA_CLIENT_ID
|
|
assert session.private_key == OKTA_PRIVATE_KEY
|
|
assert session.scopes == DEFAULT_SCOPES
|
|
|
|
def test_custom_scopes_parsed_from_csv(self, _clear_okta_env, tmp_path):
|
|
key_file = tmp_path / "key.pem"
|
|
key_file.write_text(OKTA_PRIVATE_KEY)
|
|
session = OktaProvider.setup_session(
|
|
org_domain=OKTA_ORG_DOMAIN,
|
|
client_id=OKTA_CLIENT_ID,
|
|
private_key_file=str(key_file),
|
|
scopes="okta.policies.read, okta.apps.read ,okta.users.read",
|
|
)
|
|
assert session.scopes == [
|
|
"okta.policies.read",
|
|
"okta.apps.read",
|
|
"okta.users.read",
|
|
]
|
|
|
|
def test_custom_scopes_accepts_list_input(self, _clear_okta_env, tmp_path):
|
|
key_file = tmp_path / "key.pem"
|
|
key_file.write_text(OKTA_PRIVATE_KEY)
|
|
session = OktaProvider.setup_session(
|
|
org_domain=OKTA_ORG_DOMAIN,
|
|
client_id=OKTA_CLIENT_ID,
|
|
private_key_file=str(key_file),
|
|
scopes=["okta.policies.read", "okta.apps.read", "okta.users.read"],
|
|
)
|
|
assert session.scopes == [
|
|
"okta.policies.read",
|
|
"okta.apps.read",
|
|
"okta.users.read",
|
|
]
|
|
|
|
def test_custom_scopes_flattens_mixed_list_and_csv(self, _clear_okta_env, tmp_path):
|
|
# Mirrors how argparse nargs="+" delivers values when a user
|
|
# passes "--okta-scopes a,b c" — a list whose first element still
|
|
# contains a comma.
|
|
key_file = tmp_path / "key.pem"
|
|
key_file.write_text(OKTA_PRIVATE_KEY)
|
|
session = OktaProvider.setup_session(
|
|
org_domain=OKTA_ORG_DOMAIN,
|
|
client_id=OKTA_CLIENT_ID,
|
|
private_key_file=str(key_file),
|
|
scopes=["okta.policies.read,okta.apps.read", "okta.users.read"],
|
|
)
|
|
assert session.scopes == [
|
|
"okta.policies.read",
|
|
"okta.apps.read",
|
|
"okta.users.read",
|
|
]
|
|
|
|
def test_org_domain_normalized_lowercase_and_trimmed(
|
|
self, _clear_okta_env, tmp_path
|
|
):
|
|
# The provider lowercases and strips whitespace so that
|
|
# " ACME.okta.com " is accepted as "acme.okta.com".
|
|
key_file = tmp_path / "key.pem"
|
|
key_file.write_text(OKTA_PRIVATE_KEY)
|
|
session = OktaProvider.setup_session(
|
|
org_domain=" ACME.okta.com ",
|
|
client_id=OKTA_CLIENT_ID,
|
|
private_key_file=str(key_file),
|
|
)
|
|
assert session.org_domain == OKTA_ORG_DOMAIN
|
|
|
|
def test_accepts_private_key_via_content_arg(self, _clear_okta_env):
|
|
session = OktaProvider.setup_session(
|
|
org_domain=OKTA_ORG_DOMAIN,
|
|
client_id=OKTA_CLIENT_ID,
|
|
private_key=OKTA_PRIVATE_KEY,
|
|
)
|
|
assert session.private_key == OKTA_PRIVATE_KEY
|
|
|
|
def test_accepts_private_key_via_env_var(self, monkeypatch):
|
|
monkeypatch.setenv("OKTA_PRIVATE_KEY", OKTA_PRIVATE_KEY)
|
|
monkeypatch.delenv("OKTA_PRIVATE_KEY_FILE", raising=False)
|
|
session = OktaProvider.setup_session(
|
|
org_domain=OKTA_ORG_DOMAIN,
|
|
client_id=OKTA_CLIENT_ID,
|
|
)
|
|
assert session.private_key == OKTA_PRIVATE_KEY
|
|
|
|
def test_content_takes_precedence_over_file(self, _clear_okta_env, tmp_path):
|
|
# File has stale content; explicit content arg should win.
|
|
key_file = tmp_path / "stale.pem"
|
|
key_file.write_text("STALE CONTENT FROM FILE")
|
|
fresh_key = "-----BEGIN PRIVATE KEY-----\nFRESH\n-----END PRIVATE KEY-----"
|
|
session = OktaProvider.setup_session(
|
|
org_domain=OKTA_ORG_DOMAIN,
|
|
client_id=OKTA_CLIENT_ID,
|
|
private_key=fresh_key,
|
|
private_key_file=str(key_file),
|
|
)
|
|
assert session.private_key == fresh_key
|
|
|
|
|
|
class Test_OktaProvider_setup_identity:
|
|
def _session(self, tmp_path):
|
|
key_file = tmp_path / "key.pem"
|
|
key_file.write_text(OKTA_PRIVATE_KEY)
|
|
return OktaProvider.setup_session(
|
|
org_domain=OKTA_ORG_DOMAIN,
|
|
client_id=OKTA_CLIENT_ID,
|
|
private_key_file=str(key_file),
|
|
)
|
|
|
|
def test_synthesizes_identity_and_probes_successfully(
|
|
self, _clear_okta_env, tmp_path
|
|
):
|
|
session = self._session(tmp_path)
|
|
|
|
async def fake_list_policies(*_a, **_k):
|
|
return ([], mock.MagicMock(headers={}), None)
|
|
|
|
with mock.patch(
|
|
"prowler.providers.okta.okta_provider.OktaSDKClient"
|
|
) as mocked_client_cls:
|
|
mocked = mock.MagicMock()
|
|
mocked.list_policies = fake_list_policies
|
|
mocked_client_cls.return_value = mocked
|
|
identity = OktaProvider.setup_identity(session)
|
|
|
|
assert identity.org_domain == OKTA_ORG_DOMAIN
|
|
assert identity.client_id == OKTA_CLIENT_ID
|
|
|
|
def test_populates_granted_scopes_from_access_token_scp_claim(
|
|
self, _clear_okta_env, tmp_path
|
|
):
|
|
session = self._session(tmp_path)
|
|
|
|
async def fake_list_policies(*_a, **_k):
|
|
return ([], mock.MagicMock(headers={}), None)
|
|
|
|
with mock.patch(
|
|
"prowler.providers.okta.okta_provider.OktaSDKClient"
|
|
) as mocked_client_cls:
|
|
mocked = mock.MagicMock()
|
|
mocked.list_policies = fake_list_policies
|
|
mocked._request_executor._oauth._access_token = _make_jwt(
|
|
{"scp": ["okta.policies.read", "okta.brands.read"]}
|
|
)
|
|
mocked_client_cls.return_value = mocked
|
|
identity = OktaProvider.setup_identity(session)
|
|
|
|
assert identity.granted_scopes == [
|
|
"okta.policies.read",
|
|
"okta.brands.read",
|
|
]
|
|
|
|
def test_granted_scopes_empty_when_token_unavailable(
|
|
self, _clear_okta_env, tmp_path
|
|
):
|
|
session = self._session(tmp_path)
|
|
|
|
async def fake_list_policies(*_a, **_k):
|
|
return ([], mock.MagicMock(headers={}), None)
|
|
|
|
with mock.patch(
|
|
"prowler.providers.okta.okta_provider.OktaSDKClient"
|
|
) as mocked_client_cls:
|
|
mocked = mock.MagicMock()
|
|
mocked.list_policies = fake_list_policies
|
|
mocked._request_executor._oauth._access_token = None
|
|
mocked_client_cls.return_value = mocked
|
|
identity = OktaProvider.setup_identity(session)
|
|
|
|
assert identity.granted_scopes == []
|
|
|
|
def test_raises_invalid_credentials_when_probe_returns_error(
|
|
self, _clear_okta_env, tmp_path
|
|
):
|
|
session = self._session(tmp_path)
|
|
|
|
async def failing_list_policies(*_a, **_k):
|
|
return ([], None, Exception("E0000011: Invalid token"))
|
|
|
|
with mock.patch(
|
|
"prowler.providers.okta.okta_provider.OktaSDKClient"
|
|
) as mocked_client_cls:
|
|
mocked = mock.MagicMock()
|
|
mocked.list_policies = failing_list_policies
|
|
mocked_client_cls.return_value = mocked
|
|
with pytest.raises(OktaInvalidCredentialsError):
|
|
OktaProvider.setup_identity(session)
|
|
|
|
def test_raises_insufficient_permissions_on_scope_error(
|
|
self, _clear_okta_env, tmp_path
|
|
):
|
|
session = self._session(tmp_path)
|
|
|
|
async def failing_list_policies(*_a, **_k):
|
|
return ([], None, Exception("invalid_scope: policies.read missing"))
|
|
|
|
with mock.patch(
|
|
"prowler.providers.okta.okta_provider.OktaSDKClient"
|
|
) as mocked_client_cls:
|
|
mocked = mock.MagicMock()
|
|
mocked.list_policies = failing_list_policies
|
|
mocked_client_cls.return_value = mocked
|
|
with pytest.raises(OktaInsufficientPermissionsError):
|
|
OktaProvider.setup_identity(session)
|
|
|
|
def test_raises_insufficient_permissions_on_forbidden(
|
|
self, _clear_okta_env, tmp_path
|
|
):
|
|
session = self._session(tmp_path)
|
|
|
|
async def failing_list_policies(*_a, **_k):
|
|
return ([], None, Exception("403 Forbidden"))
|
|
|
|
with mock.patch(
|
|
"prowler.providers.okta.okta_provider.OktaSDKClient"
|
|
) as mocked_client_cls:
|
|
mocked = mock.MagicMock()
|
|
mocked.list_policies = failing_list_policies
|
|
mocked_client_cls.return_value = mocked
|
|
with pytest.raises(OktaInsufficientPermissionsError):
|
|
OktaProvider.setup_identity(session)
|
|
|
|
def test_raises_insufficient_permissions_on_consent_required(
|
|
self, _clear_okta_env, tmp_path
|
|
):
|
|
# When zero requested scopes are consented on the service app, Okta
|
|
# rejects the token request with HTTP 400 `consent_required` rather
|
|
# than `invalid_scope` — must still be classified as a permission
|
|
# gap so the user is pointed at the Okta API Scopes tab, not at
|
|
# credential troubleshooting.
|
|
session = self._session(tmp_path)
|
|
|
|
async def failing_list_policies(*_a, **_k):
|
|
return (
|
|
[],
|
|
None,
|
|
Exception(
|
|
"Okta HTTP 400 consent_required You are not allowed any "
|
|
"of the requested scopes."
|
|
),
|
|
)
|
|
|
|
with mock.patch(
|
|
"prowler.providers.okta.okta_provider.OktaSDKClient"
|
|
) as mocked_client_cls:
|
|
mocked = mock.MagicMock()
|
|
mocked.list_policies = failing_list_policies
|
|
mocked_client_cls.return_value = mocked
|
|
with pytest.raises(OktaInsufficientPermissionsError):
|
|
OktaProvider.setup_identity(session)
|
|
|
|
def test_wraps_unexpected_errors_in_setup_identity_error(
|
|
self, _clear_okta_env, tmp_path
|
|
):
|
|
session = self._session(tmp_path)
|
|
|
|
async def boom(*_a, **_k):
|
|
raise RuntimeError("network down")
|
|
|
|
with mock.patch(
|
|
"prowler.providers.okta.okta_provider.OktaSDKClient"
|
|
) as mocked_client_cls:
|
|
mocked = mock.MagicMock()
|
|
mocked.list_policies = boom
|
|
mocked_client_cls.return_value = mocked
|
|
with pytest.raises(OktaSetUpIdentityError):
|
|
OktaProvider.setup_identity(session)
|
|
|
|
|
|
def _mock_setup_paths():
|
|
"""Patches that bypass the real SDK during provider construction."""
|
|
session = OktaSession(
|
|
org_domain=OKTA_ORG_DOMAIN,
|
|
client_id=OKTA_CLIENT_ID,
|
|
scopes=list(DEFAULT_SCOPES),
|
|
private_key=OKTA_PRIVATE_KEY,
|
|
)
|
|
identity = OktaIdentityInfo(org_domain=OKTA_ORG_DOMAIN, client_id=OKTA_CLIENT_ID)
|
|
return (
|
|
mock.patch.object(OktaProvider, "validate_arguments"),
|
|
mock.patch.object(OktaProvider, "setup_session", return_value=session),
|
|
mock.patch.object(OktaProvider, "setup_identity", return_value=identity),
|
|
)
|
|
|
|
|
|
class Test_OktaProvider_init:
|
|
def test_init_end_to_end(self, _clear_okta_env, tmp_path):
|
|
validate_p, session_p, identity_p = _mock_setup_paths()
|
|
with validate_p, session_p, identity_p:
|
|
provider = OktaProvider(
|
|
okta_org_domain=OKTA_ORG_DOMAIN,
|
|
okta_client_id=OKTA_CLIENT_ID,
|
|
okta_private_key_file="/tmp/key.pem",
|
|
)
|
|
|
|
assert provider.type == "okta"
|
|
assert provider.auth_method == "OAuth 2.0 (private-key JWT)"
|
|
assert provider.identity.org_domain == OKTA_ORG_DOMAIN
|
|
assert provider.identity.client_id == OKTA_CLIENT_ID
|
|
assert provider.session.scopes == DEFAULT_SCOPES
|
|
assert provider.audit_config is not None
|
|
assert provider.mutelist is not None
|
|
|
|
def test_default_max_retries_from_config_file(self, _clear_okta_env, tmp_path):
|
|
validate_p, session_p, identity_p = _mock_setup_paths()
|
|
with validate_p, session_p, identity_p:
|
|
provider = OktaProvider(
|
|
okta_org_domain=OKTA_ORG_DOMAIN,
|
|
okta_client_id=OKTA_CLIENT_ID,
|
|
okta_private_key_file="/tmp/key.pem",
|
|
)
|
|
|
|
# No CLI override: value comes from the bundled config.yaml default.
|
|
assert provider.audit_config["okta_max_retries"] == 5
|
|
|
|
def test_cli_retries_override_config_file(self, _clear_okta_env, tmp_path):
|
|
validate_p, session_p, identity_p = _mock_setup_paths()
|
|
with validate_p, session_p, identity_p:
|
|
provider = OktaProvider(
|
|
okta_org_domain=OKTA_ORG_DOMAIN,
|
|
okta_client_id=OKTA_CLIENT_ID,
|
|
okta_private_key_file="/tmp/key.pem",
|
|
okta_retries_max_attempts=9,
|
|
)
|
|
|
|
assert provider.audit_config["okta_max_retries"] == 9
|
|
|
|
def test_cli_retries_override_accepts_zero(self, _clear_okta_env, tmp_path):
|
|
validate_p, session_p, identity_p = _mock_setup_paths()
|
|
with validate_p, session_p, identity_p:
|
|
provider = OktaProvider(
|
|
okta_org_domain=OKTA_ORG_DOMAIN,
|
|
okta_client_id=OKTA_CLIENT_ID,
|
|
okta_private_key_file="/tmp/key.pem",
|
|
okta_retries_max_attempts=0,
|
|
)
|
|
|
|
# 0 disables retries and must not be treated as "unset".
|
|
assert provider.audit_config["okta_max_retries"] == 0
|
|
|
|
def test_rate_limiter_built_from_config_file_default(
|
|
self, _clear_okta_env, tmp_path
|
|
):
|
|
validate_p, session_p, identity_p = _mock_setup_paths()
|
|
with validate_p, session_p, identity_p:
|
|
provider = OktaProvider(
|
|
okta_org_domain=OKTA_ORG_DOMAIN,
|
|
okta_client_id=OKTA_CLIENT_ID,
|
|
okta_private_key_file="/tmp/key.pem",
|
|
)
|
|
|
|
# Bundled config.yaml enables throttling at 4 req/s.
|
|
assert provider.rate_limiter is not None
|
|
assert provider.audit_config["okta_requests_per_second"] == 4
|
|
|
|
def test_cli_requests_per_second_override(self, _clear_okta_env, tmp_path):
|
|
validate_p, session_p, identity_p = _mock_setup_paths()
|
|
with validate_p, session_p, identity_p:
|
|
provider = OktaProvider(
|
|
okta_org_domain=OKTA_ORG_DOMAIN,
|
|
okta_client_id=OKTA_CLIENT_ID,
|
|
okta_private_key_file="/tmp/key.pem",
|
|
okta_requests_per_second=10,
|
|
)
|
|
|
|
assert provider.audit_config["okta_requests_per_second"] == 10
|
|
assert provider.rate_limiter is not None
|
|
|
|
def test_requests_per_second_zero_disables_throttling(
|
|
self, _clear_okta_env, tmp_path
|
|
):
|
|
validate_p, session_p, identity_p = _mock_setup_paths()
|
|
with validate_p, session_p, identity_p:
|
|
provider = OktaProvider(
|
|
okta_org_domain=OKTA_ORG_DOMAIN,
|
|
okta_client_id=OKTA_CLIENT_ID,
|
|
okta_private_key_file="/tmp/key.pem",
|
|
okta_requests_per_second=0,
|
|
)
|
|
|
|
assert provider.rate_limiter is None
|
|
|
|
|
|
class Test_OktaProvider_test_connection:
|
|
def test_success(self, _clear_okta_env, tmp_path):
|
|
validate_p, session_p, identity_p = _mock_setup_paths()
|
|
with validate_p, session_p, identity_p:
|
|
connection = OktaProvider.test_connection(
|
|
okta_org_domain=OKTA_ORG_DOMAIN,
|
|
okta_client_id=OKTA_CLIENT_ID,
|
|
okta_private_key_file="/tmp/key.pem",
|
|
)
|
|
assert connection.is_connected is True
|
|
assert connection.error is None
|
|
|
|
def test_returns_error_when_raise_disabled(self, _clear_okta_env):
|
|
connection = OktaProvider.test_connection(raise_on_exception=False)
|
|
assert connection.is_connected is False
|
|
assert connection.error is not None
|
|
|
|
def test_raises_when_raise_enabled(self, _clear_okta_env):
|
|
with pytest.raises(OktaEnvironmentVariableError):
|
|
OktaProvider.test_connection()
|
|
|
|
def test_provider_id_match_succeeds(self, _clear_okta_env, tmp_path):
|
|
validate_p, session_p, identity_p = _mock_setup_paths()
|
|
with validate_p, session_p, identity_p:
|
|
connection = OktaProvider.test_connection(
|
|
okta_org_domain=OKTA_ORG_DOMAIN,
|
|
okta_client_id=OKTA_CLIENT_ID,
|
|
okta_private_key_file="/tmp/key.pem",
|
|
provider_id=OKTA_ORG_DOMAIN,
|
|
)
|
|
assert connection.is_connected is True
|
|
assert connection.error is None
|
|
|
|
def test_provider_id_match_is_case_insensitive(self, _clear_okta_env, tmp_path):
|
|
validate_p, session_p, identity_p = _mock_setup_paths()
|
|
with validate_p, session_p, identity_p:
|
|
connection = OktaProvider.test_connection(
|
|
okta_org_domain=OKTA_ORG_DOMAIN,
|
|
okta_client_id=OKTA_CLIENT_ID,
|
|
okta_private_key_file="/tmp/key.pem",
|
|
provider_id=OKTA_ORG_DOMAIN.upper(),
|
|
)
|
|
assert connection.is_connected is True
|
|
|
|
def test_provider_id_mismatch_raises(self, _clear_okta_env, tmp_path):
|
|
validate_p, session_p, identity_p = _mock_setup_paths()
|
|
with validate_p, session_p, identity_p:
|
|
with pytest.raises(OktaInvalidProviderIdError):
|
|
OktaProvider.test_connection(
|
|
okta_org_domain=OKTA_ORG_DOMAIN,
|
|
okta_client_id=OKTA_CLIENT_ID,
|
|
okta_private_key_file="/tmp/key.pem",
|
|
provider_id="other.okta.com",
|
|
)
|
|
|
|
def test_provider_id_mismatch_returns_error_when_raise_disabled(
|
|
self, _clear_okta_env, tmp_path
|
|
):
|
|
validate_p, session_p, identity_p = _mock_setup_paths()
|
|
with validate_p, session_p, identity_p:
|
|
connection = OktaProvider.test_connection(
|
|
okta_org_domain=OKTA_ORG_DOMAIN,
|
|
okta_client_id=OKTA_CLIENT_ID,
|
|
okta_private_key_file="/tmp/key.pem",
|
|
provider_id="other.okta.com",
|
|
raise_on_exception=False,
|
|
)
|
|
assert connection.is_connected is False
|
|
assert isinstance(connection.error, OktaInvalidProviderIdError)
|
|
|
|
|
|
class Test_OktaProvider_print_credentials:
|
|
def test_invokes_print_boxes_with_org_and_client(self, _clear_okta_env, tmp_path):
|
|
validate_p, session_p, identity_p = _mock_setup_paths()
|
|
with (
|
|
validate_p,
|
|
session_p,
|
|
identity_p,
|
|
mock.patch(
|
|
"prowler.providers.okta.okta_provider.print_boxes"
|
|
) as mock_print,
|
|
):
|
|
provider = OktaProvider(
|
|
okta_org_domain=OKTA_ORG_DOMAIN,
|
|
okta_client_id=OKTA_CLIENT_ID,
|
|
okta_private_key_file="/tmp/key.pem",
|
|
)
|
|
provider.print_credentials()
|
|
|
|
mock_print.assert_called_once()
|
|
rendered = " ".join(mock_print.call_args.args[0])
|
|
assert OKTA_ORG_DOMAIN in rendered
|
|
assert OKTA_CLIENT_ID in rendered
|
|
assert "OAuth 2.0" in rendered
|