|
5 | 5 | for pre-release versions and wildcard version strings. |
6 | 6 | """ |
7 | 7 |
|
| 8 | +import logging |
| 9 | +import re |
8 | 10 | import semver |
| 11 | +import requests |
| 12 | +from requests.auth import HTTPBasicAuth |
| 13 | + |
| 14 | +logger = logging.getLogger(__name__) |
9 | 15 |
|
10 | 16 |
|
11 | 17 | def isVersionBefore(_compare_to_version, _current_version): |
@@ -74,3 +80,92 @@ def isVersionEqualOrAfter(_compare_to_version, _current_version): |
74 | 80 | current_version = semver.VersionInfo.parse(strippedVersion) |
75 | 81 | compareToVersion = semver.VersionInfo.parse(_compare_to_version) |
76 | 82 | return current_version.compare(compareToVersion) >= 0 |
| 83 | + |
| 84 | + |
| 85 | +def validateIBMEntitlementKey(entitlementKey: str, repository: str = "cp/mas/coreapi", timeout: int = 30) -> bool: |
| 86 | + """Validate IBM entitlement key against cp.icr.io registry. |
| 87 | +
|
| 88 | + This function validates an IBM entitlement key by attempting to obtain |
| 89 | + an authentication token from the IBM Container Registry and verifying |
| 90 | + access to the specified repository. |
| 91 | +
|
| 92 | + Args: |
| 93 | + entitlementKey (str): IBM entitlement key to validate. |
| 94 | + repository (str, optional): Repository to test access against. Defaults to "cp/mas/coreapi". |
| 95 | + timeout (int, optional): Request timeout in seconds. Defaults to 30. |
| 96 | +
|
| 97 | + Returns: |
| 98 | + bool: True if key is valid and grants access to the repository, False otherwise. |
| 99 | +
|
| 100 | + Raises: |
| 101 | + requests.exceptions.RequestException: If network request fails. |
| 102 | + """ |
| 103 | + try: |
| 104 | + registry_url = f"https://cp.icr.io/v2/{repository}/tags/list" |
| 105 | + logger.debug(f"Validating entitlement key against {repository}") |
| 106 | + |
| 107 | + # First request without auth to get the auth challenge |
| 108 | + response = requests.get(registry_url, timeout=timeout) |
| 109 | + |
| 110 | + if response.status_code == 401: |
| 111 | + # Parse WWW-Authenticate header to get token endpoint |
| 112 | + auth_header = response.headers.get("WWW-Authenticate", "") |
| 113 | + logger.debug(f"Auth challenge received: {auth_header[:100]}...") |
| 114 | + |
| 115 | + # Extract realm and service from auth header |
| 116 | + realm_match = re.search(r'realm="([^"]+)"', auth_header) |
| 117 | + service_match = re.search(r'service="([^"]+)"', auth_header) |
| 118 | + scope_match = re.search(r'scope="([^"]+)"', auth_header) |
| 119 | + |
| 120 | + if not realm_match: |
| 121 | + logger.error("Could not parse authentication realm") |
| 122 | + return False |
| 123 | + |
| 124 | + token_url = realm_match.group(1) |
| 125 | + params = {} |
| 126 | + |
| 127 | + if service_match: |
| 128 | + params["service"] = service_match.group(1) |
| 129 | + if scope_match: |
| 130 | + params["scope"] = scope_match.group(1) |
| 131 | + else: |
| 132 | + params["scope"] = f"repository:{repository}:pull" |
| 133 | + |
| 134 | + logger.debug(f"Token endpoint: {token_url}") |
| 135 | + |
| 136 | + # Get authentication token |
| 137 | + token_response = requests.get(token_url, params=params, auth=HTTPBasicAuth("cp", entitlementKey), timeout=timeout) |
| 138 | + |
| 139 | + if token_response.status_code != 200: |
| 140 | + logger.error(f"Failed to get token (HTTP {token_response.status_code})") |
| 141 | + return False |
| 142 | + |
| 143 | + token_data = token_response.json() |
| 144 | + token = token_data.get("token") or token_data.get("access_token") |
| 145 | + |
| 146 | + if not token: |
| 147 | + logger.error("No token received - invalid entitlement key") |
| 148 | + return False |
| 149 | + |
| 150 | + # Validate token by accessing registry |
| 151 | + logger.debug("Validating token against registry") |
| 152 | + headers = {"Authorization": f"Bearer {token}"} |
| 153 | + validate_response = requests.get(registry_url, headers=headers, timeout=timeout) |
| 154 | + |
| 155 | + if validate_response.status_code == 200: |
| 156 | + logger.info(f"Valid entitlement key with access to {repository}") |
| 157 | + return True |
| 158 | + else: |
| 159 | + logger.error(f"Token validation failed (HTTP {validate_response.status_code})") |
| 160 | + return False |
| 161 | + |
| 162 | + elif response.status_code == 200: |
| 163 | + logger.info("Registry accessible without authentication (public repository)") |
| 164 | + return True |
| 165 | + else: |
| 166 | + logger.error(f"Unexpected response (HTTP {response.status_code})") |
| 167 | + return False |
| 168 | + |
| 169 | + except requests.exceptions.RequestException as e: |
| 170 | + logger.error(f"Request failed: {e}") |
| 171 | + raise |
0 commit comments