|
10 | 10 | # Builtins |
11 | 11 | import json |
12 | 12 | import logging |
| 13 | +import re |
13 | 14 | from typing import Optional, Tuple |
14 | 15 |
|
| 16 | +from packaging.version import Version |
| 17 | + |
15 | 18 | from conjur_api.errors.errors import ResourceNotFoundException, MissingRequiredParameterException, HttpStatusError |
16 | 19 | from conjur_api.http.api import Api |
17 | 20 | from conjur_api.interface.authentication_strategy_interface import AuthenticationStrategyInterface |
@@ -39,7 +42,7 @@ class Client: |
39 | 42 | try: |
40 | 43 | integration_version = version("conjur_api") |
41 | 44 | except PackageNotFoundError: |
42 | | - integration_version = 'No Version Found' |
| 45 | + integration_version = '0.0.dev' |
43 | 46 | integration_type = 'cybr-secretsmanager' |
44 | 47 | vendor_name = 'CyberArk' |
45 | 48 | vendor_version = None |
@@ -95,6 +98,7 @@ def __init__( |
95 | 98 | self.ssl_verification_mode = ssl_verification_mode |
96 | 99 | self.connection_info = connection_info |
97 | 100 | self.debug = debug |
| 101 | + self.conjur_version = None # Cache for server version |
98 | 102 | self._api = self._create_api(http_debug, authn_strategy) |
99 | 103 |
|
100 | 104 | self.set_telemetry_header() |
@@ -293,22 +297,28 @@ async def set(self, variable_id: str, value: str) -> str: |
293 | 297 | """ |
294 | 298 | await self._api.set_variable(variable_id, value) |
295 | 299 |
|
296 | | - async def load_policy_file(self, policy_name: str, policy_file: str, dry_run: False) -> dict: |
| 300 | + async def load_policy_file(self, policy_name: str, policy_file: str, dry_run: bool = False) -> dict: |
297 | 301 | """ |
298 | 302 | Applies a file-based policy to the Conjur instance |
299 | 303 | """ |
| 304 | + if dry_run: |
| 305 | + await self._validate_dry_run_support() |
300 | 306 | return await self._api.load_policy_file(policy_name, policy_file, dry_run) |
301 | 307 |
|
302 | | - async def replace_policy_file(self, policy_name: str, policy_file: str, dry_run: False) -> dict: |
| 308 | + async def replace_policy_file(self, policy_name: str, policy_file: str, dry_run: bool = False) -> dict: |
303 | 309 | """ |
304 | 310 | Replaces a file-based policy defined in the Conjur instance |
305 | 311 | """ |
| 312 | + if dry_run: |
| 313 | + await self._validate_dry_run_support() |
306 | 314 | return await self._api.replace_policy_file(policy_name, policy_file, dry_run) |
307 | 315 |
|
308 | | - async def update_policy_file(self, policy_name: str, policy_file: str, dry_run: False) -> dict: |
| 316 | + async def update_policy_file(self, policy_name: str, policy_file: str, dry_run: bool = False) -> dict: |
309 | 317 | """ |
310 | 318 | Replaces a file-based policy defined in the Conjur instance |
311 | 319 | """ |
| 320 | + if dry_run: |
| 321 | + await self._validate_dry_run_support() |
312 | 322 | return await self._api.update_policy_file(policy_name, policy_file, dry_run) |
313 | 323 |
|
314 | 324 | async def rotate_other_api_key(self, resource: Resource) -> str: |
@@ -350,6 +360,125 @@ async def get_server_info(self): |
350 | 360 | else: |
351 | 361 | raise |
352 | 362 |
|
| 363 | + def _is_version_less_than(self, version1: str, version2: str) -> bool: |
| 364 | + """ |
| 365 | + Checks if version1 is less than version2. |
| 366 | + @param version1: First version string (e.g., "1.21.1", "1.21.1-beta", "1.24.0-1049") |
| 367 | + @param version2: Second version string (e.g., "1.21.1") |
| 368 | + @return: True if version1 < version2, False otherwise |
| 369 | + """ |
| 370 | + return Version(version1) < Version(version2) |
| 371 | + |
| 372 | + def _is_conjur_cloud_url(self, url: str) -> bool: |
| 373 | + """ |
| 374 | + Checks if the URL is a Conjur Cloud (Secrets Manager SaaS) URL. |
| 375 | + Matches the Go regex pattern: (\\.secretsmgr|-secretsmanager) followed by one of the cloud suffixes. |
| 376 | + @param url: The Conjur URL to check |
| 377 | + @return: True if the URL is a Conjur Cloud URL, False otherwise |
| 378 | + """ |
| 379 | + if not url: |
| 380 | + return False |
| 381 | + |
| 382 | + # ConjurCloudSuffixes - all possible Secrets Manager SaaS URL suffixes |
| 383 | + conjur_cloud_suffixes = [ |
| 384 | + ".cyberark.cloud", |
| 385 | + ".integration-cyberark.cloud", |
| 386 | + ".test-cyberark.cloud", |
| 387 | + ".dev-cyberark.cloud", |
| 388 | + ".cyberark-everest-integdev.cloud", |
| 389 | + ".cyberark-everest-pre-prod.cloud", |
| 390 | + ".sandbox-cyberark.cloud", |
| 391 | + ".pt-cyberark.cloud", |
| 392 | + ] |
| 393 | + |
| 394 | + # Build regex pattern: (\\.secretsmgr|-secretsmanager) followed by one of the suffixes |
| 395 | + suffixes_pattern = "|".join(re.escape(suffix) for suffix in conjur_cloud_suffixes) |
| 396 | + pattern = rf"(\.secretsmgr|-secretsmanager)({suffixes_pattern})" |
| 397 | + |
| 398 | + conjur_cloud_regexp = re.compile(pattern, re.IGNORECASE) |
| 399 | + return bool(conjur_cloud_regexp.search(url)) |
| 400 | + |
| 401 | + async def server_version(self) -> str: |
| 402 | + """ |
| 403 | + Retrieves the Conjur server version, either from the '/info' endpoint in Secrets Manager Self-Hosted, |
| 404 | + or from the root endpoint in Conjur OSS. The version returned corresponds to the Conjur OSS version, |
| 405 | + which in Conjur Enterprise is the version of the 'possum' service. |
| 406 | + |
| 407 | + The version is cached after the first retrieval to avoid making multiple requests. |
| 408 | +
|
| 409 | + @return: Server version string |
| 410 | + @raises: Exception if unable to retrieve server version or if running against Conjur Cloud |
| 411 | + """ |
| 412 | + # Return cached version if available |
| 413 | + if self.conjur_version is not None: |
| 414 | + return self.conjur_version |
| 415 | + |
| 416 | + url = self.connection_info.conjur_url |
| 417 | + |
| 418 | + if self._is_conjur_cloud_url(url): |
| 419 | + raise Exception("Unable to retrieve server version: not supported in Secrets Manager SaaS") |
| 420 | + |
| 421 | + # Try to get enterprise server info first |
| 422 | + enterprise_error = None |
| 423 | + try: |
| 424 | + info = await self.get_server_info() |
| 425 | + # Return the version of the 'possum' service, which corresponds to the Conjur OSS version |
| 426 | + if isinstance(info, dict) and 'services' in info: |
| 427 | + services = info.get('services', {}) |
| 428 | + if 'possum' in services: |
| 429 | + possum_service = services.get('possum', {}) |
| 430 | + if isinstance(possum_service, dict) and 'version' in possum_service: |
| 431 | + self.conjur_version = possum_service['version'] |
| 432 | + return self.conjur_version |
| 433 | + except Exception as err: |
| 434 | + # If enterprise info fails, try root endpoint |
| 435 | + enterprise_error = err |
| 436 | + |
| 437 | + # Try to get version from root endpoint (Conjur OSS) |
| 438 | + try: |
| 439 | + version = await self._api.get_server_version_from_root() |
| 440 | + if version: |
| 441 | + self.conjur_version = version |
| 442 | + return self.conjur_version |
| 443 | + except Exception as root_err: |
| 444 | + # Both methods failed, raise an error with details |
| 445 | + error_msg = "failed to retrieve server version" |
| 446 | + if enterprise_error: |
| 447 | + error_msg += f": enterprise info error - {enterprise_error}" |
| 448 | + if root_err: |
| 449 | + error_msg += f", root endpoint error - {root_err}" |
| 450 | + raise Exception(error_msg) from root_err |
| 451 | + |
| 452 | + raise Exception("failed to retrieve server version: both enterprise info and root endpoint failed") |
| 453 | + |
| 454 | + async def _validate_dry_run_support(self): |
| 455 | + """ |
| 456 | + Validates that dry_run is supported by checking: |
| 457 | + 1. The server is not Conjur Cloud (SaaS) |
| 458 | + 2. The server version is >= 1.21.1 |
| 459 | + |
| 460 | + @raises: Exception if dry_run is not supported |
| 461 | + """ |
| 462 | + url = self.connection_info.conjur_url |
| 463 | + |
| 464 | + # Check if it's Conjur Cloud |
| 465 | + if self._is_conjur_cloud_url(url): |
| 466 | + raise Exception("dry_run is not supported in Secrets Manager SaaS") |
| 467 | + |
| 468 | + # Check server version |
| 469 | + try: |
| 470 | + server_version = await self.server_version() |
| 471 | + min_version = "1.21.1" |
| 472 | + |
| 473 | + if self._is_version_less_than(server_version, min_version): |
| 474 | + raise Exception(f"dry_run requires Conjur server version {min_version} or higher, but server version is {server_version}") |
| 475 | + except Exception as err: |
| 476 | + # If we can't get the version, we should still raise an error |
| 477 | + # but include the original error message |
| 478 | + error_msg = str(err) |
| 479 | + raise Exception(f"Unable to validate dry_run support: {error_msg}") from err |
| 480 | + |
| 481 | + |
353 | 482 | async def change_personal_password( |
354 | 483 | self, logged_in_user: str, current_password: str, |
355 | 484 | new_password: str) -> str: |
|
0 commit comments