pax_global_header00006660000000000000000000000064150225664500014517gustar00rootroot0000000000000052 comment=7ebd71dcb20846b59b938e7df810dfa75df45bec IBM-python-sdk-core-7ebd71d/000077500000000000000000000000001502256645000156515ustar00rootroot00000000000000IBM-python-sdk-core-7ebd71d/.bumpversion.toml000066400000000000000000000007171502256645000212020ustar00rootroot00000000000000[tool.bumpversion] current_version = "3.24.2" commit = true [[tool.bumpversion.files]] filename = "ibm_cloud_sdk_core/version.py" search = "__version__ = '{current_version}'" replace = "__version__ = '{new_version}'" [[tool.bumpversion.files]] filename = "pyproject.toml" search = "version = \"{current_version}\"" replace = "version = \"{new_version}\"" [[tool.bumpversion.files]] filename = "README.md" search = "{current_version}" replace = "{new_version}" IBM-python-sdk-core-7ebd71d/.github/000077500000000000000000000000001502256645000172115ustar00rootroot00000000000000IBM-python-sdk-core-7ebd71d/.github/workflows/000077500000000000000000000000001502256645000212465ustar00rootroot00000000000000IBM-python-sdk-core-7ebd71d/.github/workflows/build.yaml000066400000000000000000000044621502256645000232370ustar00rootroot00000000000000# This workflow will build and unit test the project. # If the workflow is running on the "main" branch, then # semantic-release is also run to create a new release (if # warranted by the new commits being built). name: build on: push: branches: ['**'] pull_request: branches: ['**'] workflow_dispatch: # Allow workflow to be triggered manually. jobs: detect-secrets: if: "!contains(github.event.head_commit.message, '[skip ci]')" name: detect-secrets runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v4 - name: Setup Python uses: actions/setup-python@v5 with: python-version: 3.13 - name: Install detect-secrets run: | pip install --upgrade "git+https://github.com/ibm/detect-secrets.git@master#egg=detect-secrets" - name: Run detect-secrets run: | detect-secrets scan --update .secrets.baseline detect-secrets -v audit --report --fail-on-unaudited --fail-on-live --fail-on-audited-real .secrets.baseline build: name: build-test (python ${{ matrix.python-version }}) needs: detect-secrets runs-on: ubuntu-latest strategy: matrix: python-version: ['3.9', '3.10', '3.11', '3.12', '3.13'] steps: - name: Checkout repository uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Build & Test run: make ci create-release: name: semantic-release needs: build if: "github.ref_name == 'main' && github.event_name != 'pull_request'" runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v4 with: persist-credentials: false - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: 22 - name: Setup Python uses: actions/setup-python@v5 with: python-version: 3.13 - name: Install Publishing Tools run: | pip install bump-my-version npm install - name: Run semantic-release env: GH_TOKEN: ${{ secrets.GH_TOKEN }} run: npm run semantic-release IBM-python-sdk-core-7ebd71d/.github/workflows/publish.yaml000066400000000000000000000014371502256645000236050ustar00rootroot00000000000000# This workflow is responsible for: # - publishing artifacts to Maven Central # - building and publishing javadocs to the git repository. # It is triggered when a new release is created. name: publish on: release: types: [created] workflow_dispatch: # Allow this workflow to be triggered manually jobs: publish: name: publish-pypi runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v4 with: persist-credentials: false - name: Setup Python uses: actions/setup-python@v5 with: python-version: 3.13 - name: Build and publish distribution env: TWINE_PASSWORD: ${{ secrets.TWINE_PASSWORD }} run: | make ci make publish-release IBM-python-sdk-core-7ebd71d/.gitignore000066400000000000000000000016311502256645000176420ustar00rootroot00000000000000# IDE OS .DS_Store .idea # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # Distribution / packaging .Python env/ build/ develop-eggs/ dist/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ *.egg-info/ .installed.cfg *.egg # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *,cover .vscode # virtual env venv*/ # python 3 virtual env python3/ *.env !resources/*.env .sfdx/tools/apex.db .pytest_cache/ /.project /.pydevproject /.settings/ # ignore detect secrets files .pre-commit-config.yaml .secrets.baseline # files produced by "npm install" commands during build package-lock.json node_modules/ IBM-python-sdk-core-7ebd71d/.npmrc000066400000000000000000000000221502256645000167630ustar00rootroot00000000000000package-lock=falseIBM-python-sdk-core-7ebd71d/.pylintrc000066400000000000000000000005701502256645000175200ustar00rootroot00000000000000# lint Python modules using external checkers. [MASTER] disable= bare-except, duplicate-code, missing-module-docstring, too-many-arguments, unnecessary-pass, no-member, consider-using-f-string, too-many-instance-attributes [TYPECHECK] ignored-classes= responses [FORMAT] # Maximum number of characters on a single line. max-line-length=120 IBM-python-sdk-core-7ebd71d/.releaserc000066400000000000000000000011461502256645000176210ustar00rootroot00000000000000{ "branches": "main", "debug": true, "plugins": [ "@semantic-release/commit-analyzer", "@semantic-release/release-notes-generator", "@semantic-release/changelog", [ "@semantic-release/exec", { "prepareCmd": "bump-my-version bump --allow-dirty --current-version ${lastRelease.version} --new-version ${nextRelease.version}" } ], [ "@semantic-release/git", { "assets" : [ "CHANGELOG.md" ], "message": "chore(release): ${nextRelease.version} release notes\n\n${nextRelease.notes}" } ], "@semantic-release/github" ] }IBM-python-sdk-core-7ebd71d/.secrets.baseline000066400000000000000000000411701502256645000211060ustar00rootroot00000000000000{ "exclude": { "files": "package-lock.json|^.secrets.baseline$", "lines": null }, "generated_at": "2025-05-30T15:41:43Z", "plugins_used": [ { "name": "AWSKeyDetector" }, { "name": "ArtifactoryDetector" }, { "base64_limit": 4.5, "name": "Base64HighEntropyString" }, { "name": "BasicAuthDetector" }, { "name": "BoxDetector" }, { "name": "CloudantDetector" }, { "ghe_instance": "github.ibm.com", "name": "GheDetector" }, { "hex_limit": 3, "name": "HexHighEntropyString" }, { "name": "IbmCloudIamDetector" }, { "name": "IbmCosHmacDetector" }, { "name": "JwtTokenDetector" }, { "keyword_exclude": null, "name": "KeywordDetector" }, { "name": "MailchimpDetector" }, { "name": "PrivateKeyDetector" }, { "name": "SlackDetector" }, { "name": "SoftlayerDetector" }, { "name": "StripeDetector" }, { "name": "TwilioKeyDetector" } ], "results": { "Authentication.md": [ { "hashed_secret": "91dfd9ddb4198affc5c194cd8ce6d338fde470e2", "is_secret": false, "is_verified": false, "line_number": 67, "type": "Secret Keyword", "verified_result": null }, { "hashed_secret": "4f51cde3ac0a5504afa4bc06859b098366592c19", "is_secret": false, "is_verified": false, "line_number": 208, "type": "Secret Keyword", "verified_result": null }, { "hashed_secret": "e87559ed7decb62d0733ae251ae58d42a55291d8", "is_secret": false, "is_verified": false, "line_number": 210, "type": "Secret Keyword", "verified_result": null }, { "hashed_secret": "12f4a68ed3d0863e56497c9cdb1e2e4e91d5cb68", "is_secret": false, "is_verified": false, "line_number": 274, "type": "Secret Keyword", "verified_result": null }, { "hashed_secret": "c837b75d7cd93ef9c2243ca28d6e5156259fd253", "is_secret": false, "is_verified": false, "line_number": 278, "type": "Secret Keyword", "verified_result": null }, { "hashed_secret": "98635b2eaa2379f28cd6d72a38299f286b81b459", "is_secret": false, "is_verified": false, "line_number": 505, "type": "Secret Keyword", "verified_result": null }, { "hashed_secret": "47fcf185ee7e15fe05cae31fbe9e4ebe4a06a40d", "is_secret": false, "is_verified": false, "line_number": 694, "type": "Secret Keyword", "verified_result": null } ], "CHANGELOG.md": [ { "hashed_secret": "bc2f74c22f98f7b6ffbc2f67453dbfa99bce9a32", "is_secret": false, "is_verified": false, "line_number": 6, "type": "Secret Keyword", "verified_result": null } ], "ibm_cloud_sdk_core/authenticators/authenticator.py": [ { "hashed_secret": "fdee05598fdd57ff8e9ae29e92c25a04f2c52fa6", "is_secret": false, "is_verified": false, "line_number": 31, "type": "Secret Keyword", "verified_result": null } ], "resources/ibm-credentials-basic.env": [ { "hashed_secret": "5eb942810a75ebc850972a89285d570d484c89c4", "is_secret": false, "is_verified": false, "line_number": 2, "type": "Secret Keyword", "verified_result": null } ], "resources/ibm-credentials-container.env": [ { "hashed_secret": "8eb25c6fb51add4e1c21199b1066b273f10b53be", "is_secret": false, "is_verified": false, "line_number": 8, "type": "Secret Keyword", "verified_result": null } ], "resources/ibm-credentials-cp4d.env": [ { "hashed_secret": "5eb942810a75ebc850972a89285d570d484c89c4", "is_secret": false, "is_verified": false, "line_number": 2, "type": "Secret Keyword", "verified_result": null } ], "resources/ibm-credentials-external.env": [ { "hashed_secret": "b9cad336062c0dc3bb30145b1a6697fccfe755a6", "is_secret": false, "is_verified": false, "line_number": 1, "type": "Secret Keyword", "verified_result": null } ], "resources/ibm-credentials-gzip.env": [ { "hashed_secret": "ce49dd46e23153d6593eccd68534b9f1465d5bbd", "is_secret": false, "is_verified": false, "line_number": 1, "type": "Secret Keyword", "verified_result": null } ], "resources/ibm-credentials-iam-assume.env": [ { "hashed_secret": "f2e7745f43b0ef0e2c2faf61d6c6a28be2965750", "is_secret": false, "is_verified": false, "line_number": 2, "type": "Secret Keyword", "verified_result": null } ], "resources/ibm-credentials-iam.env": [ { "hashed_secret": "b9cad336062c0dc3bb30145b1a6697fccfe755a6", "is_secret": false, "is_verified": false, "line_number": 1, "type": "Secret Keyword", "verified_result": null } ], "resources/ibm-credentials-mcsp.env": [ { "hashed_secret": "f2e7745f43b0ef0e2c2faf61d6c6a28be2965750", "is_secret": false, "is_verified": false, "line_number": 2, "type": "Secret Keyword", "verified_result": null } ], "resources/ibm-credentials-mcspv2.env": [ { "hashed_secret": "f2e7745f43b0ef0e2c2faf61d6c6a28be2965750", "is_secret": false, "is_verified": false, "line_number": 23, "type": "Secret Keyword", "verified_result": null } ], "resources/ibm-credentials-retry.env": [ { "hashed_secret": "ce49dd46e23153d6593eccd68534b9f1465d5bbd", "is_secret": false, "is_verified": false, "line_number": 1, "type": "Secret Keyword", "verified_result": null } ], "resources/ibm-credentials.env": [ { "hashed_secret": "b9cad336062c0dc3bb30145b1a6697fccfe755a6", "is_secret": false, "is_verified": false, "line_number": 1, "type": "Secret Keyword", "verified_result": null }, { "hashed_secret": "2863fa4b5510c46afc2bd2998dfbc0cf3d6df032", "is_secret": false, "is_verified": false, "line_number": 16, "type": "Secret Keyword", "verified_result": null }, { "hashed_secret": "407bb564bd0e4d31368cb53004c9c42adf8f7be8", "is_secret": false, "is_verified": false, "line_number": 18, "type": "Secret Keyword", "verified_result": null } ], "resources/test_ssl.key": [ { "hashed_secret": "1348b145fa1a555461c1b790a2f66614781091e9", "is_secret": false, "is_verified": false, "line_number": 1, "type": "Private Key", "verified_result": null } ], "test/test_base_service.py": [ { "hashed_secret": "da2f27d2c57a0e1ed2dc3a34b4ef02faf2f7a4c2", "is_secret": false, "is_verified": false, "line_number": 133, "type": "Hex High Entropy String", "verified_result": null } ], "test/test_container_authenticator.py": [ { "hashed_secret": "37e94c31b6a756ba2afd2fe9a9765172cd79ac47", "is_secret": false, "is_verified": false, "line_number": 95, "type": "Secret Keyword", "verified_result": null } ], "test/test_container_token_manager.py": [ { "hashed_secret": "c8f0df25bade89c1873f5f01b85bcfb921443ac6", "is_secret": false, "is_verified": false, "line_number": 34, "type": "JSON Web Token", "verified_result": null }, { "hashed_secret": "f06e1073ca9afdd800a2cf27f944d06530b5b755", "is_secret": false, "is_verified": false, "line_number": 35, "type": "JSON Web Token", "verified_result": null }, { "hashed_secret": "360c23c1ac7d9d6dad1d0710606b0df9de6e1a18", "is_secret": false, "is_verified": false, "line_number": 39, "type": "Secret Keyword", "verified_result": null }, { "hashed_secret": "62cdb7020ff920e5aa642c3d4066950dd1f01f4d", "is_secret": false, "is_verified": false, "line_number": 146, "type": "Secret Keyword", "verified_result": null } ], "test/test_cp4d_authenticator.py": [ { "hashed_secret": "5eb942810a75ebc850972a89285d570d484c89c4", "is_secret": false, "is_verified": false, "line_number": 95, "type": "Secret Keyword", "verified_result": null }, { "hashed_secret": "da2f27d2c57a0e1ed2dc3a34b4ef02faf2f7a4c2", "is_secret": false, "is_verified": false, "line_number": 136, "type": "Hex High Entropy String", "verified_result": null } ], "test/test_cp4d_token_manager.py": [ { "hashed_secret": "da2f27d2c57a0e1ed2dc3a34b4ef02faf2f7a4c2", "is_secret": false, "is_verified": false, "line_number": 48, "type": "Hex High Entropy String", "verified_result": null } ], "test/test_get_authenticator.py": [ { "hashed_secret": "34a0a47a51d5bf739df0214450385e29ee7e9847", "is_secret": false, "is_verified": false, "line_number": 256, "type": "Secret Keyword", "verified_result": null }, { "hashed_secret": "f2e7745f43b0ef0e2c2faf61d6c6a28be2965750", "is_secret": false, "is_verified": false, "line_number": 267, "type": "Secret Keyword", "verified_result": null }, { "hashed_secret": "2863fa4b5510c46afc2bd2998dfbc0cf3d6df032", "is_secret": false, "is_verified": false, "line_number": 348, "type": "Secret Keyword", "verified_result": null }, { "hashed_secret": "b9cad336062c0dc3bb30145b1a6697fccfe755a6", "is_secret": false, "is_verified": false, "line_number": 409, "type": "Secret Keyword", "verified_result": null } ], "test/test_iam_assume_authenticator.py": [ { "hashed_secret": "4080eeeaf54faf879b9e8d99c49a8503f7e855bb", "is_secret": false, "is_verified": false, "line_number": 37, "type": "Secret Keyword", "verified_result": null }, { "hashed_secret": "37e94c31b6a756ba2afd2fe9a9765172cd79ac47", "is_secret": false, "is_verified": false, "line_number": 110, "type": "Secret Keyword", "verified_result": null }, { "hashed_secret": "da2f27d2c57a0e1ed2dc3a34b4ef02faf2f7a4c2", "is_secret": false, "is_verified": false, "line_number": 131, "type": "Hex High Entropy String", "verified_result": null } ], "test/test_iam_assume_token_manager.py": [ { "hashed_secret": "da2f27d2c57a0e1ed2dc3a34b4ef02faf2f7a4c2", "is_secret": false, "is_verified": false, "line_number": 63, "type": "Hex High Entropy String", "verified_result": null }, { "hashed_secret": "37e94c31b6a756ba2afd2fe9a9765172cd79ac47", "is_secret": false, "is_verified": false, "line_number": 206, "type": "Secret Keyword", "verified_result": null } ], "test/test_iam_authenticator.py": [ { "hashed_secret": "4080eeeaf54faf879b9e8d99c49a8503f7e855bb", "is_secret": false, "is_verified": false, "line_number": 75, "type": "Secret Keyword", "verified_result": null }, { "hashed_secret": "37e94c31b6a756ba2afd2fe9a9765172cd79ac47", "is_secret": false, "is_verified": false, "line_number": 97, "type": "Secret Keyword", "verified_result": null }, { "hashed_secret": "da2f27d2c57a0e1ed2dc3a34b4ef02faf2f7a4c2", "is_secret": false, "is_verified": false, "line_number": 117, "type": "Hex High Entropy String", "verified_result": null } ], "test/test_iam_token_manager.py": [ { "hashed_secret": "c8f0df25bade89c1873f5f01b85bcfb921443ac6", "is_secret": false, "is_verified": false, "line_number": 32, "type": "JSON Web Token", "verified_result": null }, { "hashed_secret": "f06e1073ca9afdd800a2cf27f944d06530b5b755", "is_secret": false, "is_verified": false, "line_number": 33, "type": "JSON Web Token", "verified_result": null }, { "hashed_secret": "da2f27d2c57a0e1ed2dc3a34b4ef02faf2f7a4c2", "is_secret": false, "is_verified": false, "line_number": 56, "type": "Hex High Entropy String", "verified_result": null }, { "hashed_secret": "b3f00e146afe19aab0069029b7fb3926ad756d26", "is_secret": false, "is_verified": false, "line_number": 134, "type": "Hex High Entropy String", "verified_result": null }, { "hashed_secret": "62cdb7020ff920e5aa642c3d4066950dd1f01f4d", "is_secret": false, "is_verified": false, "line_number": 196, "type": "Secret Keyword", "verified_result": null } ], "test/test_jwt_token_manager.py": [ { "hashed_secret": "da2f27d2c57a0e1ed2dc3a34b4ef02faf2f7a4c2", "is_secret": false, "is_verified": false, "line_number": 39, "type": "Hex High Entropy String", "verified_result": null } ], "test/test_mcsp_authenticator.py": [ { "hashed_secret": "da2f27d2c57a0e1ed2dc3a34b4ef02faf2f7a4c2", "is_secret": false, "is_verified": false, "line_number": 95, "type": "Hex High Entropy String", "verified_result": null }, { "hashed_secret": "f2e7745f43b0ef0e2c2faf61d6c6a28be2965750", "is_secret": false, "is_verified": false, "line_number": 151, "type": "Secret Keyword", "verified_result": null } ], "test/test_mcsp_token_manager.py": [ { "hashed_secret": "da2f27d2c57a0e1ed2dc3a34b4ef02faf2f7a4c2", "is_secret": false, "is_verified": false, "line_number": 50, "type": "Hex High Entropy String", "verified_result": null }, { "hashed_secret": "f2e7745f43b0ef0e2c2faf61d6c6a28be2965750", "is_secret": false, "is_verified": false, "line_number": 63, "type": "Secret Keyword", "verified_result": null }, { "hashed_secret": "ace1a5bf229c3af3f699369c6f2fa1a628692ab8", "is_secret": false, "is_verified": false, "line_number": 81, "type": "Secret Keyword", "verified_result": null } ], "test/test_mcspv2_authenticator.py": [ { "hashed_secret": "f2e7745f43b0ef0e2c2faf61d6c6a28be2965750", "is_secret": false, "is_verified": false, "line_number": 15, "type": "Secret Keyword", "verified_result": null }, { "hashed_secret": "da2f27d2c57a0e1ed2dc3a34b4ef02faf2f7a4c2", "is_secret": false, "is_verified": false, "line_number": 246, "type": "Hex High Entropy String", "verified_result": null }, { "hashed_secret": "ace1a5bf229c3af3f699369c6f2fa1a628692ab8", "is_secret": false, "is_verified": false, "line_number": 392, "type": "Secret Keyword", "verified_result": null } ], "test/test_vpc_instance_token_manager.py": [ { "hashed_secret": "c8f0df25bade89c1873f5f01b85bcfb921443ac6", "is_secret": false, "is_verified": false, "line_number": 32, "type": "JSON Web Token", "verified_result": null }, { "hashed_secret": "f06e1073ca9afdd800a2cf27f944d06530b5b755", "is_secret": false, "is_verified": false, "line_number": 33, "type": "JSON Web Token", "verified_result": null } ] }, "version": "0.13.1+ibm.62.dss", "word_list": { "file": null, "hash": null } } IBM-python-sdk-core-7ebd71d/.travis.yml000066400000000000000000000023101502256645000177560ustar00rootroot00000000000000language: python dist: jammy stages: - name: Build-Test if: tag IS blank - name: Semantic-Release if: (branch = main) AND (type IN (push, api)) AND (fork = false) - name: Publish-Release if: (tag IS present) AND (fork = false) # Default "install" and "script" steps. install: true script: - make ci jobs: include: - stage: Build-Test python: '3.9' - python: '3.10' - python: '3.11' - python: '3.12' - python: '3.13' - name: Detect-Secrets language: python python: '3.13' install: - pip install --upgrade "git+https://github.com/ibm/detect-secrets.git@master#egg=detect-secrets" script: - detect-secrets scan --update .secrets.baseline - detect-secrets -v audit --report --fail-on-unaudited --fail-on-live --fail-on-audited-real .secrets.baseline - stage: Semantic-Release language: node_js node_js: 22 install: - npm install - pip install --user bump-my-version script: - npm run semantic-release - stage: Publish-Release python: "3.9" name: Publish-To-PyPi script: - make ci - make publish-deps - make publish-release IBM-python-sdk-core-7ebd71d/Authentication.md000066400000000000000000000756261502256645000211720ustar00rootroot00000000000000# Authentication The python-sdk-core project supports the following types of authentication: - Basic Authentication - Bearer Token Authentication - Identity and Access Management (IAM) Authentication (grant type: apikey) - Identity and Access Management (IAM) Authentication (grant type: assume) - Container Authentication - VPC Instance Authentication - Cloud Pak for Data Authentication - Multi-Cloud Saas Platform (MCSP) V1 Authentication - Multi-Cloud Saas Platform (MCSP) V2 Authentication - No Authentication (for testing) The SDK user configures the appropriate type of authentication for use with service instances. The authentication types that are appropriate for a particular service may vary from service to service, so it is important for the SDK user to consult with the appropriate service documentation to understand which authenticators are supported for that service. The python-sdk-core allows an authenticator to be specified in one of two ways: 1. programmatically - the SDK user invokes the appropriate function(s) to create an instance of the desired authenticator and then passes the authenticator instance when constructing an instance of the service client. 2. configuration - the SDK user provides external configuration information (in the form of environment variables or a credentials file) to indicate the type of authenticator, along with the configuration of the necessary properties for that authenticator. The SDK user then invokes the configuration-based service client constructor method to construct an instance of the authenticator and service client that reflect the external configuration information. The sections below will provide detailed information for each authenticator which will include the following: - A description of the authenticator - The properties associated with the authenticator - An example of how to construct the authenticator programmatically - An example of how to configure the authenticator through the use of external configuration information. The configuration examples below will use environment variables, although the same properties could be specified in a credentials file instead. ## Basic Authentication The `BasicAuthenticator` is used to add Basic Authentication information to each outbound request in the `Authorization` header in the form: ``` Authorization: Basic ``` ### Properties - username: (required) the basic auth username - password: (required) the basic auth password ### Programming example ```python from ibm_cloud_sdk_core.authenticators import BasicAuthenticator from .example_service_v1 import * # Create the authenticator. authenticator = BasicAuthenticator(, ) # Construct the service instance. service = ExampleServiceV1(authenticator=authenticator) # 'service' can now be used to invoke operations. ``` ### Configuration example External configuration: ``` export EXAMPLE_SERVICE_AUTH_TYPE=basic export EXAMPLE_SERVICE_USERNAME=myuser export EXAMPLE_SERVICE_PASSWORD=mypassword ``` Application code: ```python from .example_service_v1 import * # Construct the service instance. service = ExampleServiceV1.new_instance(service_name='example_service') # 'service' can now be used to invoke operations. ``` ## Bearer Token Authentication The `BearerTokenAuthenticator` will add a user-supplied bearer token to each outbound request in the `Authorization` header in the form: ``` Authorization: Bearer ``` ### Properties - bearerToken: (required) the bearer token value ### Programming example ```python from ibm_cloud_sdk_core.authenticators import BearerTokenAuthenticator from .example_service_v1 import * # Create the authenticator. authenticator = BearerTokenAuthenticator() # Construct the service instance. service = ExampleServiceV1(authenticator=authenticator) # 'service' can now be used to invoke operations. ... # Later, if your bearer token value expires, you can set a new one like this: new_token = '' authenticator.set_bearer_token(new_token); ``` ### Configuration example External configuration: ``` export EXAMPLE_SERVICE_AUTH_TYPE=bearertoken export EXAMPLE_SERVICE_BEARER_TOKEN= ``` Application code: ```python from .example_service_v1 import * # Construct the service instance. service = ExampleServiceV1.new_instance(service_name='example_service') # 'service' can now be used to invoke operations. ``` Note that the use of external configuration is not as useful with the `BearerTokenAuthenticator` as it is for other authenticator types because bearer tokens typically need to be obtained and refreshed programmatically since they normally have a relatively short lifespan before they expire. This authenticator type is intended for situations in which the application will be managing the bearer token itself in terms of initial acquisition and refreshing as needed. ## Identity and Access Management (IAM) Authentication (grant type: apikey) The `IamAuthenticator` will accept a user-supplied apikey and will perform the necessary interactions with the IAM token service to obtain a suitable bearer token for the specified apikey. The authenticator will also obtain a new bearer token when the current token expires. The bearer token is then added to each outbound request in the `Authorization` header in the form: ``` Authorization: Bearer ``` ### Properties - apikey: (required) the IAM api key to be used to obtain an IAM access token. - url: (optional) The base endpoint URL of the IAM token service. The default value of this property is the "prod" IAM token service endpoint (`https://iam.cloud.ibm.com`). Make sure that you use an IAM token service endpoint that is appropriate for the location of the service being used by your application. For example, if you are using an instance of a service in the "production" environment (e.g. `https://resource-controller.cloud.ibm.com`), then the default "prod" IAM token service endpoint should suffice. However, if your application is using an instance of a service in the "staging" environment (e.g. `https://resource-controller.test.cloud.ibm.com`), then you would also need to configure the authenticator to use the IAM token service "staging" endpoint as well (`https://iam.test.cloud.ibm.com`). - client_id/client_secret: (optional) The `client_id` and `client_secret` fields are used to form a "basic auth" Authorization header for interactions with the IAM token server. If neither field is specified, then no Authorization header will be sent with token server requests. These fields are optional, but must be specified together. - scope: (optional) the scope to be associated with the IAM access token. If not specified, then no scope will be associated with the access token. - disable_ssl_verification: (optional) A flag that indicates whether verification of the server's SSL certificate should be disabled or not. The default value is `false`. - headers: (optional) A set of key/value pairs that will be sent as HTTP headers in requests made to the IAM token service. ### Programming example ```python from ibm_cloud_sdk_core.authenticators import IAMAuthenticator from .example_service_v1 import * # Create the authenticator. authenticator = IAMAuthenticator('myapikey') # Construct the service instance. service = ExampleServiceV1(authenticator=authenticator) # 'service' can now be used to invoke operations. ``` ### Configuration example External configuration: ``` export EXAMPLE_SERVICE_AUTH_TYPE=iam export EXAMPLE_SERVICE_APIKEY=myapikey ``` Application code: ```python from .example_service_v1 import * # Construct the service instance. service = ExampleServiceV1.new_instance(service_name='example_service') # 'service' can now be used to invoke operations. ``` ## Identity and Access Management (IAM) Authentication (grant type: assume) The `IAMAssumeAuthenticator` performs a two-step token fetch sequence to obtain a bearer token that allows the application to assume the identity of a trusted profile: 1. First, the authenticator obtains an initial bearer token using grant type `urn:ibm:params:oauth:grant-type:apikey`. This initial token will reflect the identity associated with the input apikey. 2. Second, the authenticator uses the grant type `urn:ibm:params:oauth:grant-type:assume` to obtain a bearer token that reflects the identity of the trusted profile, passing in the initial bearer token from the first step, along with the trusted profile-related inputs. The authenticator will also obtain a new bearer token when the current token expires. The bearer token is then added to each outbound request in the `Authorization` header in the form: ``` Authorization: Bearer ``` ### Properties - apikey: (required) the IAM apikey to be used to obtain the initial IAM access token. - iam_profile_crn: (optional) the Cloud Resource Name (CRN) associated with the trusted profile for which an access token should be fetched. Exactly one of iam_profile_crn, iam_profile_id or iam_profile_name must be specified. - iam_profile_id: (optional) the ID associated with the trusted profile for which an access token should be fetched. Exactly one of iam_profile_crn, iam_profile_id or iam_profile_name must be specified. - iam_profile_name: (optional) the name associated with the trusted profile for which an access token should be fetched. When specifying this property, you must also specify the iam_account_id property as well. Exactly one of iam_profile_crn, iam_profile_id or iam_profile_name must be specified. - iam_account_id: (optional) the ID associated with the IAM account that contains the trusted profile referenced by the iam_profile_name property. The iam_account_id property must be specified if and only if the iam_profile_name property is specified. - url: (optional) The base endpoint URL of the IAM token service. The default value of this property is the "prod" IAM token service endpoint (`https://iam.cloud.ibm.com`). Make sure that you use an IAM token service endpoint that is appropriate for the location of the service being used by your application. For example, if you are using an instance of a service in the "production" environment (e.g. `https://resource-controller.cloud.ibm.com`), then the default "prod" IAM token service endpoint should suffice. However, if your application is using an instance of a service in the "staging" environment (e.g. `https://resource-controller.test.cloud.ibm.com`), then you would also need to configure the authenticator to use the IAM token service "staging" endpoint as well (`https://iam.test.cloud.ibm.com`). - client_id/client_secret: (optional) The `client_id` and `client_secret` fields are used to form a "basic auth" Authorization header for interactions with the IAM token server when fetching the initial IAM access token. These fields are optional, but must be specified together. - scope: (optional) the scope to be used when obtaining the initial IAM access token. If not specified, then no scope will be associated with the access token. - disable_ssl_verification: (optional) A flag that indicates whether verification of the server's SSL certificate should be disabled or not. The default value is `false`. - headers: (optional) A set of key/value pairs that will be sent as HTTP headers in requests made to the IAM token service. ### Usage Notes - The IAMAssumeAuthenticator is used to obtain an access token (a bearer token) from the IAM token service that allows an application to "assume" the identity of a trusted profile. - The authenticator first uses the apikey, url, client_id/client_secret, scope, disable_ssl_verification, and headers properties to obtain an initial access token by invoking the IAM `get_token` (grant_type=`urn:ibm:params:oauth:grant-type:apikey`) operation. - The authenticator then uses the initial access token along with the url, iam_profile_crn, iam_profile_id, iam_profile_name, iam_account_id, disable_ssl_verification, and headers properties to obtain an access token by invoking the IAM `get_token` (grant_type=`urn:ibm:params:oauth:grant-type:assume`) operation. The access token resulting from this second step will reflect the identity of the specified trusted profile. - When providing the trusted profile information, you must specify exactly one of: iam_profile_crn, iam_profile_id or iam_profile_name. If you specify iam_profile_crn or iam_profile_id, then the trusted profile must exist in the same account that is associated with the input apikey. If you specify iam_profile_name, then you must also specify the iam_account_id property to indicate the IAM account in which the named trusted profile can be found. ### Programming example ```python from ibm_cloud_sdk_core.authenticators import IAMAssumeAuthenticator from .example_service_v1 import * # Create the authenticator. authenticator = IAMAssumeAuthenticator('myapikey', iam_profile_id='my_profile_id') # Construct the service instance. service = ExampleServiceV1(authenticator=authenticator) # 'service' can now be used to invoke operations. ``` ### Configuration example External configuration: ``` export EXAMPLE_SERVICE_AUTH_TYPE=iamAssume export EXAMPLE_SERVICE_APIKEY=myapikey export EXAMPLE_SERVICE_IAM_PROFILE_ID=myprofile-1 ``` Application code: ```python from .example_service_v1 import * # Construct the service instance. service = ExampleServiceV1.new_instance(service_name='example_service') # 'service' can now be used to invoke operations. ``` ## Container Authentication The `ContainerAuthenticator` is intended to be used by application code running inside a compute resource managed by the IBM Kubernetes Service (IKS) or IBM Cloud Code Engine in which a secure compute resource token (CR token) has been stored in a file within the compute resource's local file system. The CR token is similar to an IAM apikey except that it is managed automatically by the compute resource provider (IKS or Code Engine). This allows the application developer to: - avoid storing credentials in application code, configuration files or a password vault - avoid managing or rotating credentials The `ContainerAuthenticator` will retrieve the CR token from the compute resource in which the application is running, and will then perform the necessary interactions with the IAM token service to obtain an IAM access token using the IAM "get token" operation with grant-type `cr-token`. The authenticator will repeat these steps to obtain a new IAM access token when the current access token expires. The IAM access token is added to each outbound request in the `Authorization` header in the form: ``` Authorization: Bearer ``` ### Properties - cr_token_filename: (optional) The name of the file containing the injected CR token value. If not specified, then the authenticator will first try `/var/run/secrets/tokens/vault-token` and then `/var/run/secrets/tokens/sa-token` and finally `/var/run/secrets/codeengine.cloud.ibm.com/compute-resource-token/token` as the default value (first file found is used). The application must have `read` permissions on the file containing the CR token value. - iam_profile_name: (optional) The name of the linked trusted IAM profile to be used when obtaining the IAM access token (a CR token might map to multiple IAM profiles). One of `iam_profile_name` or `iam_profile_id` must be specified. - iam_profile_id: (optional) The ID of the linked trusted IAM profile to be used when obtaining the IAM access token (a CR token might map to multiple IAM profiles). One of `iam_profile_name` or `iam_profile_id` must be specified. - url: (optional) The base endpoint URL of the IAM token service. The default value of this property is the "prod" IAM token service endpoint (`https://iam.cloud.ibm.com`). Make sure that you use an IAM token service endpoint that is appropriate for the location of the service being used by your application. For example, if you are using an instance of a service in the "production" environment (e.g. `https://resource-controller.cloud.ibm.com`), then the default "prod" IAM token service endpoint should suffice. However, if your application is using an instance of a service in the "staging" environment (e.g. `https://resource-controller.test.cloud.ibm.com`), then you would also need to configure the authenticator to use the IAM token service "staging" endpoint as well (`https://iam.test.cloud.ibm.com`). - client_id/client_secret: (optional) The `client_id` and `client_secret` fields are used to form a "basic auth" Authorization header for interactions with the IAM token server. If neither field is specified, then no Authorization header will be sent with token server requests. These fields are optional, but must be specified together. - scope (optional): the scope to be associated with the IAM access token. If not specified, then no scope will be associated with the access token. - disable_ssl_verification: (optional) A flag that indicates whether verification of the server's SSL certificate should be disabled or not. The default value is `False`. - proxies (optional): The proxy endpoint to use for HTTP(S) requests. - headers: (optional) A set of key/value pairs that will be sent as HTTP headers in requests made to the IAM token service. ### Programming example ```python from ibm_cloud_sdk_core.authenticators import ContainerAuthenticator from .example_service_v1 import * # Create the authenticator. authenticator = ContainerAuthenticator(iam_profile_name='iam-user-123') # Construct the service instance. service = ExampleServiceV1(authenticator=authenticator) # 'service' can now be used to invoke operations. ``` ### Configuration example External configuration: ``` export EXAMPLE_SERVICE_AUTH_TYPE=container export EXAMPLE_SERVICE_IAM_PROFILE_NAME=iam-user-123 ``` Application code: ```python from .example_service_v1 import * # Construct the service instance. service = ExampleServiceV1.new_instance(service_name='example_service') # 'service' can now be used to invoke operations. ``` ## VPC Instance Authentication The `VPCInstanceAuthenticator` is intended to be used by application code running inside a VPC-managed compute resource (virtual server instance) that has been configured to use the "compute resource identity" feature. The compute resource identity feature allows you to assign a trusted IAM profile to the compute resource as its "identity". This, in turn, allows applications running within the compute resource to take on this identity when interacting with IAM-secured IBM Cloud services. This results in a simplified security model that allows the application developer to: - avoid storing credentials in application code, configuration files or a password vault - avoid managing or rotating credentials The `VPCInstanceAuthenticator` will invoke the appropriate operations on the compute resource's locally-available VPC Instance Metadata Service to (1) retrieve an instance identity token and then (2) exchange that instance identity token for an IAM access token. The authenticator will repeat these steps to obtain a new IAM access token whenever the current access token expires. The IAM access token is added to each outbound request in the `Authorization` header in the form: ``` Authorization: Bearer ``` ### Properties - iam_profile_crn: (optional) the crn of the linked trusted IAM profile to be used when obtaining the IAM access token. - iam_profile_id: (optional) the id of the linked trusted IAM profile to be used when obtaining the IAM access token. - url: (optional) The VPC Instance Metadata Service's base URL. The default value of this property is `http://169.254.169.254`. However, if the VPC Instance Metadata Service is configured with the HTTP Secure Protocol setting (`https`), then you should configure this property to be `https://api.metadata.cloud.ibm.com`. Usage Notes: 1. At most one of `iam_profile_crn` or `iam_profile_id` may be specified. The specified value must map to a trusted IAM profile that has been linked to the compute resource (virtual server instance). 2. If both `iam_profile_crn` and `iam_profile_id` are specified, then an error occurs. 3. If neither `iam_profile_crn` nor `iam_profile_id` are specified, then the default trusted profile linked to the compute resource will be used to perform the IAM token exchange. If no default trusted profile is defined for the compute resource, then an error occurs. ### Programming example ```python from ibm_cloud_sdk_core.authenticators import VPCInstanceAuthenticator from .example_service_v1 import * # Create the authenticator. authenticator = VPCInstanceAuthenticator(iam_profile_crn='crn:iam-profile-123') # Construct the service instance. service = ExampleServiceV1(authenticator=authenticator) # 'service' can now be used to invoke operations. ``` ### Configuration example External configuration: ``` export EXAMPLE_SERVICE_AUTH_TYPE=vpc export EXAMPLE_SERVICE_IAM_PROFILE_CRN=crn:iam-profile-123 ``` Application code: ```python from .example_service_v1 import * # Construct the service instance. service = ExampleServiceV1.new_instance(service_name='example_service') # 'service' can now be used to invoke operations. ``` ## Cloud Pak for Data The `CloudPakForDataAuthenticator` will accept a user-supplied username value, along with either a password or apikey, and will perform the necessary interactions with the Cloud Pak for Data token service to obtain a suitable bearer token. The authenticator will also obtain a new bearer token when the current token expires. The bearer token is then added to each outbound request in the `Authorization` header in the form: ``` Authorization: Bearer ``` ### Properties - username: (required) the username used to obtain a bearer token. - password: (required if apikey is not specified) the password used to obtain a bearer token. - apikey: (required if password is not specified) the apikey used to obtain a bearer token. - url: (required) The URL representing the Cloud Pak for Data token service endpoint's base URL string. This value should not include the `/v1/authorize` path portion. - disable_ssl_verification: (optional) A flag that indicates whether verification of the server's SSL certificate should be disabled or not. The default value is `false`. - headers: (optional) A set of key/value pairs that will be sent as HTTP headers in requests made to the Cloud Pak for Data token service. ### Programming example ```python from ibm_cloud_sdk_core.authenticators import CloudPakForDataAuthenticator from .example_service_v1 import * # Create the authenticator using username/apikey. authenticator = CloudPakForDataAuthenticator(username='myuser', apikey='myapikey', url='https://mycp4dhost.com') # Construct the service instance. service = ExampleServiceV1(authenticator=authenticator) # 'service' can now be used to invoke operations. ``` ### Configuration example External configuration: ``` # Configure "example_service" with username/apikey. export EXAMPLE_SERVICE_AUTH_TYPE=cp4d export EXAMPLE_SERVICE_USERNAME=myuser export EXAMPLE_SERVICE_PASSWORD=myapikey export EXAMPLE_SERVICE_URL=https://mycp4dhost.com ``` Application code: ```python from .example_service_v1 import * # Construct the service instance. service = ExampleServiceV1.new_instance(service_name='example_service') # 'service' can now be used to invoke operations. ``` ## Multi-Cloud Saas Platform (MCSP) V1 Authentication The `MCSPAuthenticator` can be used in scenarios where an application needs to interact with an IBM Cloud service that has been deployed to a non-IBM Cloud environment (e.g. AWS). It accepts a user-supplied apikey and invokes the Multi-Cloud Saas Platform token service's `POST /siusermgr/api/1.0/apikeys/token` operation to obtain a suitable MCSP access token (a bearer token) for the specified apikey. The authenticator will also obtain a new bearer token when the current token expires. The bearer token is then added to each outbound request in the `Authorization` header in the form: ``` Authorization: Bearer ``` ### Properties - apikey: (required) the apikey to be used to obtain an MCSP access token. - url: (required) The URL representing the MCSP token service endpoint's base URL string. Do not include the operation path (e.g. `/siusermgr/api/1.0/apikeys/token`) as part of this property's value. - disable_ssl_verification: (optional) A flag that indicates whether verification of the server's SSL certificate should be disabled or not. The default value is `false`. - headers: (optional) A set of key/value pairs that will be sent as HTTP headers in requests made to the MCSP token service. ### Usage Notes - When constructing an MCSPAuthenticator instance, you must specify the apikey and url properties. - The authenticator will use the token server's `POST /siusermgr/api/1.0/apikeys/token` operation to exchange the user-supplied apikey for an MCSP access token (the bearer token). ### Programming example ```python from ibm_cloud_sdk_core.authenticators import MCSPAuthenticator from .example_service_v1 import * # Create the authenticator. authenticator = MCSPAuthenticator(apikey='myapikey', url='https://example.mcsp.token-exchange.com') # Construct the service instance. service = ExampleServiceV1(authenticator=authenticator) # 'service' can now be used to invoke operations. ``` ### Configuration example External configuration: ``` export EXAMPLE_SERVICE_AUTH_TYPE=mcsp export EXAMPLE_SERVICE_APIKEY=myapikey export EXAMPLE_SERVICE_AUTH_URL=https://example.mcsp.token-exchange.com ``` Application code: ```python from .example_service_v1 import * # Construct the service instance. service = ExampleServiceV1.new_instance(service_name='example_service') # 'service' can now be used to invoke operations. ``` ## Multi-Cloud Saas Platform (MCSP) V2 Authentication The `MCSPV2Authenticator` can be used in scenarios where an application needs to interact with an IBM Cloud service that has been deployed to a non-IBM Cloud environment (e.g. AWS). It accepts a user-supplied apikey and invokes the Multi-Cloud Saas Platform token service's `POST /api/2.0/{scopeCollectionType}/{scopeId}/apikeys/token` operation to obtain a suitable MCSP access token (a bearer token) for the specified apikey. The authenticator will also obtain a new bearer token when the current token expires. The bearer token is then added to each outbound request in the `Authorization` header in the form: ``` Authorization: Bearer ``` ### Properties - apikey: (required) The apikey to be used to obtain an MCSP access token. - url: (required) The URL representing the MCSP token service endpoint's base URL string. Do not include the operation path (e.g. `/api/2.0/{scopeCollectionType}/{scopeId}/apikeys/token`) as part of this property's value. - scope_collection_type: (required) The scope collection type of item(s). The valid values are: `accounts`, `subscriptions`, `services`. - scope_id: (required) The scope identifier of item(s). - include_builtin_actions: (optional) A flag to include builtin actions in the `actions` claim in the MCSP token (default: false). - include_custom_actions: (optional) A flag to include custom actions in the `actions` claim in the MCSP token (default: false). - include_roles: (optional) A flag to include the `roles` claim in the MCSP token (default: true). - prefix_roles: (optional) A flag to add a prefix with the scope level where the role is defined in the `roles` claim (default: false). - caller_ext_claim: (optional) A map containing keys and values to be injected into the returned access token as the `callerExt` claim. The keys used in this map must be enabled in the apikey by setting the `callerExtClaimNames` property when the apikey is created. This property is typically only used in scenarios involving an apikey with identityType `SERVICEID`. - disable_ssl_verification: (optional) A flag that indicates whether verification of the server's SSL certificate should be disabled or not. The default value is `false`. - headers: (optional) A set of key/value pairs that will be sent as HTTP headers in requests made to the MCSP token service. ### Usage Notes - When constructing an MCSPV2Authenticator instance, the apikey, url, scope_collection_type, and scope_id properties are required. - If you specify the caller_ext_claim map, the keys used in the map must have been previously enabled in the apikey by setting the `callerExtClaimNames` property when you created the apikey. The entries contained in this map will appear in the `callerExt` field (claim) of the returned access token. - The authenticator will invoke the token server's `POST /api/2.0/{scopeCollectionType}/{scopeId}/apikeys/token` operation to exchange the apikey for an MCSP access token (the bearer token). ### Programming example ```python from ibm_cloud_sdk_core.authenticators import MCSPV2Authenticator from .example_service_v1 import * # Create the authenticator. authenticator = MCSPV2Authenticator( apikey='myapikey', url='https://example.mcspv2.token-exchange.com', scope_collection_type='accounts', scope_id='20250519-2128-3755-60b3-103e01c509e8', include_builtin_actions=True, caller_ext_claim={'productID': 'prod-123'}, ) # Construct the service instance. service = ExampleServiceV1(authenticator=authenticator) # 'service' can now be used to invoke operations. ``` ### Configuration example External configuration: ``` export EXAMPLE_SERVICE_AUTH_TYPE=mcspv2 export EXAMPLE_SERVICE_APIKEY=myapikey export EXAMPLE_SERVICE_AUTH_URL=https://example.mcspv2.token-exchange.com export EXAMPLE_SERVICE_SCOPE_COLLECTION_TYPE=accounts export EXAMPLE_SERVICE_SCOPE_ID=20250519-2128-3755-60b3-103e01c509e8 export EXAMPLE_SERVICE_INCLUDE_BUILTIN_ACTIONS=true export EXAMPLE_SERVICE_CALLER_EXT_CLAIM={"productID":"prod-123"} ``` Application code: ```python from .example_service_v1 import * # Construct the service instance. service = ExampleServiceV1.new_instance(service_name='example_service') # 'service' can now be used to invoke operations. ``` ## No Auth Authentication The `NoAuthAuthenticator` is a placeholder authenticator which performs no actual authentication function. It can be used in situations where authentication needs to be bypassed, perhaps while developing or debugging an application or service. ### Properties None ### Programming example ```python from ibm_cloud_sdk_core.authenticators import NoAuthAuthenticator from .example_service_v1 import * # Create the authenticator using username/apikey. authenticator = NoAuthAuthenticator() # Construct the service instance. service = ExampleServiceV1(authenticator=authenticator) # 'service' can now be used to invoke operations. ``` ### Configuration example External configuration: ``` export EXAMPLE_SERVICE_AUTH_TYPE=noauth ``` Application code: ```python from .example_service_v1 import * # Construct the service instance. service = ExampleServiceV1.new_instance(service_name='example_service') # 'service' can now be used to invoke operations. IBM-python-sdk-core-7ebd71d/CHANGELOG.md000066400000000000000000000637071502256645000174770ustar00rootroot00000000000000## [3.24.2](https://github.com/IBM/python-sdk-core/compare/v3.24.1...v3.24.2) (2025-06-12) ### Bug Fixes * **build:** bump requests to avoid CVE-2024-47081 ([7b5f21b](https://github.com/IBM/python-sdk-core/commit/7b5f21b3d9e3a82e5d1292f290a7d4fee00b27a7)) ## [3.24.1](https://github.com/IBM/python-sdk-core/compare/v3.24.0...v3.24.1) (2025-05-30) ### Bug Fixes * **build:** suppress detect-secrets false positive ([4e9bc4c](https://github.com/IBM/python-sdk-core/commit/4e9bc4c6dbb651a8d25ad28b6233d28569275819)) # [3.24.0](https://github.com/IBM/python-sdk-core/compare/v3.23.0...v3.24.0) (2025-05-30) ### Features * **auth:** add support for MCSP V2 authentication ([d467d02](https://github.com/IBM/python-sdk-core/commit/d467d02d5a8d49b7dd212319003e727d55494b63)) # [3.23.0](https://github.com/IBM/python-sdk-core/compare/v3.22.1...v3.23.0) (2025-03-07) ### Features * **ContainerAuthenticator:** add support for code engine workload ([#218](https://github.com/IBM/python-sdk-core/issues/218)) ([765d33f](https://github.com/IBM/python-sdk-core/commit/765d33f794265784f216cdccb156d2f8476a66d1)) ## [3.22.1](https://github.com/IBM/python-sdk-core/compare/v3.22.0...v3.22.1) (2025-01-09) ### Bug Fixes * enable github workflows ([436ba89](https://github.com/IBM/python-sdk-core/commit/436ba891fd0d1f960b3427de3d6b40b1950d1a53)) # [3.22.0](https://github.com/IBM/python-sdk-core/compare/v3.21.0...v3.22.0) (2024-10-15) ### Features * **IAMAssumeAuthenticator:** introduce a new authenticator ([#211](https://github.com/IBM/python-sdk-core/issues/211)) ([29a8eb7](https://github.com/IBM/python-sdk-core/commit/29a8eb7f5d9d72b7e48d4bf7f3867e5c93e747fb)) # [3.21.0](https://github.com/IBM/python-sdk-core/compare/v3.20.6...v3.21.0) (2024-09-18) ### Bug Fixes * **logging:** improve python core's debug logging ([67b126c](https://github.com/IBM/python-sdk-core/commit/67b126c2fd87d14057b185a8e6d13644a955f332)) ### Features * redact secrets in debug logging ([#209](https://github.com/IBM/python-sdk-core/issues/209)) ([ed043dc](https://github.com/IBM/python-sdk-core/commit/ed043dc2814a2959da9b92c4286ca8b23b5fa59d)) ## [3.20.6](https://github.com/IBM/python-sdk-core/compare/v3.20.5...v3.20.6) (2024-08-08) ### Bug Fixes * **build:** use correct package list with setuptools ([#207](https://github.com/IBM/python-sdk-core/issues/207)) ([27fa51d](https://github.com/IBM/python-sdk-core/commit/27fa51d85c8df0bc5e52bdc084155bd284a77ec0)) ## [3.20.5](https://github.com/IBM/python-sdk-core/compare/v3.20.4...v3.20.5) (2024-08-07) ### Bug Fixes * **build:** migrate setup.py to pyproject.toml ([#205](https://github.com/IBM/python-sdk-core/issues/205)) ([#206](https://github.com/IBM/python-sdk-core/issues/206)) ([03c8a6d](https://github.com/IBM/python-sdk-core/commit/03c8a6d81dd8227e6510a26fef75e502b82db304)) ## [3.20.4](https://github.com/IBM/python-sdk-core/compare/v3.20.3...v3.20.4) (2024-07-29) ### Bug Fixes * remove test command and related code from `setup.py` ([#203](https://github.com/IBM/python-sdk-core/issues/203)) ([bd44dd1](https://github.com/IBM/python-sdk-core/commit/bd44dd1152e01bb381f95e6295103991c0dd9ac4)) ## [3.20.3](https://github.com/IBM/python-sdk-core/compare/v3.20.2...v3.20.3) (2024-07-11) ### Bug Fixes * improve the detection and loading of default certificates ([#197](https://github.com/IBM/python-sdk-core/issues/197)) ([3dc4cc4](https://github.com/IBM/python-sdk-core/commit/3dc4cc47a8fa5363b1b08c9fbc1412885748af3e)) ## [3.20.2](https://github.com/IBM/python-sdk-core/compare/v3.20.1...v3.20.2) (2024-07-09) ### Bug Fixes * always load the default certs on our custom SSL context ([#196](https://github.com/IBM/python-sdk-core/issues/196)) ([ff14a4b](https://github.com/IBM/python-sdk-core/commit/ff14a4b1177b31ab2afd8aed7c6e3ffecdabfb4c)) ## [3.20.1](https://github.com/IBM/python-sdk-core/compare/v3.20.0...v3.20.1) (2024-06-03) ### Bug Fixes * specify the maximum version of `requests` to avoid regression ([#194](https://github.com/IBM/python-sdk-core/issues/194)) ([f4ac0d1](https://github.com/IBM/python-sdk-core/commit/f4ac0d143d5d0079a4f2e99c1ebf1328b91a432f)) # [3.20.0](https://github.com/IBM/python-sdk-core/compare/v3.19.2...v3.20.0) (2024-04-17) ### Features * send user-agent header with auth token requests ([#191](https://github.com/IBM/python-sdk-core/issues/191)) ([37014b5](https://github.com/IBM/python-sdk-core/commit/37014b57a27090deb6c51e5e6019dd7bee90a6fd)) ## [3.19.2](https://github.com/IBM/python-sdk-core/compare/v3.19.1...v3.19.2) (2024-02-28) ### Bug Fixes * adjust IAM token expiration time ([#189](https://github.com/IBM/python-sdk-core/issues/189)) ([f4f0b5a](https://github.com/IBM/python-sdk-core/commit/f4f0b5afe4746c8dfc9687ebeb9db20bf1df2414)) ## [3.19.1](https://github.com/IBM/python-sdk-core/compare/v3.19.0...v3.19.1) (2024-01-24) ### Bug Fixes * use the correct SSL config if cert verification is disabled ([#187](https://github.com/IBM/python-sdk-core/issues/187)) ([7fc172a](https://github.com/IBM/python-sdk-core/commit/7fc172ad325545aa07bd5cbab1cb65cb0c078b9b)) # [3.19.0](https://github.com/IBM/python-sdk-core/compare/v3.18.2...v3.19.0) (2024-01-22) ### Features * rename `ApiException.code` to `ApiException.status_code` ([#185](https://github.com/IBM/python-sdk-core/issues/185)) ([cf74671](https://github.com/IBM/python-sdk-core/commit/cf74671a9a5d538fbf2b28a4e9c5046644e696fb)) ## [3.18.2](https://github.com/IBM/python-sdk-core/compare/v3.18.1...v3.18.2) (2023-12-11) ### Bug Fixes * use retry_interval as retry backoff_max ([#184](https://github.com/IBM/python-sdk-core/issues/184)) ([7e84825](https://github.com/IBM/python-sdk-core/commit/7e848259ca88fa1b4831786eaaae2a02f8afbcd2)) ## [3.18.1](https://github.com/IBM/python-sdk-core/compare/v3.18.0...v3.18.1) (2023-12-08) ### Bug Fixes * bump urllib3 to v2.1.0 ([#183](https://github.com/IBM/python-sdk-core/issues/183)) ([d1ab40c](https://github.com/IBM/python-sdk-core/commit/d1ab40c941fb21be798461169a1937dbaf4de1de)) # [3.18.0](https://github.com/IBM/python-sdk-core/compare/v3.17.3...v3.18.0) (2023-11-15) ### Features * **MCSPAuthenticator:** add new authenticator for Multi-Cloud Saas Platform ([#181](https://github.com/IBM/python-sdk-core/issues/181)) ([1be97e5](https://github.com/IBM/python-sdk-core/commit/1be97e5712c7b62847d8412cfa75096c1bf51b9d)) ## [3.17.3](https://github.com/IBM/python-sdk-core/compare/v3.17.2...v3.17.3) (2023-11-06) ### Bug Fixes * **build:** bump urllib version to avoid vulnerability ([#180](https://github.com/IBM/python-sdk-core/issues/180)) ([5363104](https://github.com/IBM/python-sdk-core/commit/5363104a765d3b1928ec76e6fc75b314398029c5)) ## [3.17.2](https://github.com/IBM/python-sdk-core/compare/v3.17.1...v3.17.2) (2023-10-10) ### Bug Fixes * use on-the-fly compression only for file objects ([#177](https://github.com/IBM/python-sdk-core/issues/177)) ([dd1cd05](https://github.com/IBM/python-sdk-core/commit/dd1cd056ec030344e2b8fec5093b2acae53ae941)) ## [3.17.1](https://github.com/IBM/python-sdk-core/compare/v3.17.0...v3.17.1) (2023-10-04) ### Bug Fixes * add correct support for compressing file-like objects ([#174](https://github.com/IBM/python-sdk-core/issues/174)) ([2f91105](https://github.com/IBM/python-sdk-core/commit/2f911055aac6ffe8e0bab056b861ce41a29ad58b)) # [3.17.0](https://github.com/IBM/python-sdk-core/compare/v3.16.7...v3.17.0) (2023-10-03) ### Features * bump min supported version of python to 3.8 ([#175](https://github.com/IBM/python-sdk-core/issues/175)) ([8933684](https://github.com/IBM/python-sdk-core/commit/8933684d8a4a5a4450239811d1169529982c3029)) ## [3.16.7](https://github.com/IBM/python-sdk-core/compare/v3.16.6...v3.16.7) (2023-05-31) ### Bug Fixes * **build:** bump requests to latest version (2.31.0) ([#171](https://github.com/IBM/python-sdk-core/issues/171)) ([a293e76](https://github.com/IBM/python-sdk-core/commit/a293e760228492cb108ab41f5093355bae018cc2)) ## [3.16.6](https://github.com/IBM/python-sdk-core/compare/v3.16.5...v3.16.6) (2023-05-22) ### Bug Fixes * **ContainerAuthenticator:** add sa-token as default CR token filename ([#165](https://github.com/IBM/python-sdk-core/issues/165)) ([7c6bd0f](https://github.com/IBM/python-sdk-core/commit/7c6bd0f1d3521628ef0fd593f7110d9174c5b771)) ## [3.16.5](https://github.com/IBM/python-sdk-core/compare/v3.16.4...v3.16.5) (2023-03-23) ### Bug Fixes * allow control characters in JSON responses ([#160](https://github.com/IBM/python-sdk-core/issues/160)) ([2f09503](https://github.com/IBM/python-sdk-core/commit/2f09503ac4ebfb6f603f142f461122c402aa03d5)) ## [3.16.4](https://github.com/IBM/python-sdk-core/compare/v3.16.3...v3.16.4) (2023-03-23) ### Bug Fixes * bump requests version to avoid import error ([#159](https://github.com/IBM/python-sdk-core/issues/159)) ([e9ca94c](https://github.com/IBM/python-sdk-core/commit/e9ca94c4ddd707b84bfa72b6a04ee3c56d21c08a)) ## [3.16.3](https://github.com/IBM/python-sdk-core/compare/v3.16.2...v3.16.3) (2023-03-22) ### Bug Fixes * detect JSON unmarshal errors in responses ([#157](https://github.com/IBM/python-sdk-core/issues/157)) ([30d7c52](https://github.com/IBM/python-sdk-core/commit/30d7c52659f92f8d9218b66372ef0d707a297232)) ## [3.16.2](https://github.com/IBM/python-sdk-core/compare/v3.16.1...v3.16.2) (2023-02-08) ### Bug Fixes * avoid pylint errors ([00ee90b](https://github.com/IBM/python-sdk-core/commit/00ee90bf223a1f620e211fe9de2ed95ccfff8233)) ## [3.16.1](https://github.com/IBM/python-sdk-core/compare/v3.16.0...v3.16.1) (2023-01-09) ### Bug Fixes * pin build to semantic-release v19 ([03c5474](https://github.com/IBM/python-sdk-core/commit/03c547442c91e0fc497a2a9ba202b8231674562d)) * **VPCInstanceAuthenticator:** use correct version string ([93729a3](https://github.com/IBM/python-sdk-core/commit/93729a3ee09b8011989b7679b4820d042f79964b)) # [3.16.0](https://github.com/IBM/python-sdk-core/compare/v3.15.3...v3.16.0) (2022-08-01) ### Features * **CP4D Authentication:** add ssl verification for self-signed certificates ([#147](https://github.com/IBM/python-sdk-core/issues/147)) ([a16685d](https://github.com/IBM/python-sdk-core/commit/a16685d83641e467f2dcaf610194fd3f84f13a84)) ## [3.15.3](https://github.com/IBM/python-sdk-core/compare/v3.15.2...v3.15.3) (2022-06-07) ### Bug Fixes * avoid warning when custom headers are used ([#142](https://github.com/IBM/python-sdk-core/issues/142)) ([a87d66d](https://github.com/IBM/python-sdk-core/commit/a87d66da4b9ed98b9ac910ca67c86b674aab7ef6)) ## [3.15.2](https://github.com/IBM/python-sdk-core/compare/v3.15.1...v3.15.2) (2022-05-31) ### Bug Fixes * bump PyJWT version to avoid CVE ([#141](https://github.com/IBM/python-sdk-core/issues/141)) ([bea54f0](https://github.com/IBM/python-sdk-core/commit/bea54f03fab6d9ebef1a26f170280b6e8698c04e)) ## [3.15.1](https://github.com/IBM/python-sdk-core/compare/v3.15.0...v3.15.1) (2022-03-21) ### Bug Fixes * set minimum TLS version to v1.2 ([#139](https://github.com/IBM/python-sdk-core/issues/139)) ([8d6ec4b](https://github.com/IBM/python-sdk-core/commit/8d6ec4b675642664258650e7d7c12c8fc333a410)) # [3.15.0](https://github.com/IBM/python-sdk-core/compare/v3.14.0...v3.15.0) (2022-02-25) ### Features * update Python versions ([#138](https://github.com/IBM/python-sdk-core/issues/138)) ([a8c201e](https://github.com/IBM/python-sdk-core/commit/a8c201ed7eb88bf3509da0db6da84619190c7bfb)) # [3.14.0](https://github.com/IBM/python-sdk-core/compare/v3.13.2...v3.14.0) (2022-01-14) ### Features * use module names for loggers ([#136](https://github.com/IBM/python-sdk-core/issues/136)) ([36523c8](https://github.com/IBM/python-sdk-core/commit/36523c869627323374afc7b246959969a8a1c9a3)) ## [3.13.2](https://github.com/IBM/python-sdk-core/compare/v3.13.1...v3.13.2) (2021-11-15) ### Bug Fixes * bump requests and urllib3 deps ([#132](https://github.com/IBM/python-sdk-core/issues/132)) ([ced5b7e](https://github.com/IBM/python-sdk-core/commit/ced5b7ea2398570e187a109bc51dba2b30ab7243)) ## [3.13.1](https://github.com/IBM/python-sdk-core/compare/v3.13.0...v3.13.1) (2021-11-15) ### Bug Fixes * strip trailing slashes in BaseService.set_service_url ([#130](https://github.com/IBM/python-sdk-core/issues/130)) ([37d0099](https://github.com/IBM/python-sdk-core/commit/37d0099cfd7bfe4bdb9f1cddc6bb2b62f4609f60)) # [3.13.0](https://github.com/IBM/python-sdk-core/compare/v3.12.0...v3.13.0) (2021-11-08) ### Features * **VPCInstanceAuthenticator:** add support for new VPC authentication flow ([#129](https://github.com/IBM/python-sdk-core/issues/129)) ([5cb1c21](https://github.com/IBM/python-sdk-core/commit/5cb1c212aaef5df62df00064a12d6581e960a95b)) # [3.12.0](https://github.com/IBM/python-sdk-core/compare/v3.11.3...v3.12.0) (2021-10-15) ### Features * add authentication_type method to authenticators ([#127](https://github.com/IBM/python-sdk-core/issues/127)) ([c56ce73](https://github.com/IBM/python-sdk-core/commit/c56ce73454a2d049ed787649be0d3d464aae4c24)) ## [3.11.3](https://github.com/IBM/python-sdk-core/compare/v3.11.2...v3.11.3) (2021-08-24) ### Bug Fixes * multiple IAM based authenticator overrides ([#124](https://github.com/IBM/python-sdk-core/issues/124)) ([b142e9e](https://github.com/IBM/python-sdk-core/commit/b142e9ecf337f73dcfd1e577afb330e993a3371b)) ## [3.11.2](https://github.com/IBM/python-sdk-core/compare/v3.11.1...v3.11.2) (2021-08-20) ### Bug Fixes * **retries:** change default retry_interval to 1 second (was 0.1) ([#122](https://github.com/IBM/python-sdk-core/issues/122)) ([3daef00](https://github.com/IBM/python-sdk-core/commit/3daef00a61c461512da0a3b37952ed275180569d)) ## [3.11.1](https://github.com/IBM/python-sdk-core/compare/v3.11.0...v3.11.1) (2021-08-18) ### Bug Fixes * encode serialized JSON string as UTF-8 ([#121](https://github.com/IBM/python-sdk-core/issues/121)) ([6c1ddac](https://github.com/IBM/python-sdk-core/commit/6c1ddacff512478ec9aaa6f5c6ff454eae780c74)) # [3.11.0](https://github.com/IBM/python-sdk-core/compare/v3.10.1...v3.11.0) (2021-08-12) ### Features * implement container authentication ([#119](https://github.com/IBM/python-sdk-core/issues/119)) ([5237277](https://github.com/IBM/python-sdk-core/commit/5237277b4e7e9daf54bb70d2ec01882cfa3167c2)) ## [3.10.1](https://github.com/IBM/python-sdk-core/compare/v3.10.0...v3.10.1) (2021-07-08) ### Bug Fixes * remove reserved keywords from kwargs before passing it to requests ([#117](https://github.com/IBM/python-sdk-core/issues/117)) ([6191978](https://github.com/IBM/python-sdk-core/commit/619197844e553fd4fd3c7e9ece2abae752d5dc3f)) # [3.10.0](https://github.com/IBM/python-sdk-core/compare/v3.9.0...v3.10.0) (2021-05-12) ### Features * support api key use case for CP4D authenticator ([830c28f](https://github.com/IBM/python-sdk-core/commit/830c28f68682a885d5cd4f668964def2864930d0)) # [3.9.0](https://github.com/IBM/python-sdk-core/compare/v3.8.0...v3.9.0) (2021-03-25) ### Features * **python:** add max retry configuration for python requests session ([481192c](https://github.com/IBM/python-sdk-core/commit/481192c5468c908f28f77ce697cae13350409397)) # [3.8.0](https://github.com/IBM/python-sdk-core/compare/v3.7.0...v3.8.0) (2021-03-17) ### Features * add datetime to string utils for lists ([2a4260c](https://github.com/IBM/python-sdk-core/commit/2a4260c7aca9b99020e2b06ccffc976ece8bd6ac)) # [3.7.0](https://github.com/IBM/python-sdk-core/compare/v3.6.0...v3.7.0) (2021-03-12) ### Features * add get_query_param utility method to support pagination ([b40edde](https://github.com/IBM/python-sdk-core/commit/b40edde45dcba59ecfb626ac8bf8a98cbb11b6de)) # [3.6.0](https://github.com/IBM/python-sdk-core/compare/v3.5.2...v3.6.0) (2021-03-05) ### Bug Fixes * split token manager url path ([18d64b5](https://github.com/IBM/python-sdk-core/commit/18d64b51f0e637a01f72488635e81fc5a7bd6918)) * update default iam url to omit path ([1fbdd0c](https://github.com/IBM/python-sdk-core/commit/1fbdd0c0c61add47182c9e389f5933789276a7d0)) ### Features * expose refresh token in iam authenticator ([31e988d](https://github.com/IBM/python-sdk-core/commit/31e988d4348f313074d5c434ae2b86264e3cbbc1)) ## [3.5.2](https://github.com/IBM/python-sdk-core/compare/v3.5.1...v3.5.2) (2021-02-10) ### Bug Fixes * **build:** main migration ([3664e2e](https://github.com/IBM/python-sdk-core/commit/3664e2e7e564d227b1e9be67831b4c6d4cea8b18)) * **build:** main migration release ([8b3debc](https://github.com/IBM/python-sdk-core/commit/8b3debcf088c2bc71957645ae0925c4fa6dac56a)) ## [3.5.1](https://github.com/IBM/python-sdk-core/compare/v3.5.0...v3.5.1) (2021-02-01) ### Bug Fixes * remove unnecessary logging of exceptions ([e94d5ae](https://github.com/IBM/python-sdk-core/commit/e94d5ae81930a5e65e117f15cb20ecce4134c5ab)) * silently ignore missing configuration file ([d438ade](https://github.com/IBM/python-sdk-core/commit/d438ade26d0ddda65074d6c31f735cc1f53850cf)) # [3.5.0](https://github.com/IBM/python-sdk-core/compare/v3.4.0...v3.5.0) (2021-01-27) ### Features * **BaseService:** use a requests.Session for retry and other configuration ([14bcf41](https://github.com/IBM/python-sdk-core/commit/14bcf413c74173903621ece3ad55ca8bb0bff81b)) # [3.4.0](https://github.com/IBM/python-sdk-core/compare/v3.3.6...v3.4.0) (2021-01-26) ### Features * update minimum supported python version to 3.6 ([7f9b968](https://github.com/IBM/python-sdk-core/commit/7f9b968670a637a5619c4dda2405e11c9c3f5328)) ## [3.3.6](https://github.com/IBM/python-sdk-core/compare/v3.3.5...v3.3.6) (2021-01-08) ### Bug Fixes * include requirements.txt in MANIFEST.in for setup.py reference ([cc8935a](https://github.com/IBM/python-sdk-core/commit/cc8935a8082f852a528631b606940336253580f6)) ## [3.3.5](https://github.com/IBM/python-sdk-core/compare/v3.3.4...v3.3.5) (2021-01-08) ### Bug Fixes * update setup.py requirements to match requirements.txt ([c618928](https://github.com/IBM/python-sdk-core/commit/c618928da730eb0e94f12d7efa0ee9ba9370842f)) ## [3.3.4](https://github.com/IBM/python-sdk-core/compare/v3.3.3...v3.3.4) (2021-01-07) ### Bug Fixes * update pyjwt parameters to satisfy 2.x changes ([f2d7225](https://github.com/IBM/python-sdk-core/commit/f2d7225a6f8e8f4b19ed6d1858c93993d7d62cf5)) ## [3.3.3](https://github.com/IBM/python-sdk-core/compare/v3.3.2...v3.3.3) (2021-01-06) ### Bug Fixes * bump minimum requirement versions ([62edeb6](https://github.com/IBM/python-sdk-core/commit/62edeb6f3c89dcb49cddb4cbf315ba664ea7bd73)) ## [3.3.2](https://github.com/IBM/python-sdk-core/compare/v3.3.1...v3.3.2) (2020-11-20) ### Bug Fixes * update requests version to not include vulnerabilities ([16b2827](https://github.com/IBM/python-sdk-core/commit/16b28271f6c025f8207f2819345aecd487804534)) ## [3.3.1](https://github.com/IBM/python-sdk-core/compare/v3.3.0...v3.3.1) (2020-11-11) ### Bug Fixes * replace zlib with gzip for gzip compression ([b6a6da3](https://github.com/IBM/python-sdk-core/commit/b6a6da3445e70374b654221340aaebfc7f9d0ecc)) # [3.3.0](https://github.com/IBM/python-sdk-core/compare/v3.2.0...v3.3.0) (2020-10-07) ### Features * allow gzip compression on request bodies ([196a407](https://github.com/IBM/python-sdk-core/commit/196a407c40c0c99b2465d19ec8cca1f85b51ee86)) # [3.2.0](https://github.com/IBM/python-sdk-core/compare/v3.1.0...v3.2.0) (2020-09-18) ### Features * **IAM Authenticator:** add support for optional 'scope' property ([2e776c2](https://github.com/IBM/python-sdk-core/commit/2e776c2ba402c3b2846f5758f64d2776492ae764)) # [3.1.0](https://github.com/IBM/python-sdk-core/compare/v3.0.0...v3.1.0) (2020-06-15) ### Features * **BaseService:** support stream=True in BaseService.send() ([bf4179b](https://github.com/IBM/python-sdk-core/commit/bf4179b54407c94707b24caeab6c4aeeb67ee3e7)) # [3.0.0](https://github.com/IBM/python-sdk-core/compare/v2.0.5...v3.0.0) (2020-06-01) ### Bug Fixes * Combine multiple ending slashes to one ([5496948](https://github.com/IBM/python-sdk-core/commit/549694867285c2ba1d77187bfba108543a225e33)) ### BREAKING CHANGES * Fixing the request URL like this will break compatibility with previous generator versions ## [2.0.5](https://github.com/IBM/python-sdk-core/compare/v2.0.4...v2.0.5) (2020-05-29) ### Bug Fixes * Revert stripping request URL trailing slashes ([09a193c](https://github.com/IBM/python-sdk-core/commit/09a193ce6b4f0b54b027df07d767f29df85ab71c)) ## [2.0.4](https://github.com/IBM/python-sdk-core/compare/v2.0.3...v2.0.4) (2020-05-23) ### Bug Fixes * Revert service_url slash stripping to work with current generated unit tests ([c960b7d](https://github.com/IBM/python-sdk-core/commit/c960b7dc943ac7d8b1bbe748ee7079aa42497504)) ## [2.0.3](https://github.com/IBM/python-sdk-core/compare/v2.0.2...v2.0.3) (2020-05-22) ### Bug Fixes * Don't rstrip slash when service_url is none ([091ecde](https://github.com/IBM/python-sdk-core/commit/091ecde7ab6c8aadc81c71aa35d6a33572856ac8)) ## [2.0.2](https://github.com/IBM/python-sdk-core/compare/v2.0.1...v2.0.2) (2020-05-22) ### Bug Fixes * Strip trailing slash for request url ([47d1d99](https://github.com/IBM/python-sdk-core/commit/47d1d99261767331a2583612ebaf048cf60d1fd3)) ## [2.0.1](https://github.com/IBM/python-sdk-core/compare/v2.0.0...v2.0.1) (2020-05-12) ### Bug Fixes * allow '=' character in environment config values ([8cf4fc9](https://github.com/IBM/python-sdk-core/commit/8cf4fc945a0f77fccf977bfdd0cc3cd203aac0bb)) # [2.0.0](https://github.com/IBM/python-sdk-core/compare/v1.7.3...v2.0.0) (2020-04-10) ### Features * Add type annotations to parameters and return values ([5d4ef81](https://github.com/IBM/python-sdk-core/commit/5d4ef81a7fa85185839b966b80be6d033bc5eed5)) * Get error status phrase from status code ([d60ae58](https://github.com/IBM/python-sdk-core/commit/d60ae582be18af96c21f1e8a55b707f1d2fa44b4)) * Only override content-type if it is none ([b1177f2](https://github.com/IBM/python-sdk-core/commit/b1177f284a0c08255a5ceea26aca9570c4f699dc)) * Require optional parameters to be keyword-specified ([d9aa1d4](https://github.com/IBM/python-sdk-core/commit/d9aa1d4e4bad68961b3c365aaa8b4d5457921c06)) * Update python super call to newer version ([f038e10](https://github.com/IBM/python-sdk-core/commit/f038e103157afc8ad78d9817b1d233dc507e64db)) ### BREAKING CHANGES * Type annotations new in Python3 * Added super call feature new to Python3 * HTTPStatus is new in Python3 * Keyword-specific optional parameters are new in Python3 ## [1.7.3](https://github.com/IBM/python-sdk-core/compare/v1.7.2...v1.7.3) (2020-03-31) ### Bug Fixes * update classifiers in setup.py ([8b042a8](https://github.com/IBM/python-sdk-core/commit/8b042a831f923f8f09812560f8f0085c7431ce83)) ## [1.7.2](https://github.com/IBM/python-sdk-core/compare/v1.7.1...v1.7.2) (2020-03-31) ### Bug Fixes * update setup.py info for pypi ([1e0d63a](https://github.com/IBM/python-sdk-core/commit/1e0d63aa5b07544b0588fe211dea5b162fe67c49)) ## [1.7.1](https://github.com/IBM/python-sdk-core/compare/v1.7.0...v1.7.1) (2020-03-06) ### Bug Fixes * update README to trigger patch release ([bd389b4](https://github.com/IBM/python-sdk-core/commit/bd389b4e0c4451710c6e12d5325cadcabd6c8289)) # [1.7.0](https://github.com/IBM/python-sdk-core/compare/v1.6.2...v1.7.0) (2020-03-02) ### Features * Pace requests to token server for new auth tokens ([1dea212](https://github.com/IBM/python-sdk-core/commit/1dea212b8720849370eb8a05d95d74a469a38ab7)) ## [1.6.2](https://github.com/IBM/python-sdk-core/compare/v1.6.1...v1.6.2) (2020-02-13) ### Bug Fixes * Handle conversions for naive datetime values ([f1149fa](https://github.com/IBM/python-sdk-core/commit/f1149fa815f3f1585b3e02da278dd075b9a1f836)) ## [1.6.1](https://github.com/IBM/python-sdk-core/compare/v1.6.0...v1.6.1) (2020-02-04) ### Bug Fixes * Fix date/datetime_to_string handling of non-date/datetime inputs ([8251b82](https://github.com/IBM/python-sdk-core/commit/8251b820e3a00db855d1960defe75279e3b02515)) # [1.6.0](https://github.com/IBM/python-sdk-core/compare/v1.5.2...v1.6.0) (2019-12-19) ### Features * Add date_to_string and string_to_date utility methods ([6dbfff9](https://github.com/IBM/python-sdk-core/commit/6dbfff92a7758e7cbf78e5cb949dd15beb0dec7f)) ## [1.5.2](https://github.com/IBM/python-sdk-core/compare/v1.5.1...v1.5.2) (2019-12-18) ### Bug Fixes * Update credential file path check to current working directory ([e1ad677](https://github.com/IBM/python-sdk-core/commit/e1ad67781c8bd85739903271deb4ce7a2ea1659a)) ## [1.5.1](https://github.com/IBM/python-sdk-core/compare/v1.5.0...v1.5.1) (2019-11-21) ### Bug Fixes * more semantic-release config changes ([7b20aea](https://github.com/IBM/python-sdk-core/commit/7b20aea08a01df0c5079cb1281c265f31c444d2b)) # [1.5.0](https://github.com/IBM/python-sdk-core/compare/v1.4.0...v1.5.0) (2019-11-21) ### Features * use new semantic-release config ([040c6a7](https://github.com/IBM/python-sdk-core/commit/040c6a7bb458c109c99e1a9e496b788d24ff12bf)) # [1.4.0](https://github.com/IBM/python-sdk-core/compare/v1.3.0...v1.4.0) (2019-11-20) ### Features * configure release commit message format ([3d9cbda](https://github.com/IBM/python-sdk-core/commit/3d9cbda0ae2e263d0faf747dac5a99efb090e995)) # [1.3.0](https://github.com/IBM/python-sdk-core/compare/v1.2.0...v1.3.0) (2019-11-20) ### Features * re-order semrel steps ([525a3fd](https://github.com/IBM/python-sdk-core/commit/525a3fd126a12cb8938c88f89a18f9347394e398)) # [1.2.0](https://github.com/IBM/python-sdk-core/compare/v1.1.3...v1.2.0) (2019-11-20) ### Features * expand vcap credential loading to support user-defined service names ([32954fa](https://github.com/IBM/python-sdk-core/commit/32954fa1aa6d59416dd4b4c07ea91f51024e7d8f)) ## [1.1.3](https://github.com/IBM/python-sdk-core/compare/v1.1.2...v1.1.3) (2019-11-05) ### Bug Fixes * perform semrel steps in correct order ([545f13e](https://github.com/IBM/python-sdk-core/commit/545f13ebba37578f3cf5f1a7abba28ae159c7faa)) ## [1.1.2](https://github.com/IBM/python-sdk-core/compare/v1.1.1...v1.1.2) (2019-11-05) ### Bug Fixes * updated .buildversion.cfg to trigger patch release ([909196f](https://github.com/IBM/python-sdk-core/commit/909196f2a8e0f24736ee6bf9081b87b7dbcfc499)) ## [1.1.1](https://github.com/IBM/python-sdk-core/compare/v1.1.0...v1.1.1) (2019-11-05) ### Bug Fixes * Fix linting for Python3 and fix all lint issues ([14f2999](https://github.com/IBM/python-sdk-core/commit/14f2999010a9886c20f333247912cbe4996fb662)) IBM-python-sdk-core-7ebd71d/CONTRIBUTING.md000066400000000000000000000140131502256645000201010ustar00rootroot00000000000000# Questions If you are having difficulties using the APIs or have a question about the IBM Cloud and Services, please ask a question on [dW Answers][dw] or [Stack Overflow][stackoverflow]. # Issues If you encounter an issue with the Python Core SDK, you are welcome to submit a [bug report](https://github.com/IBM/python-sdk-core/issues). Before that, please search for similar issues. It's possible somebody has encountered this issue already. # Code ## Commit Messages Commit messages should follow the [Angular Commit Message Guidelines](https://github.com/angular/angular/blob/master/CONTRIBUTING.md#-commit-message-guidelines). This is because our release tool - [semantic-release](https://github.com/semantic-release/semantic-release) - uses this format for determining release versions and generating changelogs. Tools such as [commitizen](https://github.com/commitizen/cz-cli) or [commitlint](https://github.com/conventional-changelog/commitlint) can be used to help contributors and enforce commit messages. Here are some examples of acceptable commit messages, along with the release type that would be done based on the commit message: | Commit message | Release type | |-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|----------------------------| | `fix(IAM Authentication) propagate token request errors back to request invocation thread` | Patch Release | | `feat(JSON Serialization): add custom deserializer for dynamic models` | ~~Minor~~ Feature Release | | `feat(BaseService): added baseURL as param to BaseService ctor`

`BREAKING CHANGE: The global-search service has been updated to reflect version 3 of the API.` | ~~Major~~ Breaking Release | # Pull Requests If you want to contribute to the repository, here's a quick guide: 1. Fork the repository 1. Install `virtualenv` and `tox` 1. Develop and test your code changes with [pytest]. * Respect the original code [style guide][styleguide]. * Only use spaces for indentation. * Create minimal diffs - disable on save actions like reformat source code or organize imports. If you feel the source code should be reformatted create a separate PR for this change. * Check for unnecessary whitespace with `git diff --check` before committing. * Make sure your code tests clean on the project's supported versions of Python. You can use the `venv` module to create virtual environments for this. 1. Make the test pass * Linting errors can be fixed by running `make lint-fix` in most cases 1. Check code coverage. Add tests for all new functionality and ensure overall coverage does not decrease. 1. Commit your changes * Commits should follow the [Angular commit message guidelines](https://github.com/angular/angular/blob/master/CONTRIBUTING.md#-commit-message-guidelines). This is because our release tool uses this format for determining release versions and generating changelogs. To make this easier, we recommend using the [Commitizen CLI](https://github.com/commitizen/cz-cli) with the `cz-conventional-changelog` adapter. 1. Push to your fork and submit a pull request to the `dev` branch # Running the tests It is STRONGLY recommended that you set up and use a [virtualenv]. 1. Clone this repository: ```sh git clone git@github.com:IBM/python-sdk-core.git ``` 1. Create a virtual environment: ```sh python -m venv ./venv3 # create venv in directory './venv3' using "python" command . venv3/bin/activate # establish venv's environment python -V # check version of "python" command in venv ``` 1. Install the project dependencies and install the project as an editable package using the current source: ```sh make setup ``` 1. Run the unit tests: ```sh make test-unit ``` 1. Run the lint checks on the source code: ```sh make lint ``` 1. Run the code formatter on the source code to ensure compliance with the linter: ```sh make lint-fix ``` 1. Run the entire build (install dependencies, run unit tests and perform lint checks): ```sh make all ``` # Developer's Certificate of Origin 1.1 By making a contribution to this project, I certify that: (a) The contribution was created in whole or in part by me and I have the right to submit it under the open source license indicated in the file; or (b) The contribution is based upon previous work that, to the best of my knowledge, is covered under an appropriate open source license and I have the right under that license to submit that work with modifications, whether created in whole or in part by me, under the same open source license (unless I am permitted to submit under a different license), as indicated in the file; or (c) The contribution was provided directly to me by some other person who certified (a), (b) or (c) and I have not modified it. (d) I understand and agree that this project and the contribution are public and that a record of the contribution (including all personal information I submit with it, including my sign-off) is maintained indefinitely and may be redistributed consistent with this project or the open source license(s) involved. # Additional Resources * [General GitHub documentation](https://help.github.com/) * [GitHub pull request documentation](https://help.github.com/send-pull-requests/) [dw]: https://developer.ibm.com/answers/questions/ask.html [stackoverflow]: http://stackoverflow.com/questions/ask?tags=ibm [styleguide]: http://google.github.io/styleguide/pyguide.html [pytest]: http://pytest.org/latest/ [virtualenv]: http://virtualenv.readthedocs.org/en/latest/index.html IBM-python-sdk-core-7ebd71d/LICENSE000066400000000000000000000261351502256645000166650ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. IBM-python-sdk-core-7ebd71d/MANIFEST.in000066400000000000000000000000201502256645000173770ustar00rootroot00000000000000include LICENSE IBM-python-sdk-core-7ebd71d/Makefile000066400000000000000000000021411502256645000173070ustar00rootroot00000000000000# This makefile is used to make it easier to get the project set up # to be ready for development work in the local sandbox. # example: "make setup" PYTHON=python3 LINT=black LINT_DIRS=ibm_cloud_sdk_core test test_integration setup: deps dev-deps install-project all: upgrade-pip setup test-unit lint ci: all publish-release: publish-deps build-dist publish-dist upgrade-pip: ${PYTHON} -m pip install --upgrade pip deps: ${PYTHON} -m pip install . dev-deps: ${PYTHON} -m pip install .[dev] detect-secrets: detect-secrets scan --update .secrets.baseline detect-secrets audit .secrets.baseline publish-deps: ${PYTHON} -m pip install .[publish] install-project: ${PYTHON} -m pip install -e . test-unit: ${PYTHON} -m pytest --cov=ibm_cloud_sdk_core test lint: ${PYTHON} -m pylint ${LINT_DIRS} ${LINT} --check ${LINT_DIRS} lint-fix: ${LINT} ${LINT_DIRS} build-dist: rm -fr dist ${PYTHON} -m build # This target requires the TWINE_PASSWORD env variable to be set to the user's pypi.org API token. publish-dist: TWINE_USERNAME=__token__ ${PYTHON} -m twine upload --non-interactive --verbose dist/* IBM-python-sdk-core-7ebd71d/README.md000066400000000000000000000145301502256645000171330ustar00rootroot00000000000000[![Build Status](https://github.com/IBM/python-sdk-core/actions/workflows/build.yaml/badge.svg)](https://github.com/IBM/python-sdk-core/actions/workflows/build.yaml) [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/ibm-cloud-sdk-core)](https://pypi.org/project/ibm-cloud-sdk-core/) [![Latest Stable Version](https://img.shields.io/pypi/v/ibm-cloud-sdk-core.svg)](https://pypi.python.org/pypi/ibm-cloud-sdk-core) [![CLA assistant](https://cla-assistant.io/readme/badge/ibm/python-sdk-core)](https://cla-assistant.io/ibm/python-sdk-core) [![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release) # IBM Python SDK Core Version 3.24.2 This project contains core functionality required by Python code generated by the IBM Cloud OpenAPI SDK Generator (openapi-sdkgen). # Python Version The current minimum Python version supported is 3.9. ## Installation To install, use `pip`: ```bash python -m pip install --upgrade ibm-cloud-sdk-core ``` ## Authentication The python-sdk-core project supports the following types of authentication: - Basic Authentication - Bearer Token Authentication - Identity and Access Management (IAM) Authentication (grant type: apikey) - Identity and Access Management (IAM) Authentication (grant type: assume) - Container Authentication - VPC Instance Authentication - Cloud Pak for Data Authentication - No Authentication (for testing) For more information about the various authentication types and how to use them with your services, click [here](Authentication.md). ## Issues If you encounter an issue with this project, you are welcome to submit a [bug report](https://github.com/IBM/python-sdk-core/issues). Before opening a new issue, please search for similar issues. It's possible that someone has already reported it. ## Logging This library uses Python's built-in `logging` module to perform logging of error, warning, informational and debug messages. The components within the SDK Core library use a single logger named `ibm-cloud-sdk-core`. For complete information on the logging facility, please see: [Logging facility for Python](https://docs.python.org/3/library/logging.html). ### Enable logging There are various ways to configure and enable the logging facility. The code example below demonstrates a simple way to enable debug logging by invoking the `logging.basicConfig()` function. Note that, as a convenience, if you set the logging level to `DEBUG`, then HTTP request/response message logging is also enabled. The following code example shows how debug logging can be enabled: ```python import logging # Create a basic logging configuration that: # 1. Defines a handler to display messages on the console. # 2. Sets the root logger's logging level to DEBUG. # 3. Sets the 'format' string used to display messages. logging.basicConfig(level=logging.DEBUG, format='%(asctime)s [%(name)s:%(levelname)s] %(message)s', force=True) ``` When running your application, you should see output like this if debug logging is enabled: ``` 2024-09-16 15:44:45,174 [ibm-cloud-sdk-core:DEBUG] Get authenticator from environment, key=global_search 2024-09-16 15:44:45,175 [ibm-cloud-sdk-core:DEBUG] Set service URL: https://api.global-search-tagging.cloud.ibm.com 2024-09-16 15:44:45,175 [ibm-cloud-sdk-core:DEBUG] Set User-Agent: ibm-python-sdk-core-3.24.2 os.name=Linux os.version=6.10.9-100.fc39.x86_64 python.version=3.12.5 2024-09-16 15:44:45,181 [ibm-cloud-sdk-core:DEBUG] Configuring BaseService instance with service name: global_search 2024-09-16 15:44:45,181 [ibm-cloud-sdk-core:DEBUG] Performing synchronous token fetch 2024-09-16 15:44:45,182 [ibm-cloud-sdk-core:DEBUG] Invoking IAM get_token operation: https://iam.cloud.ibm.com/identity/token 2024-09-16 15:44:45,182 [urllib3.connectionpool:DEBUG] Starting new HTTPS connection (1): iam.cloud.ibm.com:443 send: b'POST /identity/token HTTP/1.1\r\nHost: iam.cloud.ibm.com\r\nUser-Agent: ibm-python-sdk-core/iam-authenticator-3.24.2 os.name=Linux os.version=6.10.9-100.fc39.x86_64 python.version=3.12.5\r\nAccept-Encoding: gzip, deflate\r\nAccept: application/json\r\nConnection: keep-alive\r\nContent-Type: application/x-www-form-urlencoded\r\nContent-Length: 135\r\n\r\n' send: b'grant_type=urn%3Aibm%3Aparams%3Aoauth%3Agrant-type%3Aapikey&apikey=[redacted]&response_type=cloud_iam' reply: 'HTTP/1.1 200 OK\r\n' header: Content-Type: application/json header: Content-Language: en-US header: Content-Encoding: gzip header: Date: Mon, 16 Sep 2024 20:44:45 GMT header: Content-Length: 983 header: Connection: keep-alive 2024-09-16 15:44:45,670 [urllib3.connectionpool:DEBUG] https://iam.cloud.ibm.com:443 "POST /identity/token HTTP/11" 200 983 2024-09-16 15:44:45,672 [ibm-cloud-sdk-core:DEBUG] Returned from IAM get_token operation 2024-09-16 15:44:45,673 [ibm-cloud-sdk-core:DEBUG] Authenticated outbound request (type=iam) 2024-09-16 15:44:45,673 [ibm-cloud-sdk-core:DEBUG] Prepared request [POST https://api.global-search-tagging.cloud.ibm.com/v3/resources/search] 2024-09-16 15:44:45,673 [ibm-cloud-sdk-core:DEBUG] Sending HTTP request message 2024-09-16 15:44:45,674 [urllib3.connectionpool:DEBUG] Starting new HTTPS connection (1): api.global-search-tagging.cloud.ibm.com:443 send: b'POST /v3/resources/search?limit=1 HTTP/1.1\r\nHost: api.global-search-tagging.cloud.ibm.com\r\nUser-Agent: platform-services-python-sdk/0.57.0 (lang=python; os.name=Linux; os.version=6.10.9-100.fc39.x86_64; python.version=3.12.5)\r\nAccept-Encoding: gzip, deflate\r\nAccept: application/json\r\nConnection: keep-alive\r\ncontent-type: application/json\r\nAuthorization: [redacted]\r\nContent-Length: 39\r\n\r\n' send: b'{"query": "GST-sdk-*", "fields": ["*"]}' reply: 'HTTP/1.1 200 OK\r\n' header: Content-Type: application/json header: Content-Length: 22 header: Date: Mon, 16 Sep 2024 20:44:46 GMT header: Connection: keep-alive 2024-09-16 15:44:46,079 [urllib3.connectionpool:DEBUG] https://api.global-search-tagging.cloud.ibm.com:443 "POST /v3/resources/search?limit=1 HTTP/11" 200 22 2024-09-16 15:44:46,080 [ibm-cloud-sdk-core:DEBUG] Received HTTP response message, status code 200 ``` ## Open source @ IBM Find more open source projects on the [IBM Github Page](http://github.com/IBM) ## License This library is licensed under Apache 2.0. Full license text is available in [LICENSE](LICENSE). ## Contributing See [CONTRIBUTING.md](CONTRIBUTING.md). IBM-python-sdk-core-7ebd71d/appscan-config.xml000066400000000000000000000001751502256645000212660ustar00rootroot00000000000000 test/ IBM-python-sdk-core-7ebd71d/ibm_cloud_sdk_core/000077500000000000000000000000001502256645000214575ustar00rootroot00000000000000IBM-python-sdk-core-7ebd71d/ibm_cloud_sdk_core/__init__.py000066400000000000000000000060301502256645000235670ustar00rootroot00000000000000# coding: utf-8 # Copyright 2019 IBM All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Classes and helper functions used by generated SDKs. classes: BaseService: Abstract class for common functionality between each service. DetailedResponse: The object returned from successful service operations. IAMTokenManager: Requests and refreshes IAM tokens using an apikey, and optionally a client_id and client_secret. IAMAssumeTokenManager: Requests and refreshes IAM tokens using an apikey and a trusted profile. JWTTokenManager: Abstract class for common functionality between each JWT token manager. CP4DTokenManager: Requests and refreshes CP4D tokens given a username and password. ApiException: Custom exception class for errors returned from service operations. functions: datetime_to_string: Serializes a datetime to a string. string_to_datetime: De-serializes a string to a datetime. datetime_to_string_list: Serializes a list of datetimes to a list of strings. string_to_datetime_list: De-serializes a list of strings to a list of datetimes. date_to_string: Serializes a date to a string. string_to_date: De-serializes a string to a date. convert_model: Convert a model object into an equivalent dict. convert_list: Convert a list of strings into comma-separated string. get_query_param: Return a query parameter value from a URL read_external_sources: Get config object from external sources. get_authenticator_from_environment: Get authenticator from external sources. """ from .base_service import BaseService from .detailed_response import DetailedResponse from .token_managers.iam_token_manager import IAMTokenManager from .token_managers.iam_assume_token_manager import IAMAssumeTokenManager from .token_managers.jwt_token_manager import JWTTokenManager from .token_managers.cp4d_token_manager import CP4DTokenManager from .token_managers.container_token_manager import ContainerTokenManager from .token_managers.vpc_instance_token_manager import VPCInstanceTokenManager from .token_managers.mcsp_token_manager import MCSPTokenManager from .token_managers.mcspv2_token_manager import MCSPV2TokenManager from .api_exception import ApiException from .utils import datetime_to_string, string_to_datetime, read_external_sources from .utils import datetime_to_string_list, string_to_datetime_list from .utils import date_to_string, string_to_date from .utils import convert_model, convert_list from .utils import get_query_param from .get_authenticator import get_authenticator_from_environment IBM-python-sdk-core-7ebd71d/ibm_cloud_sdk_core/api_exception.py000066400000000000000000000071061502256645000246640ustar00rootroot00000000000000# coding: utf-8 # Copyright 2019 IBM All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import warnings from http import HTTPStatus from typing import Optional from requests import Response class ApiException(Exception): """Custom exception class for errors returned from operations. Args: code: HTTP status code of the error response. message: The error response body. Defaults to None. http_response: The HTTP response of the failed request. Defaults to None. Attributes: code (int): HTTP status code of the error response. message (str): The error response body. http_response (requests.Response): The HTTP response of the failed request. global_transaction_id (str, optional): Globally unique id the service endpoint has given a transaction. """ def __init__(self, code: int, *, message: Optional[str] = None, http_response: Optional[Response] = None) -> None: # Call the base class constructor with the parameters it needs super().__init__(message) self.message = message self.status_code = code self.http_response = http_response self.global_transaction_id = None if http_response is not None: self.global_transaction_id = http_response.headers.get('X-Global-Transaction-ID') self.message = self.message if self.message else self._get_error_message(http_response) # pylint: disable=fixme # TODO: delete this by the end of 2024. @property def code(self): """The old `code` property with a deprecation warning.""" warnings.warn( 'Using the `code` attribute on the `ApiException` is deprecated and ' 'will be removed in the future. Use `status_code` instead.', DeprecationWarning, ) return self.status_code def __str__(self) -> str: msg = 'Error: ' + str(self.message) + ', Status code: ' + str(self.status_code) if self.global_transaction_id is not None: msg += ' , X-global-transaction-id: ' + str(self.global_transaction_id) return msg @staticmethod def _get_error_message(response: Response) -> str: error_message = 'Unknown error' try: error_json = response.json(strict=False) if 'errors' in error_json: if isinstance(error_json['errors'], list): err = error_json['errors'][0] error_message = err.get('message') elif 'error' in error_json: error_message = error_json['error'] elif 'message' in error_json: error_message = error_json['message'] elif 'errorMessage' in error_json: error_message = error_json['errorMessage'] elif response.status_code == 401: error_message = 'Unauthorized: Access is denied due to invalid credentials' else: error_message = HTTPStatus(response.status_code).phrase return error_message except: return response.text or error_message IBM-python-sdk-core-7ebd71d/ibm_cloud_sdk_core/authenticators/000077500000000000000000000000001502256645000245145ustar00rootroot00000000000000IBM-python-sdk-core-7ebd71d/ibm_cloud_sdk_core/authenticators/__init__.py000066400000000000000000000050761502256645000266350ustar00rootroot00000000000000# coding: utf-8 # Copyright 2019 IBM All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """The ibm_cloud_sdk_core project supports the following types of authentication: Basic Authentication Bearer Token Identity and Access Management (IAM) Cloud Pak for Data No Authentication The authentication types that are appropriate for a particular service may vary from service to service. Each authentication type is implemented as an Authenticator for consumption by a service. classes: Authenticator: Abstract Base Class. Implement this interface to provide custom authentication schemes to services. BasicAuthenticator: Authenticator for passing supplied basic authentication information to service endpoint. BearerTokenAuthenticator: Authenticator for passing supplied bearer token to service endpoint. ContainerAuthenticator: Authenticator for use in a container environment. CloudPakForDataAuthenticator: Authenticator for passing CP4D authentication information to service endpoint. IAMAuthenticator: Authenticator for passing IAM authentication information to service endpoint. IAMAssumeAuthenticator: Authenticator for the "assume" grant type. VPCInstanceAuthenticator: Authenticator for use within a VPC instance. NoAuthAuthenticator: Performs no authentication. Useful for testing purposes. MCSPAuthenticator: Authenticator that supports the MCSP v1 token exchange. MCSPV2Authenticator: Authenticator that supports the MCSP v2 token exchange. """ from .authenticator import Authenticator from .basic_authenticator import BasicAuthenticator from .bearer_token_authenticator import BearerTokenAuthenticator from .container_authenticator import ContainerAuthenticator from .cp4d_authenticator import CloudPakForDataAuthenticator from .iam_authenticator import IAMAuthenticator from .iam_assume_authenticator import IAMAssumeAuthenticator from .vpc_instance_authenticator import VPCInstanceAuthenticator from .no_auth_authenticator import NoAuthAuthenticator from .mcsp_authenticator import MCSPAuthenticator from .mcspv2_authenticator import MCSPV2Authenticator IBM-python-sdk-core-7ebd71d/ibm_cloud_sdk_core/authenticators/authenticator.py000066400000000000000000000040001502256645000277320ustar00rootroot00000000000000# coding: utf-8 # Copyright 2019, 2025 IBM All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from abc import ABC, abstractmethod class Authenticator(ABC): """This interface defines the common methods and constants associated with an Authenticator implementation.""" # Constants representing the various authenticator types. AUTHTYPE_BASIC = 'basic' AUTHTYPE_BEARERTOKEN = 'bearerToken' AUTHTYPE_IAM = 'iam' AUTHTYPE_IAM_ASSUME = 'iamAssume' AUTHTYPE_CONTAINER = 'container' AUTHTYPE_CP4D = 'cp4d' AUTHTYPE_VPC = 'vpc' AUTHTYPE_NOAUTH = 'noAuth' AUTHTYPE_MCSP = 'mcsp' AUTHTYPE_MCSPV2 = 'mcspv2' AUTHTYPE_UNKNOWN = 'unknown' @abstractmethod def authenticate(self, req: dict) -> None: """Perform the necessary authentication steps for the specified request. Attributes: req (dict): Will be modified to contain the appropriate authentication information. To be implemented by subclasses. """ pass @abstractmethod def validate(self) -> None: """Validates the current set of configuration information in the Authenticator. Raises: ValueError: The configuration information is not valid for service operations. To be implemented by subclasses. """ pass def authentication_type(self) -> str: """Returns the authenticator's type. This method should be overridden by each authenticator implementation.""" return self.AUTHTYPE_UNKNOWN IBM-python-sdk-core-7ebd71d/ibm_cloud_sdk_core/authenticators/basic_authenticator.py000066400000000000000000000064461502256645000311130ustar00rootroot00000000000000# coding: utf-8 # Copyright 2019, 2024 IBM All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import base64 from requests import Request from ibm_cloud_sdk_core.logger import get_logger from .authenticator import Authenticator from ..utils import has_bad_first_or_last_char logger = get_logger() class BasicAuthenticator(Authenticator): """The BasicAuthenticator is used to add basic authentication information to requests. Basic Authorization will be sent as an Authorization header in the form: Authorization: Basic Args: username: User-supplied username for basic auth. password: User-supplied password for basic auth. Raises: ValueError: The username or password is not specified or contains invalid characters. """ def __init__(self, username: str, password: str) -> None: self.username = username self.password = password self.validate() self.authorization_header = self.__construct_basic_auth_header() logger.debug('Created new BasicAuthenticator instance!') def authentication_type(self) -> str: """Returns this authenticator's type ('basic').""" return Authenticator.AUTHTYPE_BASIC def validate(self) -> None: """Validate username and password. Ensure the username and password are valid for service operations. Raises: ValueError: The username and/or password is not valid for service operations. """ if self.username is None or self.password is None: raise ValueError('The username and password shouldn\'t be None.') if has_bad_first_or_last_char(self.username) or has_bad_first_or_last_char(self.password): raise ValueError( 'The username and password shouldn\'t start or end with curly brackets or quotes. ' 'Please remove any surrounding {, }, or \" characters.' ) def __construct_basic_auth_header(self) -> str: authstring = "{0}:{1}".format(self.username, self.password) base64_authorization = base64.b64encode(authstring.encode('utf-8')).decode('utf-8') return 'Basic {0}'.format(base64_authorization) def authenticate(self, req: Request) -> None: """Add basic authentication information to a request. Basic Authorization will be added to the request's headers in the form: Authorization: Basic Args: req: The request to add basic auth information to. Must contain a key to a dictionary called headers. """ headers = req.get('headers') headers['Authorization'] = self.authorization_header logger.debug('Authenticated outbound request (type=%s)', self.authentication_type()) IBM-python-sdk-core-7ebd71d/ibm_cloud_sdk_core/authenticators/bearer_token_authenticator.py000066400000000000000000000054551502256645000324710ustar00rootroot00000000000000# coding: utf-8 # Copyright 2019, 2024 IBM All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from requests import Request from ibm_cloud_sdk_core.logger import get_logger from .authenticator import Authenticator logger = get_logger() class BearerTokenAuthenticator(Authenticator): """The BearerTokenAuthenticator will add a user-supplied bearer token to requests. The bearer token will be sent as an Authorization header in the form: Authorization: Bearer Args: bearer_token: The user supplied bearer token. Raises: ValueError: Bearer token is none. """ def __init__(self, bearer_token: str) -> None: self.bearer_token = bearer_token self.validate() logger.debug('Created BearerTokenAuthenticator instance!') def authentication_type(self) -> str: """Returns this authenticator's type ('bearertoken').""" return Authenticator.AUTHTYPE_BEARERTOKEN def validate(self) -> None: """Validate the bearer token. Ensures the bearer token is valid for service operations. Raises: ValueError: The bearer token is not valid for service operations. """ if self.bearer_token is None: raise ValueError('The bearer token shouldn\'t be None.') def authenticate(self, req: Request) -> None: """Adds bearer authentication information to the request. The bearer token will be added to the request's headers in the form: Authorization: Bearer Args: req: The request to add bearer authentication information to. Must contain a key to a dictionary called headers. """ headers = req.get('headers') headers['Authorization'] = 'Bearer {0}'.format(self.bearer_token) logger.debug('Authenticated outbound request (type=%s)', self.authentication_type()) def set_bearer_token(self, bearer_token: str) -> None: """Set a new bearer token to be sent in subsequent service operations. Args: bearer_token: The bearer token that will be sent in service requests. Raises: ValueError: The bearer token is not valid for service operations. """ self.bearer_token = bearer_token self.validate() IBM-python-sdk-core-7ebd71d/ibm_cloud_sdk_core/authenticators/container_authenticator.py000066400000000000000000000156151502256645000320120ustar00rootroot00000000000000# coding: utf-8 # Copyright 2021 IBM All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from typing import Dict, Optional from .iam_request_based_authenticator import IAMRequestBasedAuthenticator from ..token_managers.container_token_manager import ContainerTokenManager from .authenticator import Authenticator class ContainerAuthenticator(IAMRequestBasedAuthenticator): """ContainerAuthenticator implements an IAM-based authentication schema where by it retrieves a "compute resource token" from the local compute resource (VM) and uses that to obtain an IAM access token by invoking the IAM "get token" operation with grant-type=cr-token. The resulting IAM access token is then added to outbound requests in an Authorization header of the form: Authorization: Bearer Args: cr_token_filename: The name of the file containing the injected CR token value (applies to IKS-managed compute resources). Defaults to "/var/run/secrets/tokens/vault-token". iam_profile_name: The name of the linked trusted IAM profile to be used when obtaining the IAM access token (a CR token might map to multiple IAM profiles). One of iam_profile_name or iam_profile_id must be specified. Defaults to None. iam_profile_id: The id of the linked trusted IAM profile to be used when obtaining the IAM access token (a CR token might map to multiple IAM profiles). One of iam_profile_name or iam_profile_id must be specified. Defaults to None. url: The URL representing the IAM token service endpoint. If not specified, a suitable default value is used. client_id: The client_id and client_secret fields are used to form a "basic" authorization header for IAM token requests. Defaults to None. client_secret: The client_id and client_secret fields are used to form a "basic" authorization header for IAM token requests. Defaults to None. disable_ssl_verification: A flag that indicates whether verification of the server's SSL certificate should be disabled or not. Defaults to False. headers: Default headers to be sent with every IAM token request. Defaults to None. proxies: Dictionary for mapping request protocol to proxy URL. Defaults to None. proxies.http (optional): The proxy endpoint to use for HTTP requests. proxies.https (optional): The proxy endpoint to use for HTTPS requests. scope: The "scope" to use when fetching the bearer token from the IAM token server. This can be used to obtain an access token with a specific scope. Attributes: token_manager (ContainerTokenManager): Retrieves and manages IAM tokens from the endpoint specified by the url. Raises: TypeError: The `disable_ssl_verification` is not a bool. ValueError: Neither of iam_profile_name or iam_profile_idk are set, or client_id, and/or client_secret are not valid for IAM token requests. """ def __init__( self, *, cr_token_filename: Optional[str] = None, iam_profile_name: Optional[str] = None, iam_profile_id: Optional[str] = None, url: Optional[str] = None, client_id: Optional[str] = None, client_secret: Optional[str] = None, disable_ssl_verification: bool = False, scope: Optional[str] = None, proxies: Optional[Dict[str, str]] = None, headers: Optional[Dict[str, str]] = None, ) -> None: # Check the type of `disable_ssl_verification`. Must be a bool. if not isinstance(disable_ssl_verification, bool): raise TypeError('disable_ssl_verification must be a bool') self.token_manager = ContainerTokenManager( cr_token_filename=cr_token_filename, iam_profile_name=iam_profile_name, iam_profile_id=iam_profile_id, url=url, client_id=client_id, client_secret=client_secret, disable_ssl_verification=disable_ssl_verification, scope=scope, proxies=proxies, headers=headers, ) self.validate() def authentication_type(self) -> str: """Returns this authenticator's type ('container').""" return Authenticator.AUTHTYPE_CONTAINER def validate(self) -> None: """Validates the iam_profile_name, iam_profile_id, client_id, and client_secret for IAM token requests. Ensure that one of the iam_profile_name or iam_profile_id are specified. Additionally, ensure both of the client_id and client_secret are set if either of them are defined. Raises: ValueError: Neither of iam_profile_name or iam_profile_idk are set, or client_id, and/or client_secret are not valid for IAM token requests. """ super().validate() if not self.token_manager.iam_profile_name and not self.token_manager.iam_profile_id: raise ValueError('At least one of iam_profile_name or iam_profile_id must be specified.') def set_cr_token_filename(self, cr_token_filename: str) -> None: """Set the location of the compute resource token on the local filesystem. Args: cr_token_filename: path to the compute resource token """ self.token_manager.cr_token_filename = cr_token_filename def set_iam_profile_name(self, iam_profile_name: str) -> None: """Set the name of the IAM profile. Args: iam_profile_name: name of the linked trusted IAM profile to be used when obtaining the IAM access token Raises: ValueError: Neither of iam_profile_name or iam_profile_idk are set, or client_id, and/or client_secret are not valid for IAM token requests. """ self.token_manager.iam_profile_name = iam_profile_name self.validate() def set_iam_profile_id(self, iam_profile_id: str) -> None: """Set the id of the IAM profile. Args: iam_profile_id: id of the linked trusted IAM profile to be used when obtaining the IAM access token Raises: ValueError: Neither of iam_profile_name or iam_profile_idk are set, or client_id, and/or client_secret are not valid for IAM token requests. """ self.token_manager.iam_profile_id = iam_profile_id self.validate() IBM-python-sdk-core-7ebd71d/ibm_cloud_sdk_core/authenticators/cp4d_authenticator.py000066400000000000000000000154121502256645000306550ustar00rootroot00000000000000# coding: utf-8 # Copyright 2019, 2024 IBM All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from typing import Dict, Optional from requests import Request from ibm_cloud_sdk_core.logger import get_logger from .authenticator import Authenticator from ..token_managers.cp4d_token_manager import CP4DTokenManager from ..utils import has_bad_first_or_last_char logger = get_logger() class CloudPakForDataAuthenticator(Authenticator): """The CloudPakForDataAuthenticator utilizes a username and password pair to obtain a suitable bearer token, and adds it requests. The bearer token will be sent as an Authorization header in the form: Authorization: Bearer Keyword Args: username: The username used to obtain a bearer token [required]. password: The password used to obtain a bearer token [required if apikey not specified]. url: The URL representing the Cloud Pak for Data token service endpoint [required]. apikey: The API key used to obtain a bearer token [required if password not specified]. disable_ssl_verification: A flag that indicates whether verification of the server's SSL certificate should be disabled or not. Defaults to False. headers: Default headers to be sent with every CP4D token request. Defaults to None. proxies: Dictionary for mapping request protocol to proxy URL. proxies.http (optional): The proxy endpoint to use for HTTP requests. proxies.https (optional): The proxy endpoint to use for HTTPS requests. verify (optional): The path to the certificate to use for HTTPS requests. Attributes: token_manager (CP4DTokenManager): Retrieves and manages CP4D tokens from the endpoint specified by the url. Raises: TypeError: The `disable_ssl_verification` is not a bool. ValueError: The username, password/apikey, and/or url are not valid for CP4D token requests. """ def __init__( self, username: str = None, password: str = None, url: str = None, *, apikey: str = None, disable_ssl_verification: bool = False, headers: Optional[Dict[str, str]] = None, proxies: Optional[Dict[str, str]] = None, verify: Optional[str] = None, ) -> None: # Check the type of `disable_ssl_verification`. Must be a bool. if not isinstance(disable_ssl_verification, bool): raise TypeError('disable_ssl_verification must be a bool') self.token_manager = CP4DTokenManager( username=username, password=password, apikey=apikey, url=url, disable_ssl_verification=disable_ssl_verification, headers=headers, proxies=proxies, verify=verify, ) self.validate() def authentication_type(self) -> str: """Returns this authenticator's type ('cp4d').""" return Authenticator.AUTHTYPE_CP4D def validate(self) -> None: """Validate username, password, and url for token requests. Ensures the username, password, and url are not None. Additionally, ensures they do not contain invalid characters. Raises: ValueError: The username, password, and/or url are not valid for token requests. """ if self.token_manager.username is None: raise ValueError('The username shouldn\'t be None.') if (self.token_manager.password is None and self.token_manager.apikey is None) or ( self.token_manager.password is not None and self.token_manager.apikey is not None ): raise ValueError('Exactly one of `apikey` or `password` must be specified.') if self.token_manager.url is None: raise ValueError('The url shouldn\'t be None.') if has_bad_first_or_last_char(self.token_manager.username) or has_bad_first_or_last_char( self.token_manager.password ): raise ValueError( 'The username and password shouldn\'t start or end with curly brackets or quotes. ' 'Please remove any surrounding {, }, or \" characters.' ) if has_bad_first_or_last_char(self.token_manager.url): raise ValueError( 'The url shouldn\'t start or end with curly brackets or quotes. ' 'Please remove any surrounding {, }, or \" characters.' ) def authenticate(self, req: Request) -> None: """Adds CP4D authentication information to the request. The CP4D bearer token will be added to the request's headers in the form: Authorization: Bearer Args: req: The request to add CP4D authentication information to. Must contain a key to a dictionary called headers. """ headers = req.get('headers') bearer_token = self.token_manager.get_token() headers['Authorization'] = 'Bearer {0}'.format(bearer_token) logger.debug('Authenticated outbound request (type=%s)', self.authentication_type()) def set_disable_ssl_verification(self, status: bool = False) -> None: """Set the flag that indicates whether verification of the server's SSL certificate should be disabled or not. Defaults to False. Args: status: Set to true in order to disable SSL certificate verification. Defaults to False. Raises: TypeError: The `status` is not a bool. """ self.token_manager.set_disable_ssl_verification(status) def set_headers(self, headers: Dict[str, str]) -> None: """Default headers to be sent with every CP4D token request. Args: headers: The headers to be sent with every CP4D token request. """ self.token_manager.set_headers(headers) def set_proxies(self, proxies: Dict[str, str]) -> None: """Sets the proxies the token manager will use to communicate with CP4D on behalf of the host. Args: proxies: Dictionary for mapping request protocol to proxy URL. proxies.http (optional): The proxy endpoint to use for HTTP requests. proxies.https (optional): The proxy endpoint to use for HTTPS requests. """ self.token_manager.set_proxies(proxies) IBM-python-sdk-core-7ebd71d/ibm_cloud_sdk_core/authenticators/iam_assume_authenticator.py000066400000000000000000000151641502256645000321520ustar00rootroot00000000000000# coding: utf-8 # Copyright 2024 IBM All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from typing import Any, Dict, Optional from ibm_cloud_sdk_core.authenticators.iam_authenticator import IAMAuthenticator from ibm_cloud_sdk_core.token_managers.iam_assume_token_manager import IAMAssumeTokenManager from .authenticator import Authenticator from .iam_request_based_authenticator import IAMRequestBasedAuthenticator class IAMAssumeAuthenticator(IAMRequestBasedAuthenticator): """IAMAssumeAuthenticator obtains an IAM access token using the IAM "get-token" operation's "assume" grant type. The authenticator obtains an initial IAM access token from a user-supplied apikey, then exchanges this initial IAM access token for another IAM access token that has "assumed the identity" of the specified trusted profile. The bearer token will be sent as an Authorization header in the form: Authorization: Bearer Args: apikey: The IAM api key. Keyword Args: iam_profile_id: the ID of the trusted profile iam_profile_crn: the CRN of the trusted profile iam_profile_name: the name of the trusted profile (must be used together with `iam_account_id`) iam_account_id: the ID of the trusted profile (must be used together with `iam_profile_name`) url: The URL representing the IAM token service endpoint. If not specified, a suitable default value is used. client_id: The client_id and client_secret fields are used to form a "basic" authorization header for IAM token requests. Defaults to None. client_secret: The client_id and client_secret fields are used to form a "basic" authorization header for IAM token requests. Defaults to None. disable_ssl_verification: A flag that indicates whether verification of the server's SSL certificate should be disabled or not. Defaults to False. headers: Default headers to be sent with every IAM token request. Defaults to None. proxies: Dictionary for mapping request protocol to proxy URL. Defaults to None. proxies.http (optional): The proxy endpoint to use for HTTP requests. proxies.https (optional): The proxy endpoint to use for HTTPS requests. scope: The "scope" to use when fetching the bearer token from the IAM token server. This can be used to obtain an access token with a specific scope. Attributes: token_manager (IAMTokenManager): Retrieves and manages IAM tokens from the endpoint specified by the url. Raises: TypeError: The `disable_ssl_verification` is not a bool. ValueError: The `apikey`, `client_id`, and/or `client_secret` are not valid for IAM token requests or the following keyword arguments are incorrectly specified: `iam_profile_id`, `iam_profile_crn`, `iam_profile_name`, `iam_account_id`. """ def __init__( self, apikey: str, *, iam_profile_id: Optional[str] = None, iam_profile_crn: Optional[str] = None, iam_profile_name: Optional[str] = None, iam_account_id: Optional[str] = None, url: Optional[str] = None, client_id: Optional[str] = None, client_secret: Optional[str] = None, disable_ssl_verification: bool = False, headers: Optional[Dict[str, str]] = None, proxies: Optional[Dict[str, str]] = None, scope: Optional[str] = None, ) -> None: # Check the type of `disable_ssl_verification`. Must be a bool. if not isinstance(disable_ssl_verification, bool): raise TypeError('disable_ssl_verification must be a bool') self.token_manager = IAMAssumeTokenManager( apikey, iam_profile_id=iam_profile_id, iam_profile_crn=iam_profile_crn, iam_profile_name=iam_profile_name, iam_account_id=iam_account_id, url=url, client_id=client_id, client_secret=client_secret, disable_ssl_verification=disable_ssl_verification, headers=headers, proxies=proxies, scope=scope, ) self.validate() # Disable all setter methods, inherited from the parent class. def __getattribute__(self, name: str) -> Any: if name.startswith("set_"): raise AttributeError(f"'{self.__class__.__name__}' has no attribute '{name}'") return super().__getattribute__(name) def authentication_type(self) -> str: """Returns this authenticator's type ('iamAssume').""" return Authenticator.AUTHTYPE_IAM_ASSUME def validate(self) -> None: """Validates the provided IAM related arguments. Ensure the following: - `apikey` of the IAMTokenManager is not `None`, and has no bad characters - both `client_id` and `client_secret` are set if either of them are defined - the correct number and type of IAM profile and IAM account options are specified Raises: ValueError: The apikey, client_id, and/or client_secret are not valid for IAM token requests. """ # Create a temporary IAM authenticator that we can use to validate our delegate. tmp_authenticator = IAMAuthenticator("") tmp_authenticator.token_manager = self.token_manager.iam_delegate tmp_authenticator.validate() del tmp_authenticator # Only one of the following arguments must be specified. mutually_exclusive_attributes = [ self.token_manager.iam_profile_id, self.token_manager.iam_profile_crn, self.token_manager.iam_profile_name, ] if list(map(bool, mutually_exclusive_attributes)).count(True) != 1: raise ValueError( 'Exactly one of `iam_profile_id`, `iam_profile_crn`, or `iam_profile_name` must be specified.' ) # `iam_account_id` must be specified iff `iam_profile_name` is used. if self.token_manager.iam_profile_name and not self.token_manager.iam_account_id: raise ValueError('`iam_profile_name` and `iam_account_id` must be provided together, or not at all.') IBM-python-sdk-core-7ebd71d/ibm_cloud_sdk_core/authenticators/iam_authenticator.py000066400000000000000000000107651502256645000305770ustar00rootroot00000000000000# coding: utf-8 # Copyright 2019 IBM All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from typing import Dict, Optional from .authenticator import Authenticator from .iam_request_based_authenticator import IAMRequestBasedAuthenticator from ..token_managers.iam_token_manager import IAMTokenManager from ..utils import has_bad_first_or_last_char class IAMAuthenticator(IAMRequestBasedAuthenticator): """The IAMAuthenticator utilizes an apikey, or client_id and client_secret pair to obtain a suitable bearer token, and adds it to requests. The bearer token will be sent as an Authorization header in the form: Authorization: Bearer Args: apikey: The IAM api key. Keyword Args: url: The URL representing the IAM token service endpoint. If not specified, a suitable default value is used. client_id: The client_id and client_secret fields are used to form a "basic" authorization header for IAM token requests. Defaults to None. client_secret: The client_id and client_secret fields are used to form a "basic" authorization header for IAM token requests. Defaults to None. disable_ssl_verification: A flag that indicates whether verification of the server's SSL certificate should be disabled or not. Defaults to False. headers: Default headers to be sent with every IAM token request. Defaults to None. proxies: Dictionary for mapping request protocol to proxy URL. Defaults to None. proxies.http (optional): The proxy endpoint to use for HTTP requests. proxies.https (optional): The proxy endpoint to use for HTTPS requests. scope: The "scope" to use when fetching the bearer token from the IAM token server. This can be used to obtain an access token with a specific scope. Attributes: token_manager (IAMTokenManager): Retrieves and manages IAM tokens from the endpoint specified by the url. Raises: TypeError: The `disable_ssl_verification` is not a bool. ValueError: The apikey, client_id, and/or client_secret are not valid for IAM token requests. """ def __init__( self, apikey: str, *, url: Optional[str] = None, client_id: Optional[str] = None, client_secret: Optional[str] = None, disable_ssl_verification: bool = False, headers: Optional[Dict[str, str]] = None, proxies: Optional[Dict[str, str]] = None, scope: Optional[str] = None, ) -> None: # Check the type of `disable_ssl_verification`. Must be a bool. if not isinstance(disable_ssl_verification, bool): raise TypeError('disable_ssl_verification must be a bool') self.token_manager = IAMTokenManager( apikey, url=url, client_id=client_id, client_secret=client_secret, disable_ssl_verification=disable_ssl_verification, headers=headers, proxies=proxies, scope=scope, ) self.validate() def authentication_type(self) -> str: """Returns this authenticator's type ('iam').""" return Authenticator.AUTHTYPE_IAM def validate(self) -> None: """Validates the apikey, client_id, and client_secret for IAM token requests. Ensure the apikey is not none, and has no bad characters. Additionally, ensure the both the client_id and client_secret are both set if either of them are defined. Raises: ValueError: The apikey, client_id, and/or client_secret are not valid for IAM token requests. """ super().validate() if self.token_manager.apikey is None: raise ValueError('The apikey shouldn\'t be None.') if has_bad_first_or_last_char(self.token_manager.apikey): raise ValueError( 'The apikey shouldn\'t start or end with curly brackets or quotes. ' 'Please remove any surrounding {, }, or \" characters.' ) IBM-python-sdk-core-7ebd71d/ibm_cloud_sdk_core/authenticators/iam_request_based_authenticator.py000066400000000000000000000111161502256645000334740ustar00rootroot00000000000000# coding: utf-8 # Copyright 2019, 2024 IBM All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from typing import Dict from requests import Request from ibm_cloud_sdk_core.logger import get_logger from .authenticator import Authenticator logger = get_logger() class IAMRequestBasedAuthenticator(Authenticator): """The IAMRequestBasedAuthenticator class contains code that is common to all authenticators that need to interact with the IAM tokens service to obtain an access token. The bearer token will be sent as an Authorization header in the form: Authorization: Bearer Attributes: token_manager (TokenManager): Retrieves and manages IAM tokens from the endpoint specified by the url. """ def validate(self) -> None: """Validates the client_id, and client_secret for IAM token requests. Ensure both the client_id and client_secret are set if either of them are defined. Raises: ValueError: The client_id, and/or client_secret are not valid for IAM token requests. """ if (self.token_manager.client_id and not self.token_manager.client_secret) or ( not self.token_manager.client_id and self.token_manager.client_secret ): raise ValueError('Both client_id and client_secret should be initialized.') def authenticate(self, req: Request) -> None: """Adds IAM authentication information to the request. The IAM bearer token will be added to the request's headers in the form: Authorization: Bearer Args: req: The request to add IAM authentication information to. Must contain a key to a dictionary called headers. """ headers = req.get('headers') bearer_token = self.token_manager.get_token() headers['Authorization'] = 'Bearer {0}'.format(bearer_token) logger.debug('Authenticated outbound request (type=%s)', self.authentication_type()) def set_client_id_and_secret(self, client_id: str, client_secret: str) -> None: """Set the client_id and client_secret pair the token manager will use for IAM token requests. Args: client_id: The client id to be used in basic auth. client_secret: The client secret to be used in basic auth. Raises: ValueError: The apikey, client_id, and/or client_secret are not valid for IAM token requests. """ self.token_manager.set_client_id_and_secret(client_id, client_secret) self.validate() def set_disable_ssl_verification(self, status: bool = False) -> None: """Set the flag that indicates whether verification of the server's SSL certificate should be disabled or not. Defaults to False. Args: status: Headers to be sent with every IAM token request. Defaults to None Raises: TypeError: The `status` is not a bool. """ self.token_manager.set_disable_ssl_verification(status) def set_headers(self, headers: Dict[str, str]) -> None: """Headers to be sent with every IAM token request. Args: headers: Headers to be sent with every IAM token request. """ self.token_manager.set_headers(headers) def set_proxies(self, proxies: Dict[str, str]) -> None: """Sets the proxies the token manager will use to communicate with IAM on behalf of the host. Args: proxies: Dictionary for mapping request protocol to proxy URL. proxies.http (optional): The proxy endpoint to use for HTTP requests. proxies.https (optional): The proxy endpoint to use for HTTPS requests. """ self.token_manager.set_proxies(proxies) def set_scope(self, value: str) -> None: """Sets the "scope" parameter to use when fetching the bearer token from the IAM token server. This can be used to obtain an access token with a specific scope. Args: value: A space seperated string that makes up the scope parameter. """ self.token_manager.set_scope(value) IBM-python-sdk-core-7ebd71d/ibm_cloud_sdk_core/authenticators/mcsp_authenticator.py000066400000000000000000000123461502256645000307700ustar00rootroot00000000000000# coding: utf-8 # Copyright 2023, 2024 IBM All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from typing import Dict, Optional from requests import Request from ibm_cloud_sdk_core.logger import get_logger from .authenticator import Authenticator from ..token_managers.mcsp_token_manager import MCSPTokenManager logger = get_logger() class MCSPAuthenticator(Authenticator): """The MCSPAuthenticator uses an apikey to obtain an access token from the MCSP token server. When the access token expires, a new access token is obtained from the token server. The access token will be added to outbound requests via the Authorization header of the form: "Authorization: Bearer " Keyword Args: url: The base endpoint URL for the MCSP token service [required]. apikey: The API key used to obtain an access token [required]. disable_ssl_verification: A flag that indicates whether verification of the server's SSL certificate should be disabled or not. Defaults to False. headers: Default headers to be sent with every MCSP token request. Defaults to None. proxies: Dictionary for mapping request protocol to proxy URL. proxies.http (optional): The proxy endpoint to use for HTTP requests. proxies.https (optional): The proxy endpoint to use for HTTPS requests. Attributes: token_manager (MCSPTokenManager): Retrieves and manages MCSP tokens from the endpoint specified by the url. Raises: TypeError: The `disable_ssl_verification` is not a bool. ValueError: The apikey and/or url are not valid for MCSP token exchange requests. """ def __init__( self, apikey: str, url: str, *, disable_ssl_verification: bool = False, headers: Optional[Dict[str, str]] = None, proxies: Optional[Dict[str, str]] = None, ) -> None: # Check the type of `disable_ssl_verification`. Must be a bool. if not isinstance(disable_ssl_verification, bool): raise TypeError('disable_ssl_verification must be a bool') self.token_manager = MCSPTokenManager( apikey=apikey, url=url, disable_ssl_verification=disable_ssl_verification, headers=headers, proxies=proxies, ) self.validate() def authentication_type(self) -> str: """Returns this authenticator's type ('mcsp').""" return Authenticator.AUTHTYPE_MCSP def validate(self) -> None: """Validate apikey and url for token requests. Raises: ValueError: The apikey and/or url are not valid for token requests. """ if self.token_manager.apikey is None: raise ValueError('The apikey shouldn\'t be None.') if self.token_manager.url is None: raise ValueError('The url shouldn\'t be None.') def authenticate(self, req: Request) -> None: """Adds MCSP authentication information to the request. The MCSP bearer token will be added to the request's headers in the form: Authorization: Bearer Args: req: The request to add MCSP authentication information to. Must contain a key to a dictionary called headers. """ headers = req.get('headers') bearer_token = self.token_manager.get_token() headers['Authorization'] = 'Bearer {0}'.format(bearer_token) logger.debug('Authenticated outbound request (type=%s)', self.authentication_type()) def set_disable_ssl_verification(self, status: bool = False) -> None: """Set the flag that indicates whether verification of the server's SSL certificate should be disabled or not. Defaults to False. Args: status: Set to true in order to disable SSL certificate verification. Defaults to False. Raises: TypeError: The `status` is not a bool. """ self.token_manager.set_disable_ssl_verification(status) def set_headers(self, headers: Dict[str, str]) -> None: """Default headers to be sent with every MCSP token request. Args: headers: The headers to be sent with every MCSP token request. """ self.token_manager.set_headers(headers) def set_proxies(self, proxies: Dict[str, str]) -> None: """Sets the proxies the token manager will use to communicate with MCSP on behalf of the host. Args: proxies: Dictionary for mapping request protocol to proxy URL. proxies.http (optional): The proxy endpoint to use for HTTP requests. proxies.https (optional): The proxy endpoint to use for HTTPS requests. """ self.token_manager.set_proxies(proxies) IBM-python-sdk-core-7ebd71d/ibm_cloud_sdk_core/authenticators/mcspv2_authenticator.py000066400000000000000000000267431502256645000312460ustar00rootroot00000000000000# coding: utf-8 # Copyright 2025. IBM All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from typing import Dict, Optional from requests import Request from ibm_cloud_sdk_core.logger import get_logger from .authenticator import Authenticator from ..token_managers.mcspv2_token_manager import MCSPV2TokenManager logger = get_logger() class MCSPV2Authenticator(Authenticator): """The MCSPV2Authenticator invokes the MCSP v2 token-exchange operation (POST /api/2.0/{scopeCollectionType}/{scopeId}/apikeys/token) to obtain an access token for an apikey, and adds the access token to requests via an Authorization header of the form: "Authorization: Bearer ". Keyword Args: apikey: The apikey used to obtain an access token [required]. url: The base endpoint URL for the MCSP token service [required]. scope_collection_type: The scope collection type of item(s) [required]. Valid values are: "accounts", "subscriptions", "services". scope_id: The scope identifier of item(s) [required]. include_builtin_actions: A flag to include builtin actions in the "actions" claim in the MCSP access token (default: False). include_custom_actions: A flag to include custom actions in the "actions" claim in the MCSP access token (default: False). include_roles: A flag to include the "roles" claim in the MCSP access token (default: True). prefix_roles: A flag to add a prefix with the scope level where the role is defined in the "roles" claim (default: False). caller_ext_claim: A map (dictionary) containing keys and values to be injected into the access token as the "callerExt" claim (default: None). The keys used in this map must be enabled in the apikey by setting the "callerExtClaimNames" property when the apikey is created. This property is typically only used in scenarios involving an apikey with identityType `SERVICEID`. disable_ssl_verification: A flag that indicates whether verification of the server's SSL certificate should be disabled or not (default: False). headers: Default headers to be sent with every MCSP token request (default: None). proxies: Dictionary for mapping request protocol to proxy URL (default: None). proxies.http (optional): The proxy endpoint to use for HTTP requests. proxies.https (optional): The proxy endpoint to use for HTTPS requests. Attributes: token_manager (MCSPTokenManager): Retrieves and manages MCSP tokens from the endpoint specified by the url. Raises: TypeError: The `disable_ssl_verification` is not a bool. ValueError: An error occurred while validating the configuration. """ def __init__( self, *, apikey: str, url: str, scope_collection_type: str, scope_id: str, include_builtin_actions: bool = False, include_custom_actions: bool = False, include_roles: bool = True, prefix_roles: bool = False, caller_ext_claim: Optional[Dict[str, str]] = None, disable_ssl_verification: bool = False, headers: Optional[Dict[str, str]] = None, proxies: Optional[Dict[str, str]] = None, ) -> None: self.token_manager = MCSPV2TokenManager( apikey=apikey, url=url, scope_collection_type=scope_collection_type, scope_id=scope_id, include_builtin_actions=include_builtin_actions, include_custom_actions=include_custom_actions, include_roles=include_roles, prefix_roles=prefix_roles, caller_ext_claim=caller_ext_claim, disable_ssl_verification=disable_ssl_verification, headers=headers, proxies=proxies, ) self.validate() def authentication_type(self) -> str: """Returns this authenticator's type ('mcsp').""" return Authenticator.AUTHTYPE_MCSPV2 def validate(self) -> None: """Validate the configuration. Raises: ValueError: The <...> property shouldn't be None. """ if not isinstance(self.token_manager.apikey, str): raise TypeError('"apikey" must be a string') if not isinstance(self.token_manager.url, str): raise TypeError('"url" must be a string') if not isinstance(self.token_manager.scope_collection_type, str): raise TypeError('"scope_collection_type" must be a string') if not isinstance(self.token_manager.scope_id, str): raise TypeError('"scope_id" must be a string') if not isinstance(self.token_manager.include_builtin_actions, bool): raise TypeError('"include_builtin_actions" must be a bool') if not isinstance(self.token_manager.include_custom_actions, bool): raise TypeError('"include_custom_actions" must be a bool') if not isinstance(self.token_manager.include_roles, bool): raise TypeError('"include_roles" must be a bool') if not isinstance(self.token_manager.prefix_roles, bool): raise TypeError('"prefix_roles" must be a bool') if not isinstance(self.token_manager.caller_ext_claim, (dict, type(None))): raise TypeError('"caller_ext_claim" must be a dictionary or None') if not isinstance(self.token_manager.disable_ssl_verification, bool): raise TypeError('"disable_ssl_verification" must be a bool') if not isinstance(self.token_manager.headers, (dict, type(None))): raise TypeError('"headers" must be a dictionary or None') if not isinstance(self.token_manager.proxies, (dict, type(None))): raise TypeError('"proxies" must be a dictionary or None') def authenticate(self, req: Request) -> None: """Adds MCSP authentication information to the request. The MCSP bearer token will be added to the request's headers in the form: Authorization: Bearer Args: req: The request to add MCSP authentication information to. Must contain a key to a dictionary called headers. """ headers = req.get('headers') bearer_token = self.token_manager.get_token() headers['Authorization'] = 'Bearer {0}'.format(bearer_token) logger.debug('Authenticated outbound request (type=%s)', self.authentication_type()) def set_scope_collection_type(self, scope_collection_type: str) -> None: """Set the scope_collection_type value. Args: scope_collection_type: the value to set. Raises: TypeError: "scope_collection_type" must be a string. """ if not isinstance(scope_collection_type, str): raise TypeError('"scope_collection_type" must be a string') self.token_manager.scope_collection_type = scope_collection_type def set_scope_id(self, scope_id: str) -> None: """Set the scope_id value. Args: scope_id: the value to set. Raises: TypeError: "scope_id" must be a string. """ if not isinstance(scope_id, str): raise TypeError('"scope_id" must be a string') self.token_manager.scope_id = scope_id def set_include_builtin_actions(self, include_builtin_actions: bool = False) -> None: """Set the include_builtin_actions flag. Args: include_builtin_actions: The value to set (default: False). Raises: TypeError: "include_builtin_actions" must be a bool. """ if not isinstance(include_builtin_actions, bool): raise TypeError('"include_builtin_actions" must be a bool') self.token_manager.include_builtin_actions = include_builtin_actions def set_include_custom_actions(self, include_custom_actions: bool = False) -> None: """Set the include_custom_actions flag. Args: include_custom_actions: The value to set (default: False). Raises: TypeError: "include_custom_actions" must be a bool. """ if not isinstance(include_custom_actions, bool): raise TypeError('"include_custom_actions" must be a bool') self.token_manager.include_custom_actions = include_custom_actions def set_include_roles(self, include_roles: bool = True) -> None: """Set the include_roles flag. Args: include_roles: The value to set (default: True). Raises: TypeError: "include_roles" must be a bool. """ if not isinstance(include_roles, bool): raise TypeError('"include_roles" must be a bool') self.token_manager.include_roles = include_roles def set_prefix_roles(self, prefix_roles: bool = False) -> None: """Set the prefix_roles flag. Args: prefix_roles: The value to set (default: False). Raises: TypeError: "prefix_roles" must be a bool. """ if not isinstance(prefix_roles, bool): raise TypeError('"prefix_roles" must be a bool') self.token_manager.prefix_roles = prefix_roles def set_caller_ext_claim(self, caller_ext_claim: Optional[Dict[str, str]] = None) -> None: """Set the caller_ext_claim value. Args: caller_ext_claim: The value to set (default: False). Raises: TypeError: "caller_ext_claim" must be a dictionary or None. """ if not isinstance(caller_ext_claim, (dict, type(None))): raise TypeError('"caller_ext_claim" must be a dictionary or None') self.token_manager.caller_ext_claim = caller_ext_claim def set_disable_ssl_verification(self, disable_ssl_verification: bool = False) -> None: """Set the disable_ssl_verification flag. Args: disable_ssl_verification: The value to set (default: False). Raises: TypeError: "disable_ssl_verification" must be a bool. """ if not isinstance(disable_ssl_verification, bool): raise TypeError('"disable_ssl_verification" must be a bool') self.token_manager.disable_ssl_verification = disable_ssl_verification def set_headers(self, headers: Optional[Dict[str, str]] = None) -> None: """Set the headers to be sent with each MCSP token-exchange request. Args: headers: The headers to be sent with each MCSP token request (default: None). """ self.token_manager.set_headers(headers) def set_proxies(self, proxies: Optional[Dict[str, str]] = None) -> None: """Sets the proxies the token manager will use to communicate with MCSP on behalf of the host. Args: proxies: Dictionary for mapping request protocol to proxy URL (default: None). proxies.http (optional): The proxy endpoint to use for HTTP requests. proxies.https (optional): The proxy endpoint to use for HTTPS requests. """ self.token_manager.set_proxies(proxies) IBM-python-sdk-core-7ebd71d/ibm_cloud_sdk_core/authenticators/no_auth_authenticator.py000066400000000000000000000017231502256645000314600ustar00rootroot00000000000000# coding: utf-8 # Copyright 2019 IBM All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from .authenticator import Authenticator class NoAuthAuthenticator(Authenticator): """Performs no authentication.""" def authentication_type(self) -> str: """Returns this authenticator's type ('noauth').""" return Authenticator.AUTHTYPE_NOAUTH def validate(self) -> None: pass def authenticate(self, req) -> None: pass IBM-python-sdk-core-7ebd71d/ibm_cloud_sdk_core/authenticators/vpc_instance_authenticator.py000066400000000000000000000124461502256645000325030ustar00rootroot00000000000000# coding: utf-8 # Copyright 2021, 2024 IBM All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from typing import Optional from requests import Request from ibm_cloud_sdk_core.logger import get_logger from ..token_managers.vpc_instance_token_manager import VPCInstanceTokenManager from .authenticator import Authenticator logger = get_logger() class VPCInstanceAuthenticator(Authenticator): """VPCInstanceAuthenticator implements an authentication scheme in which it retrieves an "instance identity token" and exchanges that for an IAM access token using the VPC Instance Metadata Service API which is available on the local compute resource (VM). The instance identity token is similar to an IAM apikey, except that it is managed automatically by the compute resource provider (VPC). The resulting IAM access token is then added to outbound requests in an Authorization header of the form: Authorization: Bearer Keyword Arguments: iam_profile_crn (str, optional): The CRN of the linked trusted IAM profile to be used as the identity of the compute resource. At most one of iam_profile_crn or iam_profile_id may be specified. If neither one is specified, then the default IAM profile defined for the compute resource will be used. Defaults to None. iam_profile_id (str, optional): The ID of the linked trusted IAM profile to be used when obtaining the IAM access token. At most one of iamProfileCrn or iamProfileId may be specified. If neither one is specified, then the default IAM profile defined for the compute resource will be used. Defaults to None. url (str, optional): The VPC Instance Metadata Service's base endpoint URL. Defaults to 'http://169.254.169.254'. Attributes: iam_profile_crn (str, optional): The CRN of the linked trusted IAM profile. iam_profile_id (str, optional): The ID of the linked trusted IAM profile. url (str, optional): The VPC Instance Metadata Service's base endpoint URL. """ DEFAULT_IMS_ENDPOINT = 'http://169.254.169.254' def __init__( self, iam_profile_crn: Optional[str] = None, iam_profile_id: Optional[str] = None, url: Optional[str] = None ) -> None: if not url: url = self.DEFAULT_IMS_ENDPOINT self.token_manager = VPCInstanceTokenManager( url=url, iam_profile_crn=iam_profile_crn, iam_profile_id=iam_profile_id ) self.validate() def authentication_type(self) -> str: """Returns this authenticator's type ('VPC').""" return Authenticator.AUTHTYPE_VPC def validate(self) -> None: super().validate() if self.token_manager.iam_profile_crn and self.token_manager.iam_profile_id: raise ValueError('At most one of "iam_profile_id" or "iam_profile_crn" may be specified.') def authenticate(self, req: Request) -> None: """Adds IAM authentication information to the request. The IAM access token will be added to the request's headers in the form: Authorization: Bearer Args: req: The request to add IAM authentication information to. Must contain a key to a dictionary called headers. """ headers = req.get('headers') bearer_token = self.token_manager.get_token() headers['Authorization'] = 'Bearer {0}'.format(bearer_token) logger.debug('Authenticated outbound request (type=%s)', self.authentication_type()) def set_iam_profile_crn(self, iam_profile_crn: str) -> None: """Sets CRN of the IAM profile. Args: iam_profile_crn (str): the CRN of the linked trusted IAM profile to be used as the identity of the compute resource. Raises: ValueError: At most one of iam_profile_crn or iam_profile_id may be specified. If neither one is specified, then the default IAM profile defined for the compute resource will be used. """ self.token_manager.set_iam_profile_crn(iam_profile_crn) self.validate() def set_iam_profile_id(self, iam_profile_id: str) -> None: """Sets the ID of the IAM profile. Args: iam_profile_id (str): id of the linked trusted IAM profile to be used when obtaining the IAM access token Raises: ValueError: At most one of iam_profile_crn or iam_profile_id may be specified. If neither one is specified, then the default IAM profile defined for the compute resource will be used. """ self.token_manager.set_iam_profile_id(iam_profile_id) self.validate() IBM-python-sdk-core-7ebd71d/ibm_cloud_sdk_core/base_service.py000066400000000000000000000531011502256645000244630ustar00rootroot00000000000000# coding: utf-8 # Copyright 2019, 2024 IBM All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import gzip import io import logging import json as json_import from http.cookiejar import CookieJar from http import client from os.path import basename from typing import Dict, List, Optional, Tuple, Union from urllib3.util.retry import Retry import requests from requests.structures import CaseInsensitiveDict from requests.exceptions import JSONDecodeError from ibm_cloud_sdk_core.authenticators import Authenticator from .api_exception import ApiException from .detailed_response import DetailedResponse from .http_adapter import SSLHTTPAdapter from .token_managers.token_manager import TokenManager from .utils import ( has_bad_first_or_last_char, is_json_mimetype, remove_null_values, cleanup_values, read_external_sources, strip_extra_slashes, GzipStream, ) from .private_helpers import _build_user_agent from .logger import ( get_logger, LoggingFilter, ) logger = get_logger() # pylint: disable=too-many-instance-attributes # pylint: disable=too-many-locals class BaseService: """Common functionality shared by generated service classes. The base service authenticates requests via its authenticator, stores cookies, and wraps responses from the service endpoint in DetailedResponse or APIException objects. Keyword Arguments: service_url: Url to the service endpoint. Defaults to None. authenticator: Adds authentication data to service requests. Defaults to None. disable_ssl_verification: A flag that indicates whether verification of the server's SSL certificate should be disabled or not. Defaults to False. enable_gzip_compression: A flag that indicates whether to enable gzip compression on request bodies Attributes: service_url (str): Url to the service endpoint. authenticator (Authenticator): Adds authentication data to service requests. disable_ssl_verification (bool): A flag that indicates whether verification of the server's SSL certificate should be disabled or not. default_headers (dict): A dictionary of headers to be sent with every HTTP request to the service endpoint. jar (http.cookiejar.CookieJar): Stores cookies received from the service. http_config (dict): A dictionary containing values that control the timeout, proxies, and etc of HTTP requests. http_client (Session): A configurable session which can use Transport Adapters to configure retries, timeouts, proxies, etc. globally for all requests. enable_gzip_compression (bool): A flag that indicates whether to enable gzip compression on request bodies Raises: ValueError: If Authenticator is not provided or invalid type. """ ERROR_MSG_DISABLE_SSL = ( 'The connection failed because the SSL certificate is not valid. To use a self-signed ' 'certificate, disable verification of the server\'s SSL certificate by invoking the ' 'set_disable_ssl_verification(True) on your service instance and/ or use the ' 'disable_ssl_verification option of the authenticator.' ) def __init__( self, *, service_url: str = None, authenticator: Optional[Authenticator] = None, disable_ssl_verification: bool = False, enable_gzip_compression: bool = False, ) -> None: self.set_service_url(service_url) self.http_client = requests.Session() self.http_config = {} self.jar = CookieJar() self.authenticator = authenticator self.disable_ssl_verification = disable_ssl_verification self.default_headers = None self.enable_gzip_compression = enable_gzip_compression self._set_user_agent_header(_build_user_agent()) self.retry_config = None self.http_adapter = SSLHTTPAdapter(_disable_ssl_verification=self.disable_ssl_verification) if not self.authenticator: raise ValueError('authenticator must be provided') if not isinstance(self.authenticator, Authenticator): raise ValueError('authenticator should be of type Authenticator') self.http_client.mount('http://', self.http_adapter) self.http_client.mount('https://', self.http_adapter) # If debug logging is requested, then trigger HTTP message logging as well. if logger.isEnabledFor(logging.DEBUG): client.HTTPConnection.debuglevel = 1 # Replace the `print` function in the HTTPClient module to # use the debug logger instead of the bare Python print. client.print = lambda *args: logger.debug(LoggingFilter.filter_message(" ".join(args))) def enable_retries(self, max_retries: int = 4, retry_interval: float = 30.0) -> None: """Enable automatic retries on the underlying http client used by the BaseService instance. Args: max_retries: the maximum number of retries to attempt for a failed retryable request retry_interval: the maximum wait time (in seconds) to use for retry attempts. In general, if a response includes the Retry-After header, that will be used for the wait time associated with the retry attempt. If the Retry-After header is not present, then the wait time is based on an exponential backoff policy with a maximum backoff time of "retry_interval". """ self.retry_config = Retry( total=max_retries, backoff_factor=1.0, backoff_max=retry_interval, # List of HTTP status codes to retry on in addition to Timeout/Connection Errors status_forcelist=[429, 500, 502, 503, 504], # List of HTTP methods to retry on # Omitting this will default to all methods except POST allowed_methods=['HEAD', 'GET', 'PUT', 'DELETE', 'OPTIONS', 'TRACE', 'POST'], ) self.http_adapter = SSLHTTPAdapter( max_retries=self.retry_config, _disable_ssl_verification=self.disable_ssl_verification ) self.http_client.mount('http://', self.http_adapter) self.http_client.mount('https://', self.http_adapter) logger.debug('Enabled retries; max_retries=%d, max_retry_interval=%f', max_retries, retry_interval) def disable_retries(self): """Remove retry config from http_adapter""" self.retry_config = None self.http_adapter = SSLHTTPAdapter(_disable_ssl_verification=self.disable_ssl_verification) self.http_client.mount('http://', self.http_adapter) self.http_client.mount('https://', self.http_adapter) logger.debug('Disabled retries') def configure_service(self, service_name: str) -> None: """Look for external configuration of a service. Set service properties. Try to get config from external sources, with the following priority: 1. Credentials file(ibm-credentials.env) 2. Environment variables 3. VCAP Services(Cloud Foundry) Args: service_name: The service name Raises: ValueError: If service_name is not a string. """ if not isinstance(service_name, str): raise ValueError('Service_name must be of type string.') logger.debug('Configuring BaseService instance with service name: %s', service_name) config = read_external_sources(service_name) if config.get('URL'): self.set_service_url(config.get('URL')) if config.get('DISABLE_SSL'): self.set_disable_ssl_verification(config.get('DISABLE_SSL').lower() == 'true') if config.get('ENABLE_GZIP'): self.set_enable_gzip_compression(config.get('ENABLE_GZIP').lower() == 'true') if config.get('ENABLE_RETRIES'): if config.get('ENABLE_RETRIES').lower() == 'true': kwargs = {} if config.get('MAX_RETRIES'): kwargs["max_retries"] = int(config.get('MAX_RETRIES')) if config.get('RETRY_INTERVAL'): kwargs["retry_interval"] = float(config.get('RETRY_INTERVAL')) self.enable_retries(**kwargs) def _set_user_agent_header(self, user_agent_string: str) -> None: self.user_agent_header = {'User-Agent': user_agent_string} logger.debug('Set User-Agent: %s', user_agent_string) def set_http_config(self, http_config: dict) -> None: """Sets the http config dictionary. The dictionary can contain values that control the timeout, proxies, and etc of HTTP requests. Arguments: http_config: Configuration values to customize HTTP behaviors. Raises: TypeError: http_config is not a dict. """ if isinstance(http_config, dict): self.http_config = http_config if ( self.authenticator and hasattr(self.authenticator, 'token_manager') and isinstance(self.authenticator.token_manager, TokenManager) ): self.authenticator.token_manager.http_config = http_config else: raise TypeError("http_config parameter must be a dictionary") def set_disable_ssl_verification(self, status: bool = False) -> None: """Set the flag that indicates whether verification of the server's SSL certificate should be disabled or not. Keyword Arguments: status: set to true to disable ssl verification (default: {False}) """ if self.disable_ssl_verification == status: # Do nothing if the state doesn't change. return self.disable_ssl_verification = status self.http_adapter = SSLHTTPAdapter( max_retries=self.retry_config, _disable_ssl_verification=self.disable_ssl_verification ) self.http_client.mount('http://', self.http_adapter) self.http_client.mount('https://', self.http_adapter) logger.debug('Disabled SSL verification in HTTP client') def set_service_url(self, service_url: str) -> None: """Set the url the service will make HTTP requests too. Arguments: service_url: The WHATWG URL standard origin ex. https://example.service.com Raises: ValueError: Improperly formatted service_url """ if has_bad_first_or_last_char(service_url): raise ValueError( 'The service url shouldn\'t start or end with curly brackets or quotes. ' 'Be sure to remove any {} and \" characters surrounding your service url' ) if service_url is not None: service_url = service_url.rstrip('/') self.service_url = service_url logger.debug('Set service URL: %s', service_url) def get_http_client(self) -> requests.sessions.Session: """Get the http client session currently used by the service. Returns: The http client session currently used by the service. """ return self.http_client def set_http_client(self, http_client: requests.sessions.Session) -> None: """Set current http client session Arguments: http_client: A new requests session client """ if isinstance(http_client, requests.sessions.Session): self.http_client = http_client else: raise TypeError("http_client parameter must be a requests.sessions.Session") def get_authenticator(self) -> Authenticator: """Get the authenticator currently used by the service. Returns: The authenticator currently used by the service. """ return self.authenticator def set_default_headers(self, headers: Dict[str, str]) -> None: """Set http headers to be sent in every request. Arguments: headers: A dictionary of headers """ if isinstance(headers, dict): self.default_headers = headers else: raise TypeError("headers parameter must be a dictionary") def send(self, request: requests.Request, **kwargs) -> DetailedResponse: """Send a request and wrap the response in a DetailedResponse or APIException. Args: request: The request to send to the service endpoint. Raises: ApiException: The exception from the API. Returns: The response from the request. """ # Use a one minute timeout when our caller doesn't give a timeout. # http://docs.python-requests.org/en/master/user/quickstart/#timeouts kwargs = dict({"timeout": 60}, **kwargs) kwargs = dict(kwargs, **self.http_config) if self.disable_ssl_verification: kwargs['verify'] = False # Check to see if the caller specified the 'stream' argument. stream_response = kwargs.get('stream') or False # Remove the keys we set manually, don't let the user overwrite these. reserved_keys = ['method', 'url', 'headers', 'params', 'cookies'] silent_keys = ['headers'] for key in reserved_keys: if key in kwargs: del kwargs[key] if key not in silent_keys: logger.warning('"%s" has been removed from the request', key) try: logger.debug('Sending HTTP request message') response = self.http_client.request(**request, cookies=self.jar, **kwargs) logger.debug('Received HTTP response message, status code %d', response.status_code) # Process a "success" response. if 200 <= response.status_code <= 299: if response.status_code == 204 or request['method'] == 'HEAD': # There is no body content for a HEAD response or a 204 response. result = None elif stream_response: result = response elif not response.text: result = None elif is_json_mimetype(response.headers.get('Content-Type')): # If this is a JSON response, then try to unmarshal it. try: result = response.json(strict=False) except JSONDecodeError as err: raise ApiException( code=response.status_code, http_response=response, message='Error processing the HTTP response', ) from err else: # Non-JSON response, just use response body as-is. result = response return DetailedResponse(response=result, headers=response.headers, status_code=response.status_code) # Received error status code from server, raise an APIException. raise ApiException(response.status_code, http_response=response) except requests.exceptions.SSLError: logger.exception(self.ERROR_MSG_DISABLE_SSL) raise def set_enable_gzip_compression(self, should_enable_compression: bool = False) -> None: """Set value to enable gzip compression on request bodies""" self.enable_gzip_compression = should_enable_compression def get_enable_gzip_compression(self) -> bool: """Get value for enabling gzip compression on request bodies""" return self.enable_gzip_compression def prepare_request( self, method: str, url: str, *, headers: Optional[dict] = None, params: Optional[dict] = None, data: Optional[Union[str, dict]] = None, files: Optional[Union[Dict[str, Tuple[str]], List[Tuple[str, Tuple[str, ...]]]]] = None, **kwargs, ) -> dict: """Build a dict that represents an HTTP service request. Clean up headers, add default http configuration, convert data into json, process files, and merge all into a single request dict. Args: method: The HTTP method of the request ex. GET, POST, etc. url: The origin + pathname according to WHATWG spec. Keyword Arguments: headers: A dictionary containing the headers to be included in the request. Entries with a value of None will be ignored (excluded). params: A dictionary containing the query parameters to be included in the request. Entries with a value of None will be ignored (excluded). data: The request body. Converted to json if a dict. files: 'files' can be a dictionary (i.e { '': ()}), or a list of tuples [ (, ())... ] Returns: Prepared request dictionary. """ # pylint: disable=unused-argument; necessary for kwargs request = {'method': method} # validate the service url is set if not self.service_url: raise ValueError('The service_url is required') # Combine the service_url and operation path to form the request url. # Note: we have already stripped any trailing slashes from the service_url # and we know that the operation path ('url') will start with a slash. request['url'] = strip_extra_slashes(self.service_url + url) headers = remove_null_values(headers) if headers else {} headers = cleanup_values(headers) headers = CaseInsensitiveDict(headers) if self.default_headers is not None: headers.update(self.default_headers) if 'user-agent' not in headers: headers.update(self.user_agent_header) request['headers'] = headers params = remove_null_values(params) params = cleanup_values(params) request['params'] = params if isinstance(data, str): data = data.encode('utf-8') elif isinstance(data, dict) and data: data = remove_null_values(data) if headers.get('content-type') is None: headers.update({'content-type': 'application/json'}) data = json_import.dumps(data).encode('utf-8') request['data'] = data self.authenticator.authenticate(request) # Compress the request body if applicable if self.get_enable_gzip_compression() and 'content-encoding' not in headers and request['data'] is not None: headers['content-encoding'] = 'gzip' request['headers'] = headers # If the provided data is a file-like object, we create `GzipStream` which will handle # the compression on-the-fly when the requests package starts reading its content. # This helps avoid OOM errors when the opened file is too big. # In any other cases, we use the in memory compression directly from # the `gzip` package for backward compatibility. raw_data = request['data'] request['data'] = GzipStream(raw_data) if isinstance(raw_data, io.IOBase) else gzip.compress(raw_data) # Next, we need to process the 'files' argument to try to fill in # any missing filenames where possible. # 'files' can be a dictionary (i.e { '': ()} ) # or a list of tuples [ (, ())... ] # If 'files' is a dictionary we'll convert it to a list of tuples. new_files = [] if files is not None: # If 'files' is a dictionary, transform it into a list of tuples. if isinstance(files, dict): files = remove_null_values(files) files = files.items() # Next, fill in any missing filenames from file tuples. for part_name, file_tuple in files: if file_tuple and len(file_tuple) == 3 and file_tuple[0] is None: file = file_tuple[1] if file and hasattr(file, 'name'): filename = basename(file.name) file_tuple = (filename, file_tuple[1], file_tuple[2]) new_files.append((part_name, file_tuple)) request['files'] = new_files logger.debug('Prepared request [%s %s]', request['method'], request['url']) return request @staticmethod def encode_path_vars(*args: str) -> List[str]: """Encode path variables to be substituted into a URL path. Arguments: args: A list of strings to be URL path encoded Returns: A list of encoded strings that are safe to substitute into a URL path. """ return (requests.utils.quote(x, safe='') for x in args) # The methods below are kept for compatibility and should be removed # in the next major release. # pylint: disable=protected-access @staticmethod def _convert_model(val: str) -> None: if isinstance(val, str): val = json_import.loads(val) if hasattr(val, "_to_dict"): return val._to_dict() return val @staticmethod def _convert_list(val: list) -> None: if isinstance(val, list): return ",".join(val) return val @staticmethod def _encode_path_vars(*args) -> None: return BaseService.encode_path_vars(*args) IBM-python-sdk-core-7ebd71d/ibm_cloud_sdk_core/detailed_response.py000066400000000000000000000060351502256645000255260ustar00rootroot00000000000000# coding: utf-8 # Copyright 2019 IBM All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import json from typing import Dict, Optional, Union import requests class DetailedResponse: """Custom class for detailed response returned from APIs. Keyword Args: response: The response to the service request, defaults to None. headers: The headers of the response, defaults to None. status_code: The status code of the response, defaults to None. Attributes: result (dict, requests.Response, None): The response to the service request. headers (dict): The headers of the response. status_code (int): The status code of the response. """ def __init__( self, *, response: Optional[Union[dict, requests.Response]] = None, headers: Optional[Dict[str, str]] = None, status_code: Optional[int] = None, ) -> None: self.result = response self.headers = headers self.status_code = status_code def get_result(self) -> Optional[Union[dict, requests.Response]]: """Get the response returned by the service request. Returns: The response to the service request. This could be one of the following: 1. a dict that represents an instance of a response model 2. a requests.Response instance if the operation returns a streamed response 3. None if the server returned no response body """ return self.result def get_headers(self) -> Optional[dict]: """The HTTP response headers of the service request. Returns: A dictionary of response headers or None if no headers are present. """ return self.headers def get_status_code(self) -> Union[int, None]: """The HTTP status code of the service request. Returns: The status code associated with the service request. """ return self.status_code def _to_dict(self) -> dict: _dict = {} if hasattr(self, 'result') and self.result is not None: _dict['result'] = self.result if isinstance(self.result, (dict, list)) else 'HTTP response' if hasattr(self, 'headers') and self.headers is not None: _dict['headers'] = self.headers if hasattr(self, 'status_code') and self.status_code is not None: _dict['status_code'] = self.status_code return _dict def __str__(self) -> str: return json.dumps(self._to_dict(), indent=4, default=lambda o: o.__dict__) IBM-python-sdk-core-7ebd71d/ibm_cloud_sdk_core/get_authenticator.py000066400000000000000000000164051502256645000255500ustar00rootroot00000000000000# coding: utf-8 # Copyright 2019, 2025 IBM All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import json from .authenticators import ( Authenticator, BasicAuthenticator, BearerTokenAuthenticator, ContainerAuthenticator, CloudPakForDataAuthenticator, IAMAuthenticator, IAMAssumeAuthenticator, NoAuthAuthenticator, VPCInstanceAuthenticator, MCSPAuthenticator, MCSPV2Authenticator, ) from .utils import read_external_sources, string_to_bool from .logger import get_logger logger = get_logger() def get_authenticator_from_environment(service_name: str) -> Authenticator: """Look for external configuration of authenticator. Try to get authenticator from external sources, with the following priority: 1. Credentials file(ibm-credentials.env) 2. Environment variables 3. VCAP Services(Cloud Foundry) Args: service_name: The service name. Returns: The authenticator found from service information. """ logger.debug('Get authenticator from environment, key=%s', service_name) authenticator = None config = read_external_sources(service_name) if config: authenticator = __construct_authenticator(config) if authenticator is not None: logger.debug('Returning authenticator, type=%s', authenticator.authentication_type()) return authenticator # pylint: disable=too-many-branches # pylint: disable=too-many-statements def __construct_authenticator(config: dict) -> Authenticator: # Determine the authentication type if not specified explicitly. if config.get('AUTH_TYPE'): auth_type = config.get('AUTH_TYPE') elif config.get('AUTHTYPE'): auth_type = config.get('AUTHTYPE') else: # If authtype wasn't specified explicitly, then determine the default. # If the APIKEY property is specified, then it should be IAM, otherwise Container Auth. if config.get('APIKEY'): auth_type = Authenticator.AUTHTYPE_IAM else: auth_type = Authenticator.AUTHTYPE_CONTAINER auth_type = auth_type.lower() authenticator = None if auth_type == Authenticator.AUTHTYPE_BASIC.lower(): authenticator = BasicAuthenticator(username=config.get('USERNAME'), password=config.get('PASSWORD')) elif auth_type == Authenticator.AUTHTYPE_BEARERTOKEN.lower(): authenticator = BearerTokenAuthenticator(bearer_token=config.get('BEARER_TOKEN')) elif auth_type == Authenticator.AUTHTYPE_CONTAINER.lower(): authenticator = ContainerAuthenticator( cr_token_filename=config.get('CR_TOKEN_FILENAME'), iam_profile_name=config.get('IAM_PROFILE_NAME'), iam_profile_id=config.get('IAM_PROFILE_ID'), url=config.get('AUTH_URL'), client_id=config.get('CLIENT_ID'), client_secret=config.get('CLIENT_SECRET'), disable_ssl_verification=string_to_bool(config.get('AUTH_DISABLE_SSL', 'false')), scope=config.get('SCOPE'), ) elif auth_type == Authenticator.AUTHTYPE_CP4D.lower(): authenticator = CloudPakForDataAuthenticator( username=config.get('USERNAME'), password=config.get('PASSWORD'), url=config.get('AUTH_URL'), apikey=config.get('APIKEY'), disable_ssl_verification=string_to_bool(config.get('AUTH_DISABLE_SSL', 'false')), ) elif auth_type == Authenticator.AUTHTYPE_IAM.lower() and config.get('APIKEY'): authenticator = IAMAuthenticator( apikey=config.get('APIKEY'), url=config.get('AUTH_URL'), client_id=config.get('CLIENT_ID'), client_secret=config.get('CLIENT_SECRET'), disable_ssl_verification=string_to_bool(config.get('AUTH_DISABLE_SSL', 'false')), scope=config.get('SCOPE'), ) elif auth_type == Authenticator.AUTHTYPE_IAM_ASSUME.lower(): authenticator = IAMAssumeAuthenticator( apikey=config.get('APIKEY'), iam_profile_id=config.get('IAM_PROFILE_ID'), iam_profile_crn=config.get('IAM_PROFILE_CRN'), iam_profile_name=config.get('IAM_PROFILE_NAME'), iam_account_id=config.get('IAM_ACCOUNT_ID'), url=config.get('AUTH_URL'), client_id=config.get('CLIENT_ID'), client_secret=config.get('CLIENT_SECRET'), disable_ssl_verification=string_to_bool(config.get('AUTH_DISABLE_SSL', 'false')), scope=config.get('SCOPE'), ) elif auth_type == Authenticator.AUTHTYPE_VPC.lower(): authenticator = VPCInstanceAuthenticator( iam_profile_crn=config.get('IAM_PROFILE_CRN'), iam_profile_id=config.get('IAM_PROFILE_ID'), url=config.get('AUTH_URL'), ) elif auth_type == Authenticator.AUTHTYPE_MCSP.lower(): authenticator = MCSPAuthenticator( apikey=config.get('APIKEY'), url=config.get('AUTH_URL'), ) elif auth_type == Authenticator.AUTHTYPE_MCSPV2.lower(): # Required arguments. apikey = config.get('APIKEY') url = config.get('AUTH_URL') scope_collection_type = config.get('SCOPE_COLLECTION_TYPE') scope_id = config.get('SCOPE_ID') # Optional arguments. optional_args = {} str_value = config.get("INCLUDE_BUILTIN_ACTIONS") if str_value is not None: optional_args['include_builtin_actions'] = string_to_bool(str_value) str_value = config.get("INCLUDE_CUSTOM_ACTIONS") if str_value is not None: optional_args['include_custom_actions'] = string_to_bool(str_value) str_value = config.get("INCLUDE_ROLES") if str_value is not None: optional_args['include_roles'] = string_to_bool(str_value) str_value = config.get("PREFIX_ROLES") if str_value is not None: optional_args['prefix_roles'] = string_to_bool(str_value) str_value = config.get("CALLER_EXT_CLAIM") if str_value is not None: try: optional_args['caller_ext_claim'] = json.loads(str_value) except Exception as caused_by: msg = 'An error occurred while unmarshalling the CALLER_EXT_CLAIM configuration property: {0}'.format( str_value ) raise ValueError(msg) from caused_by str_value = config.get("AUTH_DISABLE_SSL") if str_value is not None: optional_args['disable_ssl_verification'] = string_to_bool(str_value) authenticator = MCSPV2Authenticator( apikey=apikey, url=url, scope_collection_type=scope_collection_type, scope_id=scope_id, **optional_args ) elif auth_type == Authenticator.AUTHTYPE_NOAUTH.lower(): authenticator = NoAuthAuthenticator() return authenticator IBM-python-sdk-core-7ebd71d/ibm_cloud_sdk_core/http_adapter.py000066400000000000000000000020631502256645000245110ustar00rootroot00000000000000import ssl from requests import certs from requests.adapters import HTTPAdapter, DEFAULT_POOLBLOCK from urllib3.util.ssl_ import create_urllib3_context class SSLHTTPAdapter(HTTPAdapter): """Wraps the original HTTP adapter and adds additional SSL context.""" def __init__(self, *args, **kwargs): self._disable_ssl_verification = kwargs.pop('_disable_ssl_verification', None) super().__init__(*args, **kwargs) def init_poolmanager(self, connections, maxsize, block=DEFAULT_POOLBLOCK, **pool_kwargs): """Create and use custom SSL configuration.""" ssl_context = create_urllib3_context() # NOTE: https://github.com/psf/requests/pull/6731/files#r1622893724 ssl_context.load_verify_locations(certs.where()) ssl_context.minimum_version = ssl.TLSVersion.TLSv1_2 if self._disable_ssl_verification: ssl_context.check_hostname = False ssl_context.verify_mode = ssl.CERT_NONE super().init_poolmanager(connections, maxsize, block, ssl_context=ssl_context, **pool_kwargs) IBM-python-sdk-core-7ebd71d/ibm_cloud_sdk_core/logger.py000066400000000000000000000052241502256645000233130ustar00rootroot00000000000000# coding: utf-8 # Copyright 2024 IBM All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import logging import re # This is the name of the primary logger used by the library. LOGGER_NAME = 'ibm-cloud-sdk-core' # Keywords that are redacted. REDACTED_KEYWORDS = [ "apikey", "api_key", "passcode", "password", "token", "aadClientId", "aadClientSecret", "auth", "auth_provider_x509_cert_url", "auth_uri", "client_email", "client_id", "client_x509_cert_url", "key", "project_id", "secret", "subscriptionId", "tenantId", "thumbprint", "token_uri", ] class LoggingFilter: """Functions used to filter messages before they are logged.""" redacted_tokens = "|".join(REDACTED_KEYWORDS) auth_header_pattern = re.compile(r"(?m)(Authorization|X-Auth\S*): ((.*?)(\r\n.*)|(.*))") property_settings_pattern = re.compile(r"(?i)(" + redacted_tokens + r")=[^&]*(&|$)") json_field_pattern = re.compile(r'(?i)"([^"]*(' + redacted_tokens + r')[^"_]*)":\s*"[^\,]*"') @classmethod def redact_secrets(cls, text: str) -> str: """Replaces values of potential secret keywords with a placeholder value. Args: text (str): the string to check and process Returns: str: the safe, redacted string with all secrets masked out """ placeholder = "[redacted]" redacted = cls.auth_header_pattern.sub(r"\1: " + placeholder + r"\4", text) redacted = cls.property_settings_pattern.sub(r"\1=" + placeholder + r"\2", redacted) redacted = cls.json_field_pattern.sub(r'"\1":"' + placeholder + r'"', redacted) return redacted @classmethod def filter_message(cls, s: str) -> str: """Filters 's' prior to logging it as a debug message""" # Redact secrets s = LoggingFilter.redact_secrets(s) # Replace CRLF characters with an actual newline to make the message more readable. s = s.replace('\\r\\n', '\n') return s def get_logger() -> logging.Logger: """Returns the primary logger object instance used by the library.""" return logging.getLogger(LOGGER_NAME) IBM-python-sdk-core-7ebd71d/ibm_cloud_sdk_core/private_helpers.py000066400000000000000000000022351502256645000252270ustar00rootroot00000000000000# coding: utf-8 # Copyright 2024 IBM All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # from ibm_cloud_sdk_core.authenticators import Authenticator import platform from .version import __version__ SDK_NAME = 'ibm-python-sdk-core' def _get_system_info() -> str: return 'os.name={0} os.version={1} python.version={2}'.format( platform.system(), platform.release(), platform.python_version() ) def _build_user_agent(component: str = None) -> str: sub_component = "" if component is not None: sub_component = '/{0}'.format(component) return '{0}{1}-{2} {3}'.format(SDK_NAME, sub_component, __version__, _get_system_info()) IBM-python-sdk-core-7ebd71d/ibm_cloud_sdk_core/token_managers/000077500000000000000000000000001502256645000244545ustar00rootroot00000000000000IBM-python-sdk-core-7ebd71d/ibm_cloud_sdk_core/token_managers/__init__.py000066400000000000000000000011361502256645000265660ustar00rootroot00000000000000# coding: utf-8 # Copyright 2021 IBM All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. IBM-python-sdk-core-7ebd71d/ibm_cloud_sdk_core/token_managers/container_token_manager.py000066400000000000000000000232351502256645000317070ustar00rootroot00000000000000# coding: utf-8 # Copyright 2021, 2025 IBM All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from typing import Dict, Optional from ibm_cloud_sdk_core.logger import get_logger from .iam_request_based_token_manager import IAMRequestBasedTokenManager from ..private_helpers import _build_user_agent logger = get_logger() class ContainerTokenManager(IAMRequestBasedTokenManager): """The ContainerTokenManager takes a compute resource token and performs the necessary interactions with the IAM token service to obtain and store a suitable bearer token. Additionally, the ContainerTokenManager will retrieve bearer tokens via basic auth using a supplied client_id and client_secret pair. If the current stored bearer token has expired a new bearer token will be retrieved. Attributes: cr_token_filename(str): The name of the file containing the injected CR token value (applies to IKS-managed compute resources). iam_profile_name (str): The name of the linked trusted IAM profile to be used when obtaining the IAM access token (a CR token might map to multiple IAM profiles). One of iam_profile_name or iam_profile_id must be specified. iam_profile_id (str): The id of the linked trusted IAM profile to be used when obtaining the IAM access token (a CR token might map to multiple IAM profiles). One of iam_profile_name or iam_profile_id must be specified. url (str): The IAM endpoint to token requests. client_id (str): The client_id and client_secret fields are used to form a "basic auth" Authorization header for interactions with the IAM token server. client_secret (str): The client_id and client_secret fields are used to form a "basic auth" Authorization header for interactions with the IAM token server. headers (dict): Default headers to be sent with every IAM token request. proxies (dict): Proxies to use for communicating with IAM. proxies.http (str): The proxy endpoint to use for HTTP requests. proxies.https (str): The proxy endpoint to use for HTTPS requests. http_config (dict): A dictionary containing values that control the timeout, proxies, and etc of HTTP requests. scope (str): The "scope" to use when fetching the bearer token from the IAM token server. This can be used to obtain an access token with a specific scope. Keyword Args: cr_token_filename: The name of the file containing the injected CR token value. Defaults to "/var/run/secrets/tokens/vault-token", or "/var/run/secrets/tokens/sa-token" and "/var/run/secrets/codeengine.cloud.ibm.com/compute-resource-token/token" when not provided. iam_profile_name: The name of the linked trusted IAM profile to be used when obtaining the IAM access token (a CR token might map to multiple IAM profiles). One of iam_profile_name or iam_profile_id must be specified. Defaults to None. iam_profile_id: The id of the linked trusted IAM profile to be used when obtaining the IAM access token (a CR token might map to multiple IAM profiles). One of iam_profile_name or iam_prfoile_id must be specified. Defaults to None. url: The IAM endpoint to token requests. Defaults to None. client_id: The client_id and client_secret fields are used to form a "basic auth" Authorization header for interactions with the IAM token server. Defaults to None. client_secret: The client_id and client_secret fields are used to form a "basic auth" Authorization header for interactions with the IAM token server. Defaults to None. disable_ssl_verification: A flag that indicates whether verification of the server's SSL certificate should be disabled or not. Defaults to False. headers: Default headers to be sent with every IAM token request. Defaults to None. proxies: Proxies to use for communicating with IAM. Defaults to None. proxies.http: The proxy endpoint to use for HTTP requests. proxies.https: The proxy endpoint to use for HTTPS requests. scope: The "scope" to use when fetching the bearer token from the IAM token server. This can be used to obtain an access token with a specific scope. """ DEFAULT_CR_TOKEN_FILENAME1 = '/var/run/secrets/tokens/vault-token' DEFAULT_CR_TOKEN_FILENAME2 = '/var/run/secrets/tokens/sa-token' DEFAULT_CR_TOKEN_FILENAME3 = '/var/run/secrets/codeengine.cloud.ibm.com/compute-resource-token/token' def __init__( self, *, cr_token_filename: Optional[str] = None, iam_profile_name: Optional[str] = None, iam_profile_id: Optional[str] = None, url: Optional[str] = None, client_id: Optional[str] = None, client_secret: Optional[str] = None, disable_ssl_verification: bool = False, scope: Optional[str] = None, proxies: Optional[Dict[str, str]] = None, headers: Optional[Dict[str, str]] = None, ) -> None: super().__init__( url=url, client_id=client_id, client_secret=client_secret, disable_ssl_verification=disable_ssl_verification, headers=headers, proxies=proxies, scope=scope, ) self.cr_token_filename = cr_token_filename self.iam_profile_name = iam_profile_name self.iam_profile_id = iam_profile_id self.request_payload['grant_type'] = 'urn:ibm:params:oauth:grant-type:cr-token' self._set_user_agent(_build_user_agent('container-authenticator')) def retrieve_cr_token(self) -> str: """Retrieves the CR token for the current compute resource by reading it from the local file system. Raises: Exception: Error retrieving the compute resource token. Returns: A string which contains the compute resource token. """ try: cr_token = None if self.cr_token_filename: # If the user specified a filename, then use that. cr_token = self.read_file(self.cr_token_filename) else: # If the user didn't specify a filename, then try our three defaults. try: cr_token = self.read_file(self.DEFAULT_CR_TOKEN_FILENAME1) except: try: cr_token = self.read_file(self.DEFAULT_CR_TOKEN_FILENAME2) except: cr_token = self.read_file(self.DEFAULT_CR_TOKEN_FILENAME3) return cr_token except Exception as ex: # pylint: disable=broad-exception-raised raise Exception('Unable to retrieve the CR token: {}'.format(ex)) from None def request_token(self) -> dict: """Retrieves a CR token value from the current compute resource, then uses that to obtain a new IAM access token from the IAM token server. Returns: A dictionary containing the bearer token to be subsequently used service requests. """ # Set the request payload. self.request_payload['cr_token'] = self.retrieve_cr_token() if self.iam_profile_id: self.request_payload['profile_id'] = self.iam_profile_id if self.iam_profile_name: self.request_payload['profile_name'] = self.iam_profile_name return super().request_token() def set_cr_token_filename(self, cr_token_filename: str) -> None: """Set the location of the compute resource token on the local filesystem. Args: cr_token_filename: path to the compute resource token """ self.cr_token_filename = cr_token_filename def set_iam_profile_name(self, iam_profile_name: str) -> None: """Set the name of the IAM profile. Args: iam_profile_name: name of the linked trusted IAM profile to be used when obtaining the IAM access token """ self.iam_profile_name = iam_profile_name def set_iam_profile_id(self, iam_profile_id: str) -> None: """Set the id of the IAM profile. Args: iam_profile_id: id of the linked trusted IAM profile to be used when obtaining the IAM access token """ self.iam_profile_id = iam_profile_id def read_file(self, filename: str) -> str: """Read in the specified file and return the contents as a string. Args: filename: the name of the file to read Returns: The contents of the file as a string. Raises: Exception: An error occured reading the file. """ try: logger.debug('Attempting to read CR token from file: %s', filename) with open(filename, 'r', encoding='utf-8') as file: cr_token = file.read() logger.debug('Successfully read CR token from file: %s', filename) return cr_token except Exception as ex: # pylint: disable=broad-exception-raised raise Exception('Error reading CR token from file {}: {}'.format(filename, ex)) from None IBM-python-sdk-core-7ebd71d/ibm_cloud_sdk_core/token_managers/cp4d_token_manager.py000066400000000000000000000120531502256645000305530ustar00rootroot00000000000000# coding: utf-8 # Copyright 2019, 2024 IBM All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import json from typing import Dict, Optional from ibm_cloud_sdk_core.logger import get_logger from ..private_helpers import _build_user_agent from .jwt_token_manager import JWTTokenManager logger = get_logger() class CP4DTokenManager(JWTTokenManager): """Token Manager of CloudPak for data. The Token Manager performs basic auth with a username and password to acquire JWT tokens. Keyword Arguments: username: The username for authentication [required]. password: The password for authentication [required if apikey not specified]. url: The endpoint for JWT token requests [required]. apikey: The apikey for authentication [required if password not specified]. disable_ssl_verification: Disable ssl verification. Defaults to False. headers: Headers to be sent with every service token request. Defaults to None. proxies: Proxies to use for making request. Defaults to None. proxies.http (optional): The proxy endpoint to use for HTTP requests. proxies.https (optional): The proxy endpoint to use for HTTPS requests. verify (optional): The path to the certificate to use for HTTPS requests. Attributes: username (str): The username for authentication. password (str): The password for authentication. url (str): The endpoint for JWT token requests. headers (dict): Headers to be sent with every service token request. proxies (dict): Proxies to use for making token requests. proxies.http (str): The proxy endpoint to use for HTTP requests. proxies.https (str): The proxy endpoint to use for HTTPS requests. verify (str): The path to the certificate to use for HTTPS requests. """ TOKEN_NAME = 'token' VALIDATE_AUTH_PATH = '/v1/authorize' def __init__( self, username: str = None, password: str = None, url: str = None, *, apikey: str = None, disable_ssl_verification: bool = False, headers: Optional[Dict[str, str]] = None, proxies: Optional[Dict[str, str]] = None, verify: Optional[str] = None, ) -> None: self.username = username self.password = password self.verify = verify if url and not self.VALIDATE_AUTH_PATH in url: url = url + '/v1/authorize' self.apikey = apikey self.headers = headers if self.headers is None: self.headers = {} self.headers['Content-Type'] = 'application/json' self.proxies = proxies super().__init__(url, disable_ssl_verification=disable_ssl_verification, token_name=self.TOKEN_NAME) self._set_user_agent(_build_user_agent('cp4d-authenticator')) def request_token(self) -> dict: """Makes a request for a token.""" required_headers = { 'User-Agent': self.user_agent, } request_headers = {} if self.headers is not None and isinstance(self.headers, dict): request_headers.update(self.headers) request_headers.update(required_headers) logger.debug('Invoking CP4D token service operation: %s', self.url) response = self._request( method='POST', headers=request_headers, url=self.url, data=json.dumps({"username": self.username, "password": self.password, "api_key": self.apikey}), proxies=self.proxies, verify=self.verify, ) logger.debug('Returned from CP4D token service operation') return response def set_headers(self, headers: Dict[str, str]) -> None: """Headers to be sent with every CP4D token request. Args: headers: The headers to be sent with every CP4D token request. """ if isinstance(headers, dict): self.headers = headers else: raise TypeError('headers must be a dictionary') def set_proxies(self, proxies: Dict[str, str]) -> None: """Sets the proxies the token manager will use to communicate with CP4D on behalf of the host. Args: proxies: Proxies to use for making request. Defaults to None. proxies.http (optional): The proxy endpoint to use for HTTP requests. proxies.https (optional): The proxy endpoint to use for HTTPS requests. """ if isinstance(proxies, dict): self.proxies = proxies else: raise TypeError('proxies must be a dictionary') IBM-python-sdk-core-7ebd71d/ibm_cloud_sdk_core/token_managers/iam_assume_token_manager.py000066400000000000000000000154011502256645000320440ustar00rootroot00000000000000# coding: utf-8 # Copyright 2024 IBM All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from typing import Any, Dict, Optional from ibm_cloud_sdk_core.token_managers.iam_token_manager import IAMTokenManager from .iam_request_based_token_manager import IAMRequestBasedTokenManager from ..private_helpers import _build_user_agent # pylint: disable=too-many-instance-attributes class IAMAssumeTokenManager(IAMRequestBasedTokenManager): """The IAMAssumeTokenManager takes an api key and information about a trusted profile then performs the necessary interactions with the IAM token service to obtain and store a suitable bearer token. This token "assumes" the identity of the provided trusted profile. Attributes: iam_profile_id (str): the ID of the trusted profile iam_profile_crn (str): the CRN of the trusted profile iam_profile_name (str): the name of the trusted profile (must be used together with `iam_account_id`) iam_account_id (str): the ID of the trusted profile (must be used together with `iam_profile_name`) iam_delegate (IAMTokenManager): an IAMTokenManager instance used to obtain the user's IAM access token from the `apikey`. url (str): The IAM endpoint to token requests. headers (dict): Default headers to be sent with every IAM token request. proxies (dict): Proxies to use for communicating with IAM. proxies.http (str): The proxy endpoint to use for HTTP requests. proxies.https (str): The proxy endpoint to use for HTTPS requests. http_config (dict): A dictionary containing values that control the timeout, proxies, and etc of HTTP requests. Args: apikey: A generated APIKey from IBM Cloud. Keyword Args: iam_profile_id: the ID of the trusted profile iam_profile_crn: the CRN of the trusted profile iam_profile_name: the name of the trusted profile (must be used together with `iam_account_id`) iam_account_id: the ID of the trusted profile (must be used together with `iam_profile_name`) url: The IAM endpoint to token requests. Defaults to None. client_id: The client_id and client_secret fields are used to form a "basic auth" Authorization header for interactions with the IAM token server. Defaults to None. client_secret: The client_id and client_secret fields are used to form a "basic auth" Authorization header for interactions with the IAM token server. Defaults to None. disable_ssl_verification: A flag that indicates whether verification of the server's SSL certificate should be disabled or not. Defaults to False. headers: Default headers to be sent with every IAM token request. Defaults to None. proxies: Proxies to use for communicating with IAM. Defaults to None. proxies.http: The proxy endpoint to use for HTTP requests. proxies.https: The proxy endpoint to use for HTTPS requests. scope: The "scope" to use when fetching the bearer token from the IAM token server. This can be used to obtain an access token with a specific scope. """ def __init__( self, apikey: str, *, iam_profile_id: Optional[str] = None, iam_profile_crn: Optional[str] = None, iam_profile_name: Optional[str] = None, iam_account_id: Optional[str] = None, url: Optional[str] = None, client_id: Optional[str] = None, client_secret: Optional[str] = None, disable_ssl_verification: bool = False, headers: Optional[Dict[str, str]] = None, proxies: Optional[Dict[str, str]] = None, scope: Optional[str] = None, ) -> None: super().__init__( url=url, disable_ssl_verification=disable_ssl_verification, headers=headers, proxies=proxies, ) self.iam_profile_id = iam_profile_id self.iam_profile_crn = iam_profile_crn self.iam_profile_name = iam_profile_name self.iam_account_id = iam_account_id # Create an IAMTokenManager instance that will be used to obtain an IAM access token # for the IAM "assume" token exchange. We use the same configuration that's provided # for this class, as they have a lot in common. self.iam_delegate = IAMTokenManager( apikey=apikey, url=url, client_id=client_id, client_secret=client_secret, disable_ssl_verification=disable_ssl_verification, headers=headers, proxies=proxies, scope=scope, ) self.request_payload['grant_type'] = 'urn:ibm:params:oauth:grant-type:assume' self._set_user_agent(_build_user_agent('iam-assume-authenticator')) # Disable all setter methods, inherited from the parent class. def __getattribute__(self, name: str) -> Any: if name.startswith("set_"): raise AttributeError(f"'{self.__class__.__name__}' has no attribute '{name}'") return super().__getattribute__(name) def request_token(self) -> Dict: """Retrieves a standard IAM access token by using the IAM token manager then obtains another access token for the assumed identity. Returns: A dictionary that contains the access token of the assumed IAM identity. """ # Fetch the user's original IAM access token before trying to assume. self.request_payload['access_token'] = self.iam_delegate.get_token() if self.iam_profile_crn: self.request_payload['profile_crn'] = self.iam_profile_crn if self.iam_profile_id: self.request_payload['profile_id'] = self.iam_profile_id else: self.request_payload['profile_name'] = self.iam_profile_name self.request_payload['account'] = self.iam_account_id # Make sure that the unsupported attributes will never be included in the requests. self.client_id = None self.client_secret = None self.scope = None return super().request_token() def _save_token_info(self, token_response: Dict) -> None: super()._save_token_info(token_response) # Set refresh token to None unconditionally. self.refresh_token = None IBM-python-sdk-core-7ebd71d/ibm_cloud_sdk_core/token_managers/iam_request_based_token_manager.py000066400000000000000000000205671502256645000334060ustar00rootroot00000000000000# coding: utf-8 # Copyright 2019, 2024 IBM All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from typing import Dict, Optional from ibm_cloud_sdk_core.logger import get_logger from .jwt_token_manager import JWTTokenManager logger = get_logger() # pylint: disable=too-many-instance-attributes class IAMRequestBasedTokenManager(JWTTokenManager): """The IamRequestBasedTokenManager class contains code relevant to any token manager that interacts with the IAM service to manage a token. It stores information relevant to all IAM requests, such as the client ID and secret, and performs the token request with a set of request options common to any IAM token management scheme. If the current stored bearer token has expired a new bearer token will be retrieved. Attributes: request_payload(dict): the data that will be sent in the IAM OAuth token request url (str): The IAM endpoint to token requests. client_id (str): The client_id and client_secret fields are used to form a "basic auth" Authorization header for interactions with the IAM token server. client_secret (str): The client_id and client_secret fields are used to form a "basic auth" Authorization header for interactions with the IAM token server. headers (dict): Default headers to be sent with every IAM token request. proxies (dict): Proxies to use for communicating with IAM. proxies.http (str): The proxy endpoint to use for HTTP requests. proxies.https (str): The proxy endpoint to use for HTTPS requests. http_config (dict): A dictionary containing values that control the timeout, proxies, and etc of HTTP requests. scope (str): The "scope" to use when fetching the bearer token from the IAM token server. This can be used to obtain an access token with a specific scope. Keyword Args: url: The IAM endpoint to token requests. Defaults to None. client_id: The client_id and client_secret fields are used to form a "basic auth" Authorization header for interactions with the IAM token server. Defaults to None. client_secret: The client_id and client_secret fields are used to form a "basic auth" Authorization header for interactions with the IAM token server. Defaults to None. disable_ssl_verification: A flag that indicates whether verification of the server's SSL certificate should be disabled or not. Defaults to False. headers: Default headers to be sent with every IAM token request. Defaults to None. proxies: Proxies to use for communicating with IAM. Defaults to None. proxies.http: The proxy endpoint to use for HTTP requests. proxies.https: The proxy endpoint to use for HTTPS requests. scope: The "scope" to use when fetching the bearer token from the IAM token server. This can be used to obtain an access token with a specific scope. """ DEFAULT_IAM_URL = 'https://iam.cloud.ibm.com' OPERATION_PATH = "/identity/token" IAM_EXPIRATION_WINDOW = 10 def __init__( self, *, url: Optional[str] = None, client_id: Optional[str] = None, client_secret: Optional[str] = None, disable_ssl_verification: bool = False, headers: Optional[Dict[str, str]] = None, proxies: Optional[Dict[str, str]] = None, scope: Optional[str] = None, ) -> None: if not url: url = self.DEFAULT_IAM_URL if url.endswith(self.OPERATION_PATH): url = url[: -len(self.OPERATION_PATH)] self.url = url self.client_id = client_id self.client_secret = client_secret self.headers = headers self.refresh_token = None self.proxies = proxies self.scope = scope self.request_payload = {} super().__init__(self.url, disable_ssl_verification=disable_ssl_verification, token_name='access_token') def request_token(self) -> dict: """Request an IAM OAuth token given an API Key. If client_id and client_secret are specified use their values as a user and pass auth set according to WHATWG url spec. Returns: A dictionary containing the bearer token to be subsequently used service requests. """ required_headers = { 'Content-Type': 'application/x-www-form-urlencoded', 'Accept': 'application/json', 'User-Agent': self._get_user_agent(), } request_headers = {} if self.headers is not None and isinstance(self.headers, dict): request_headers.update(self.headers) request_headers.update(required_headers) data = dict(self.request_payload) if self.scope is not None and self.scope: data['scope'] = self.scope auth_tuple = None # If both the client_id and secret were specified by the user, then use them if self.client_id and self.client_secret: auth_tuple = (self.client_id, self.client_secret) request_url = (self.url + self.OPERATION_PATH) if self.url else self.url logger.debug('Invoking IAM get_token operation: %s', request_url) response = self._request( method='POST', url=request_url, headers=request_headers, data=data, auth_tuple=auth_tuple, proxies=self.proxies, ) logger.debug('Returned from IAM get_token operation') return response def set_client_id_and_secret(self, client_id: str, client_secret: str) -> None: """Set the client_id and client_secret. Args: client_id: The client id to be used for token requests. client_secret: The client secret to be used for token requests. """ self.client_id = client_id self.client_secret = client_secret def set_headers(self, headers: Dict[str, str]) -> None: """Headers to be sent with every CP4D token request. Args: headers: Headers to be sent with every IAM token request. """ if isinstance(headers, dict): self.headers = headers else: raise TypeError('headers must be a dictionary') def _save_token_info(self, token_response: dict) -> None: super()._save_token_info(token_response) self.refresh_token = token_response.get("refresh_token") def set_proxies(self, proxies: Dict[str, str]) -> None: """Sets the proxies the token manager will use to communicate with IAM on behalf of the host. Args: proxies: Proxies to use for communicating with IAM. proxies.http (str, optional): The proxy endpoint to use for HTTP requests. proxies.https (str, optional): The proxy endpoint to use for HTTPS requests. """ if isinstance(proxies, dict): self.proxies = proxies else: raise TypeError('proxies must be a dictionary') def set_scope(self, value: str) -> None: """Sets the "scope" parameter to use when fetching the bearer token from the IAM token server. Args: value: A space seperated string that makes up the scope parameter. """ self.scope = value def _is_token_expired(self) -> bool: """ Returns true iff the current cached token is expired. We'll consider an access token as expired when we reach its IAM server-reported expiration time minus our expiration window (10 secs). We do this to avoid using an access token that might expire in the middle of a long-running transaction within an IBM Cloud service. Returns ------- bool True if token is expired; False otherwise """ current_time = self._get_current_time() return current_time >= (self.expire_time - self.IAM_EXPIRATION_WINDOW) IBM-python-sdk-core-7ebd71d/ibm_cloud_sdk_core/token_managers/iam_token_manager.py000066400000000000000000000104061502256645000304670ustar00rootroot00000000000000# coding: utf-8 # Copyright 2019, 2024 IBM All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from typing import Dict, Optional from .iam_request_based_token_manager import IAMRequestBasedTokenManager from ..private_helpers import _build_user_agent class IAMTokenManager(IAMRequestBasedTokenManager): """The IAMTokenManager takes an api key and performs the necessary interactions with the IAM token service to obtain and store a suitable bearer token. Additionally, the IAMTokenManager If the current stored bearer token has expired a new bearer token will be retrieved. Attributes: apikey: A generated API key from ibmcloud. url (str): The IAM endpoint to token requests. client_id (str): The client_id and client_secret fields are used to form a "basic auth" Authorization header for interactions with the IAM token server. client_secret (str): The client_id and client_secret fields are used to form a "basic auth" Authorization header for interactions with the IAM token server. headers (dict): Default headers to be sent with every IAM token request. proxies (dict): Proxies to use for communicating with IAM. proxies.http (str): The proxy endpoint to use for HTTP requests. proxies.https (str): The proxy endpoint to use for HTTPS requests. http_config (dict): A dictionary containing values that control the timeout, proxies, and etc of HTTP requests. scope (str): The "scope" to use when fetching the bearer token from the IAM token server. This can be used to obtain an access token with a specific scope. Args: apikey: A generated APIKey from ibmcloud. Keyword Args: url: The IAM endpoint to token requests. Defaults to None. client_id: The client_id and client_secret fields are used to form a "basic auth" Authorization header for interactions with the IAM token server. Defaults to None. client_secret: The client_id and client_secret fields are used to form a "basic auth" Authorization header for interactions with the IAM token server. Defaults to None. disable_ssl_verification: A flag that indicates whether verification of the server's SSL certificate should be disabled or not. Defaults to False. headers: Default headers to be sent with every IAM token request. Defaults to None. proxies: Proxies to use for communicating with IAM. Defaults to None. proxies.http: The proxy endpoint to use for HTTP requests. proxies.https: The proxy endpoint to use for HTTPS requests. scope: The "scope" to use when fetching the bearer token from the IAM token server. This can be used to obtain an access token with a specific scope. """ def __init__( self, apikey: str, *, url: Optional[str] = None, client_id: Optional[str] = None, client_secret: Optional[str] = None, disable_ssl_verification: bool = False, headers: Optional[Dict[str, str]] = None, proxies: Optional[Dict[str, str]] = None, scope: Optional[str] = None, ) -> None: super().__init__( url=url, client_id=client_id, client_secret=client_secret, disable_ssl_verification=disable_ssl_verification, headers=headers, proxies=proxies, scope=scope, ) self.apikey = apikey # Set API key related data. self.request_payload['grant_type'] = 'urn:ibm:params:oauth:grant-type:apikey' self.request_payload['apikey'] = self.apikey self.request_payload['response_type'] = 'cloud_iam' self._set_user_agent(_build_user_agent('iam-authenticator')) IBM-python-sdk-core-7ebd71d/ibm_cloud_sdk_core/token_managers/jwt_token_manager.py000066400000000000000000000073111502256645000305260ustar00rootroot00000000000000# coding: utf-8 # Copyright 2019, 2020 IBM All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from abc import ABC from typing import Optional import jwt import requests from .token_manager import TokenManager from ..api_exception import ApiException class JWTTokenManager(TokenManager, ABC): """An abstract class to contain functionality for parsing, storing, and requesting JWT tokens. get_token will retrieve a new token from the url in case the that there is no existing token, or the previous token has expired. Child classes will implement request_token, which will do the actual acquisition of a new token. Args: url: The url to request tokens from. Keyword Args: disable_ssl_verification: A flag that indicates whether verification of the server's SSL certificate should be disabled or not. Defaults to False. token_name: The key that maps to the token in the dictionary returned from request_token. Defaults to None. Attributes: url (str): The url to request tokens from. disable_ssl_verification (bool): A flag that indicates whether verification of the server's SSL certificate should be disabled or not. token_name (str): The key used of the token in the dict returned from request_token. token_info (dict): The most token_response from request_token. """ def __init__(self, url: str, *, disable_ssl_verification: bool = False, token_name: Optional[str] = None): super().__init__(url, disable_ssl_verification=disable_ssl_verification) self.token_name = token_name self.token_info = {} def _save_token_info(self, token_response: dict) -> None: """ Decode the access token and save the response from the JWT service to the object's state Refresh time is set to approximately 80% of the token's TTL to ensure that the token refresh completes before the current token expires. Parameters ---------- token_response : dict Response from token service """ self.token_info = token_response self.access_token = token_response.get(self.token_name) # The time of expiration is found by decoding the JWT access token decoded_response = jwt.decode(self.access_token, algorithms=["RS256"], options={"verify_signature": False}) # exp is the time of expire and iat is the time of token retrieval exp = decoded_response.get('exp') iat = decoded_response.get('iat') self.expire_time = exp buffer = (exp - iat) * 0.2 self.refresh_time = self.expire_time - buffer def _request(self, method, url, *, headers=None, params=None, data=None, auth_tuple=None, **kwargs) -> dict: kwargs = dict({"timeout": 60}, **kwargs) kwargs = dict(kwargs, **self.http_config) if self.disable_ssl_verification: kwargs['verify'] = False response = requests.request( method=method, url=url, headers=headers, params=params, data=data, auth=auth_tuple, **kwargs ) if 200 <= response.status_code <= 299: return response.json() raise ApiException(response.status_code, http_response=response) IBM-python-sdk-core-7ebd71d/ibm_cloud_sdk_core/token_managers/mcsp_token_manager.py000066400000000000000000000102241502256645000306610ustar00rootroot00000000000000# coding: utf-8 # Copyright 2023, 2024 IBM All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import json from typing import Dict, Optional from ibm_cloud_sdk_core.logger import get_logger from ..private_helpers import _build_user_agent from .jwt_token_manager import JWTTokenManager logger = get_logger() class MCSPTokenManager(JWTTokenManager): """The MCSPTokenManager accepts a user-supplied apikey and performs the necessary interactions with the Multi-Cloud Saas Platform (MCSP) token service to obtain an MCSP access token (a bearer token). When the access token expires, a new access token is obtained from the token server. Keyword Arguments: apikey: The apikey for authentication [required]. url: The endpoint for JWT token requests [required]. disable_ssl_verification: Disable ssl verification. Defaults to False. headers: Headers to be sent with every service token request. Defaults to None. proxies: Proxies to use for making request. Defaults to None. proxies.http (optional): The proxy endpoint to use for HTTP requests. proxies.https (optional): The proxy endpoint to use for HTTPS requests. """ TOKEN_NAME = 'token' OPERATION_PATH = '/siusermgr/api/1.0/apikeys/token' def __init__( self, apikey: str, url: str, *, disable_ssl_verification: bool = False, headers: Optional[Dict[str, str]] = None, proxies: Optional[Dict[str, str]] = None, ) -> None: self.apikey = apikey self.headers = headers if self.headers is None: self.headers = {} self.headers['Content-Type'] = 'application/json' self.headers['Accept'] = 'application/json' self.proxies = proxies super().__init__(url, disable_ssl_verification=disable_ssl_verification, token_name=self.TOKEN_NAME) self._set_user_agent(_build_user_agent('mcsp-authenticator')) def request_token(self) -> dict: """Makes a request for a token.""" required_headers = { 'User-Agent': self.user_agent, } request_headers = {} if self.headers is not None and isinstance(self.headers, dict): request_headers.update(self.headers) request_headers.update(required_headers) request_url = self.url + self.OPERATION_PATH logger.debug('Invoking MCSP token service operation: %s', request_url) response = self._request( method='POST', headers=request_headers, url=request_url, data=json.dumps({"apikey": self.apikey}), proxies=self.proxies, ) logger.debug('Returned from MCSP token service operation') return response def set_headers(self, headers: Dict[str, str]) -> None: """Headers to be sent with every MCSP token request. Args: headers: The headers to be sent with every MCSP token request. """ if isinstance(headers, dict): self.headers = headers else: raise TypeError('headers must be a dictionary') def set_proxies(self, proxies: Dict[str, str]) -> None: """Sets the proxies the token manager will use to communicate with MCSP on behalf of the host. Args: proxies: Proxies to use for making request. Defaults to None. proxies.http (optional): The proxy endpoint to use for HTTP requests. proxies.https (optional): The proxy endpoint to use for HTTPS requests. """ if isinstance(proxies, dict): self.proxies = proxies else: raise TypeError('proxies must be a dictionary') IBM-python-sdk-core-7ebd71d/ibm_cloud_sdk_core/token_managers/mcspv2_token_manager.py000066400000000000000000000175051502256645000311420ustar00rootroot00000000000000# coding: utf-8 # Copyright 2025. IBM All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import json from typing import Dict, List, Optional import requests from ibm_cloud_sdk_core.logger import get_logger from ..private_helpers import _build_user_agent from .jwt_token_manager import JWTTokenManager logger = get_logger() class MCSPV2TokenManager(JWTTokenManager): """The MCSPV2TokenManager invokes the MCSP v2 token-exchange operation (POST /api/2.0/{scopeCollectionType}/{scopeId}/apikeys/token) to obtain an access token for an apikey. When the access token expires, a new access token is obtained from the token server. Keyword Arguments: apikey: The apikey for authentication [required]. url: The endpoint for JWT token requests [required]. scope_collection_type: The scope collection type of item(s) [required]. Valid values are: "accounts", "subscriptions", "services". scope_id: The scope identifier of item(s) [required]. include_builtin_actions: A flag to include builtin actions in the "actions" claim in the MCSP access token (default: False). include_custom_actions: A flag to include custom actions in the "actions" claim in the MCSP access token (default: False). include_roles: A flag to include the "roles" claim in the MCSP access token (default: True). prefix_roles: A flag to add a prefix with the scope level where the role is defined in the "roles" claim (default: False). caller_ext_claim: A dictionary (map) containing keys and values to be injected into the access token as the "callerExt" claim (default: None). The keys used in this map must be enabled in the apikey by setting the "callerExtClaimNames" property when the apikey is created. disable_ssl_verification: Disable ssl verification. Defaults to False. headers: Headers to be sent with every service token request. Defaults to None. proxies: Proxies to use for making request. Defaults to None. proxies.http (optional): The proxy endpoint to use for HTTP requests. proxies.https (optional): The proxy endpoint to use for HTTPS requests. """ # pylint: disable=too-many-instance-attributes # The name of the response body property that contains the access token. TOKEN_NAME = 'token' # The path associated with the token-exchange operation to be invoked. OPERATION_PATH = '/api/2.0/{scopeCollectionType}/{scopeId}/apikeys/token' # These path parameter names must be kept in sync with the operation path above. _path_param_names = ['scopeCollectionType', 'scopeId'] def __init__( self, *, apikey: str, url: str, scope_collection_type: str, scope_id: str, include_builtin_actions: bool = False, include_custom_actions: bool = False, include_roles: bool = True, prefix_roles: bool = False, caller_ext_claim: Optional[Dict[str, str]] = None, disable_ssl_verification: bool = False, headers: Optional[Dict[str, str]] = None, proxies: Optional[Dict[str, str]] = None, ) -> None: self.apikey = apikey self.scope_collection_type = scope_collection_type self.scope_id = scope_id self.include_builtin_actions = include_builtin_actions self.include_custom_actions = include_custom_actions self.include_roles = include_roles self.prefix_roles = prefix_roles self.caller_ext_claim = caller_ext_claim self.set_headers(headers) self.set_proxies(proxies) super().__init__(url, disable_ssl_verification=disable_ssl_verification, token_name=self.TOKEN_NAME) self._set_user_agent(_build_user_agent('mcspv2-authenticator')) def set_headers(self, headers: Optional[Dict[str, str]] = None) -> None: """Headers to be sent with every MCSP token request. Args: headers: The headers to be sent with every MCSP token request (default: None). """ if isinstance(headers, (dict, type(None))): self.headers = headers else: raise TypeError('"headers" must be a dictionary or None') def set_proxies(self, proxies: Optional[Dict[str, str]] = None) -> None: """Sets the proxies the token manager will use to communicate with MCSP on behalf of the host. Args: proxies: Proxies to use for making request (default: None). proxies.http (optional): The proxy endpoint to use for HTTP requests. proxies.https (optional): The proxy endpoint to use for HTTPS requests. """ if isinstance(proxies, (dict, type(None))): self.proxies = proxies else: raise TypeError('"proxies" must be a dictionary or None') def request_token(self) -> dict: """Invokes the "POST /api/2.0/{scopeCollectionType}/{scopeId}/apikeys/token" operation to obtain an access token.""" # These headers take priority over user-supplied headers. required_headers = { 'User-Agent': self.user_agent, 'Content-Type': 'application/json', 'Accept': 'application/json', } # Set up the request headers. request_headers = {} if self.headers is not None and isinstance(self.headers, dict): request_headers.update(self.headers) request_headers.update(required_headers) # Compute the request URL. path_param_values = self._encode_path_vars(self.scope_collection_type, self.scope_id) path_params = dict(zip(self._path_param_names, path_param_values)) request_url = self.url + self.OPERATION_PATH.format(**path_params) # Create the request body (apikey, callerExtClaim properties). request_body = {} request_body['apikey'] = self.apikey if self.caller_ext_claim is not None and isinstance(self.caller_ext_claim, dict): request_body['callerExtClaim'] = self.caller_ext_claim # Set up the query params. query_params = { 'includeBuiltinActions': self.bool_to_string(self.include_builtin_actions), 'includeCustomActions': self.bool_to_string(self.include_custom_actions), 'includeRoles': self.bool_to_string(self.include_roles), 'prefixRolesWithDefinitionScope': self.bool_to_string(self.prefix_roles), } logger.debug('Invoking MCSP v2 token service operation: %s', request_url) response = self._request( method='POST', headers=request_headers, url=request_url, params=query_params, data=json.dumps(request_body), proxies=self.proxies, ) logger.debug('Returned from MCSP v2 token service operation') return response def _encode_path_vars(self, *args: str) -> List[str]: """Encode path variables to be substituted into a URL path. Arguments: args: A list of strings to be URL path encoded Returns: A list of encoded strings that are safe to substitute into a URL path. """ return (requests.utils.quote(x, safe='') for x in args) def bool_to_string(self, value: bool) -> str: """Convert a boolean value to string.""" return 'true' if value else 'false' IBM-python-sdk-core-7ebd71d/ibm_cloud_sdk_core/token_managers/token_manager.py000066400000000000000000000174711502256645000276520ustar00rootroot00000000000000# coding: utf-8 # Copyright 2020, 2024 IBM All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import time from abc import ABC, abstractmethod from threading import Lock import requests from ibm_cloud_sdk_core.logger import get_logger from ..api_exception import ApiException logger = get_logger() # pylint: disable=too-many-instance-attributes class TokenManager(ABC): """An abstract class to contain functionality for parsing, storing, and requesting tokens. get_token will retrieve a new token from the url in case the that there is no existing token, or the previous token has expired. Child classes will implement request_token, which will do the actual acquisition of a new token. Args: url: The url to request tokens from. Keyword Args: disable_ssl_verification: A flag that indicates whether verification of the server's SSL certificate should be disabled or not. Defaults to False. Attributes: url (str): The url to request tokens from. disable_ssl_verification (bool): A flag that indicates whether verification of the server's SSL certificate should be disabled or not. expire_time (int): The time in epoch seconds when the current stored token will expire. refresh_time (int): The time in epoch seconds when the current stored token should be refreshed. request_time (int): The time the last outstanding token request was issued lock (Lock): Lock variable to serialize access to refresh/request times http_config (dict): A dictionary containing values that control the timeout, proxies, and etc of HTTP requests. access_token (str): The latest stored access token user_agent (str): The User-Agent header value to be included in each outbound token request """ def __init__(self, url: str, *, disable_ssl_verification: bool = False): self.url = url self.disable_ssl_verification = disable_ssl_verification self.expire_time = 0 self.refresh_time = 0 self.request_time = 0 self.lock = Lock() self.http_config = {} self.access_token = None self.user_agent = None def get_token(self) -> str: """Get a token to be used for authentication. The source of the token is determined by the following logic: 1. a) If the current token is expired (or never fetched), make a request for one b) If the current token should be refreshed, issue a refresh request 2. After any requests initiated above complete, return the stored token Returns: str: A valid access token """ if self._is_token_expired(): logger.debug('Performing synchronous token fetch') self.paced_request_token() if self._token_needs_refresh(): logger.debug('Performing background asynchronous token fetch') token_response = self.request_token() self._save_token_info(token_response) else: logger.debug('Using cached access token') return self.access_token def set_disable_ssl_verification(self, status: bool = False) -> None: """Sets the ssl verification to enabled or disabled. Args: status: the flag to be used for determining status. Raises: TypeError: The `status` is not a bool. """ if isinstance(status, bool): self.disable_ssl_verification = status else: raise TypeError('status must be a bool') def _set_user_agent(self, user_agent: str = None) -> None: self.user_agent = user_agent def _get_user_agent(self) -> str: return self.user_agent def paced_request_token(self) -> None: """ Paces requests to request_token. This method pseudo-serializes requests for an access_token when the current token is expired (or has never been fetched). The first caller into this method records its `request_time` and then issues the token request. Subsequent callers will check the `request_time` to see if a request is active (has been issued within the past 60 seconds), and if so will sleep for a short time interval (currently 0.5 seconds) before checking again. The check for an active request and update of `request_time` are serailized by the `lock` variable so that only one caller can become the active requester with a 60 second interval. Threads that sleep waiting for the active request to complete will eventually find a newly valid token and return, or 60 seconds will elapse and a new thread will assume the role of the active request. """ while self._is_token_expired(): current_time = self._get_current_time() with self.lock: request_active = self.request_time > (current_time - 60) if not request_active: self.request_time = current_time if not request_active: token_response = self.request_token() self._save_token_info(token_response) self.request_time = 0 return # Sleep for 0.5 seconds before checking token again time.sleep(0.5) @abstractmethod # pragma: no cover def request_token(self) -> None: """Should be overridden by child classes.""" pass @staticmethod def _get_current_time() -> int: return int(time.time()) def _is_token_expired(self) -> bool: """ Check if currently stored token is expired. Returns ------- bool True if token is expired; False otherwise """ current_time = self._get_current_time() return self.expire_time < current_time def _token_needs_refresh(self) -> bool: """ Check if currently stored token needs refresh. Returns ------- bool True if token needs refresh; False otherwise """ current_time = self._get_current_time() with self.lock: needs_refresh = self.refresh_time < current_time if needs_refresh: self.refresh_time = current_time + 60 return needs_refresh @abstractmethod def _save_token_info(self, token_response: dict) -> None: # pragma: no cover """ Decode the access token and save the response from the service to the object's state Refresh time is set to approximately 80% of the token's TTL to ensure that the token refresh completes before the current token expires. Parameters ---------- token_response : dict Response from token service """ pass def _request(self, method, url, *, headers=None, params=None, data=None, auth_tuple=None, **kwargs): kwargs = dict({"timeout": 60}, **kwargs) kwargs = dict(kwargs, **self.http_config) if self.disable_ssl_verification: kwargs['verify'] = False response = requests.request( method=method, url=url, headers=headers, params=params, data=data, auth=auth_tuple, **kwargs ) if 200 <= response.status_code <= 299: return response raise ApiException(response.status_code, http_response=response) IBM-python-sdk-core-7ebd71d/ibm_cloud_sdk_core/token_managers/vpc_instance_token_manager.py000066400000000000000000000155461502256645000324070ustar00rootroot00000000000000# coding: utf-8 # Copyright 2021, 2024 IBM All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import json from typing import Optional from ibm_cloud_sdk_core.logger import get_logger from ..private_helpers import _build_user_agent from .jwt_token_manager import JWTTokenManager logger = get_logger() class VPCInstanceTokenManager(JWTTokenManager): """The VPCInstanceTokenManager retrieves an "instance identity token" and exchanges that for an IAM access token using the VPC Instance Metadata Service API which is available on the local compute resource (VM). The instance identity token is similar to an IAM apikey, except that it is managed automatically by the compute resource provider (VPC). The resulting IAM access token is then added to outbound requests in an Authorization header of the form: Authorization: Bearer Keyword Arguments: iam_profile_crn (str, optional): The CRN of the linked trusted IAM profile to be used as the identity of the compute resource. At most one of iam_profile_crn or iam_profile_id may be specified. If neither one is specified, then the default IAM profile defined for the compute resource will be used. Defaults to None. iam_profile_id (str, optional): The ID of the linked trusted IAM profile to be used when obtaining the IAM access token. At most one of iam_profile_crn or iam_profile_id may be specified. If neither one is specified, then the default IAM profile defined for the compute resource will be used. Defaults to None. url (str, optional): The VPC Instance Metadata Service's base endpoint URL. Defaults to 'http://169.254.169.254'. Attributes: iam_profile_crn (str, optional): The CRN of the linked trusted IAM profile. iam_profile_id (str, optional): The ID of the linked trusted IAM profile. url (str, optional): The VPC Instance Metadata Service's base endpoint URL. """ METADATA_SERVICE_VERSION = '2022-03-01' DEFAULT_IMS_ENDPOINT = 'http://169.254.169.254' TOKEN_NAME = 'access_token' IAM_EXPIRATION_WINDOW = 10 def __init__( self, iam_profile_crn: Optional[str] = None, iam_profile_id: Optional[str] = None, url: Optional[str] = None ) -> None: if not url: url = self.DEFAULT_IMS_ENDPOINT super().__init__(url, token_name=self.TOKEN_NAME) self._set_user_agent(_build_user_agent('vpc-instance-authenticator')) self.iam_profile_crn = iam_profile_crn self.iam_profile_id = iam_profile_id def request_token(self) -> dict: """RequestToken will use the VPC Instance Metadata Service to (1) retrieve a fresh instance identity token and then (2) exchange that for an IAM access token. Returns: A dictionary containing the bearer token to be subsequently used service requests. """ # Retrieve the Instance Identity Token first. instance_identity_token = self.retrieve_instance_identity_token() url = self.url + '/instance_identity/v1/iam_token' request_payload = None if self.iam_profile_crn: request_payload = {'trusted_profile': {'crn': self.iam_profile_crn}} if self.iam_profile_id: request_payload = {'trusted_profile': {'id': self.iam_profile_id}} headers = { 'Content-Type': 'application/json', 'Accept': 'application/json', 'Authorization': 'Bearer ' + instance_identity_token, 'User-Agent': self._get_user_agent(), } logger.debug('Invoking VPC \'create_iam_token\' operation: %s', url) response = self._request( method='POST', url=url, headers=headers, params={'version': self.METADATA_SERVICE_VERSION}, data=json.dumps(request_payload) if request_payload else None, ) logger.debug('Returned from VPC \'create_iam_token\' operation.') return response def set_iam_profile_crn(self, iam_profile_crn: str) -> None: """Sets the CRN of the IAM profile. Args: iam_profile_crn (str): the CRN of the linked trusted IAM profile to be used as the identity of the compute resource. """ self.iam_profile_crn = iam_profile_crn def set_iam_profile_id(self, iam_profile_id: str) -> None: """Sets the ID of the IAM profile. Args: iam_profile_id (str): id of the linked trusted IAM profile to be used when obtaining the IAM access token """ self.iam_profile_id = iam_profile_id def retrieve_instance_identity_token(self) -> str: """Retrieves the local compute resource's instance identity token using the "create_access_token" operation of the local VPC Instance Metadata Service API. Returns: The retrieved instance identity token string. """ url = self.url + '/instance_identity/v1/token' headers = { 'Content-type': 'application/json', 'Accept': 'application/json', 'Metadata-Flavor': 'ibm', 'User-Agent': self._get_user_agent(), } request_body = {'expires_in': 300} logger.debug('Invoking VPC \'create_access_token\' operation: %s', url) response = self._request( method='PUT', url=url, headers=headers, params={'version': self.METADATA_SERVICE_VERSION}, data=json.dumps(request_body), ) logger.debug('Returned from VPC \'create_access_token\' operation.') return response['access_token'] def _is_token_expired(self) -> bool: """ Returns true iff the current cached token is expired. We'll consider an access token as expired when we reach its IAM server-reported expiration time minus our expiration window (10 secs). We do this to avoid using an access token that might expire in the middle of a long-running transaction within an IBM Cloud service. Returns ------- bool True if token is expired; False otherwise """ current_time = self._get_current_time() return current_time >= (self.expire_time - self.IAM_EXPIRATION_WINDOW) IBM-python-sdk-core-7ebd71d/ibm_cloud_sdk_core/utils.py000066400000000000000000000374311502256645000232010ustar00rootroot00000000000000# coding: utf-8 # Copyright 2019, 2025 IBM All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # from ibm_cloud_sdk_core.authenticators import Authenticator import datetime import gzip import io import json as json_import import re from os import getenv, environ, getcwd from os.path import isfile, join, expanduser from typing import List, Union from urllib.parse import urlparse, parse_qs import dateutil.parser as date_parser from .logger import get_logger logger = get_logger() class GzipStream(io.RawIOBase): """Compress files on the fly. GzipStream is a helper class around the gzip library. It helps to compress already opened files (file-like objects) on the fly, so there is no need to read everything into the memory and call the `compress` function on it. The GzipFile is opened on the instance itself so it needs to act as a file-like object. Args: input: the source of the data to be compressed. It can be a file-like object, bytes or string. """ def __init__(self, source: Union[io.IOBase, bytes, str]): self.buffer = b'' if isinstance(source, io.IOBase): # The input is already a file-like object, use it as-is. self.uncompressed = source elif isinstance(source, str): # Strings must be handled with StringIO. self.uncompressed = io.StringIO(source) else: # Handle the rest as raw bytes. self.uncompressed = io.BytesIO(source) self.compressor = gzip.GzipFile(fileobj=self, mode='wb') def read(self, size: int = -1) -> bytes: """Compresses and returns the requested size of data. Args: size: how many bytes to return. -1 to read and compress the whole file """ compressed = b'' if (size < 0) or (len(self.buffer) < size): for raw in self.uncompressed: # We need to encode text like streams (e.g. TextIOWrapper) to bytes. if isinstance(raw, str): raw = raw.encode() self.compressor.write(raw) # Stop compressing if we reached the max allowed size. if 0 < size < len(self.buffer): self.compressor.flush() break else: self.compressor.close() if size < 0: # Return all data from the buffer. compressed = self.buffer self.buffer = b'' else: # If we already have enough data in our buffer # return the desired chunk of bytes compressed = self.buffer[:size] # then remove them from the buffer. self.buffer = self.buffer[size:] return compressed def flush(self) -> None: """Not implemented.""" # Since this "pipe" sits between 2 other stream (source/read -> target/write) # it wouldn't be worth to implemet flushing. pass def write(self, compressed: bytes) -> None: """Append the compressed data to the buffer This happens when the target stream calls the `read` method and that triggers the gzip "compressor". """ self.buffer += compressed def close(self) -> None: """Closes the underlying file-like object.""" self.uncompressed.close() def has_bad_first_or_last_char(val: str) -> bool: """Returns true if a string starts with any of: {," ; or ends with any of: },". Args: val: The string to be tested. Returns: Whether or not the string starts or ends with bad characters. """ return val is not None and (val.startswith('{') or val.startswith('"') or val.endswith('}') or val.endswith('"')) def remove_null_values(dictionary: dict) -> dict: """Create a new dictionary without keys mapped to null values. Args: dictionary: The dictionary potentially containing keys mapped to values of None. Returns: A dict with no keys mapped to None. """ if isinstance(dictionary, dict): return {k: v for (k, v) in dictionary.items() if v is not None} return dictionary def cleanup_values(dictionary: dict) -> dict: """Create a new dictionary with boolean values converted to strings. Ex. true -> 'true', false -> 'false'. { 'key': true } -> { 'key': 'true' } Args: dictionary: The dictionary with keys mapped to booleans. Returns: The dictionary with certain keys mapped to s and not booleans. """ if isinstance(dictionary, dict): return {k: cleanup_value(v) for (k, v) in dictionary.items()} return dictionary def cleanup_value(value: any) -> any: """Convert a boolean value to string.""" if isinstance(value, bool): return 'true' if value else 'false' return value def strip_extra_slashes(value: str) -> str: """Combine multiple trailing slashes to a single slash""" if value.endswith('//'): return value.rstrip('/') + '/' return value def datetime_to_string(val: datetime.datetime) -> str: """Convert a datetime object to string. If the supplied datetime does not specify a timezone, it is assumed to be UTC. Args: val: The datetime object. Returns: datetime serialized to iso8601 format. """ if isinstance(val, datetime.datetime): if val.tzinfo is None: return val.isoformat() + 'Z' val = val.astimezone(datetime.timezone.utc) return val.isoformat().replace('+00:00', 'Z') return val def string_to_datetime(string: str) -> datetime.datetime: """De-serializes string to datetime. Args: string: string containing datetime in iso8601 format. Returns: the de-serialized string as a datetime object. """ val = date_parser.parse(string) if val.tzinfo is not None: return val return val.replace(tzinfo=datetime.timezone.utc) def string_to_datetime_list(string_list: List[str]) -> List[datetime.datetime]: """De-serializes each string in a list to a datetime. Args: string_list: list of strings containing datetime in iso8601 format. Returns: the de-serialized list of strings as a list of datetime objects. """ if not isinstance(string_list, list): raise ValueError( "Invalid argument type: " + str(type(string_list)) + ". Argument string_list must be of type List[str]" ) datetime_list = [] for string_val in string_list: datetime_list.append(string_to_datetime(string_val)) return datetime_list def datetime_to_string_list(datetime_list: List[datetime.datetime]) -> List[str]: """Convert a list of datetime objects to a list of strings. Args: datetime_list: The list of datetime objects. Returns: list of datetimes serialized as strings in iso8601 format. """ if not isinstance(datetime_list, list): raise ValueError( "Invalid argument type: " + str(type(datetime_list)) + ". Argument datetime_list must be of type List[datetime.datetime]" ) string_list = [] for datetime_val in datetime_list: string_list.append(datetime_to_string(datetime_val)) return string_list def date_to_string(val: datetime.date) -> str: """Convert a date object to string. Args: val: The date object. Returns: date serialized to `YYYY-MM-DD` format. """ if isinstance(val, datetime.date): return str(val) return val def string_to_date(string: str) -> datetime.date: """De-serializes string to date. Args: string: string containing date in 'YYYY-MM-DD' format. Returns: the de-serialized string as a date object. """ return date_parser.parse(string).date() def string_to_bool(string: str) -> bool: """Converts 'string' to a bool value. Args: string: the value to convert. This should be some form of "true" or "false" (e.g. "True", "TrUE", "False", "FaLsE", etc.) Returns: the boolean value """ return string.strip().lower() == 'true' def get_query_param(url_str: str, param: str) -> str: """Return a query parameter value from url_str Args: url_str: the URL from which to extract the query parameter value param: the name of the query parameter whose value should be returned Returns: the value of the `param` query parameter as a string Raises: ValueError: if errors are encountered parsing `url_str` """ if not url_str: return None url = urlparse(url_str) if not url.query: return None query = parse_qs(url.query, strict_parsing=True) values = query.get(param) return values[0] if values else None def convert_model(val: any) -> dict: """Convert a model object into an equivalent dict. Arguments: val: A dict or a model object Returns: A dict representation of the input object. """ if isinstance(val, dict): return val if hasattr(val, "to_dict"): return val.to_dict() # Consider raising a ValueError here in the next major release return val def convert_list(val: Union[str, List[str]]) -> str: """Convert a list of strings into comma-separated string. Arguments: val: A string or list of strings Returns: A comma-separated string of the items in the input list. """ if isinstance(val, str): return val if isinstance(val, list) and all(isinstance(x, str) for x in val): return ",".join(val) # Consider raising a ValueError here in the next major release return val def read_external_sources(service_name: str) -> dict: """Look for external configuration of a service. Try to get config from external sources, with the following priority: 1. Credentials file(ibm-credentials.env) 2. Environment variables 3. VCAP Services(Cloud Foundry) Args: service_name: The service name Returns: A dictionary containing relevant configuration for the service if found. """ logger.debug('Retrieving config properties for service \'%s\'', service_name) config = {} config = __read_from_credential_file(service_name) if not config: config = __read_from_env_variables(service_name) if not config: config = __read_from_vcap_services(service_name) logger.debug('Retrieved %d properties', len(config)) return config def __read_from_env_variables(service_name: str) -> dict: """Return a config object based on environment variables for a service. Args: service_name: The name of the service to look for in env variables. Returns: A set of service configuration key-value pairs. """ config = {} for key, value in environ.items(): _parse_key_and_update_config(config, service_name, key, value) return config def __read_from_credential_file(service_name: str, *, separator: str = '=') -> dict: """Return a config object based on credentials file for a service. Args: service_name: The name of the service to look for in env variables. Keyword Args: separator: The character to split on to de-serialize a line into a key-value pair. Returns: A set of service configuration key-value pairs. """ default_credentials_file_name = 'ibm-credentials.env' # 1. ${IBM_CREDENTIALS_FILE} credential_file_path = getenv('IBM_CREDENTIALS_FILE') # 2. /ibm-credentials.env if credential_file_path is None: file_path = join(getcwd(), default_credentials_file_name) if isfile(file_path): credential_file_path = file_path # 3. /ibm-credentials.env if credential_file_path is None: file_path = join(expanduser('~'), default_credentials_file_name) if isfile(file_path): credential_file_path = file_path config = {} if credential_file_path is not None: try: with open(credential_file_path, 'r', encoding='utf-8') as fobj: for line in fobj: key_val = line.strip().split(separator, 1) if len(key_val) == 2: key = key_val[0] value = key_val[1] _parse_key_and_update_config(config, service_name, key, value) except OSError: # just absorb the exception and make sure we return an empty response config = {} return config def _parse_key_and_update_config(config: dict, service_name: str, key: str, value: str) -> None: service_name = service_name.replace(' ', '_').replace('-', '_').upper() if key.startswith(service_name): config[key[len(service_name) + 1 :]] = value def __read_from_vcap_services(service_name: str) -> dict: """Return a config object based on the vcap services environment variable. Args: service_name: The name of the service to look for in the vcap. Returns: A set of service configuration key-value pairs. """ vcap_services = getenv('VCAP_SERVICES') vcap_service_credentials = {} if vcap_services: services = json_import.loads(vcap_services) for key in services.keys(): for i in range(len(services[key])): if vcap_service_credentials and isinstance(vcap_service_credentials, dict): break if services[key][i].get('name') == service_name: vcap_service_credentials = services[key][i].get('credentials', {}) if not vcap_service_credentials: if service_name in services.keys(): service = services.get(service_name) if service: vcap_service_credentials = service[0].get('credentials', {}) if vcap_service_credentials and isinstance(vcap_service_credentials, dict): new_vcap_creds = {} # cf if vcap_service_credentials.get('username') and vcap_service_credentials.get('password'): new_vcap_creds['AUTH_TYPE'] = 'basic' new_vcap_creds['USERNAME'] = vcap_service_credentials.get('username') new_vcap_creds['PASSWORD'] = vcap_service_credentials.get('password') vcap_service_credentials = new_vcap_creds elif vcap_service_credentials.get('iam_apikey'): new_vcap_creds['AUTH_TYPE'] = 'iam' new_vcap_creds['APIKEY'] = vcap_service_credentials.get('iam_apikey') vcap_service_credentials = new_vcap_creds elif vcap_service_credentials.get('apikey'): new_vcap_creds['AUTH_TYPE'] = 'iam' new_vcap_creds['APIKEY'] = vcap_service_credentials.get('apikey') vcap_service_credentials = new_vcap_creds return vcap_service_credentials # A regex that matches an "application/json" mimetype. json_mimetype_pattern = re.compile('^application/json(\\s*;.*)?$') def is_json_mimetype(mimetype: str) -> bool: """Returns true if 'mimetype' is a JSON-like mimetype, false otherwise. Args: mimetype: The mimetype to check. Returns: true if mimetype is a JSON-line mimetype, false otherwise. """ return mimetype is not None and json_mimetype_pattern.match(mimetype) is not None IBM-python-sdk-core-7ebd71d/ibm_cloud_sdk_core/version.py000066400000000000000000000000271502256645000235150ustar00rootroot00000000000000__version__ = '3.24.2' IBM-python-sdk-core-7ebd71d/package.json000066400000000000000000000007461502256645000201460ustar00rootroot00000000000000{ "name": "semantic-release-dependencies", "version": "0.0.0", "description": "This package.json is being used to manage semantic-release and its dependencies", "license": "Apache-2.0", "devDependencies": { "semantic-release": "21.0.7", "@semantic-release/changelog": "6.0.3", "@semantic-release/exec": "6.0.3", "@semantic-release/git": "10.0.1" }, "overrides": { "semver": "^7.5.3" }, "scripts": { "semantic-release": "semantic-release" } } IBM-python-sdk-core-7ebd71d/pyproject.toml000066400000000000000000000041321502256645000205650ustar00rootroot00000000000000[project] name = "ibm-cloud-sdk-core" version = "3.24.2" authors = [ { name="IBM", email="devxsdk@us.ibm.com" } ] description = "Core library used by SDKs for IBM Cloud Services" readme = "README.md" requires-python = ">=3.9" classifiers = [ "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Development Status :: 5 - Production/Stable", "Environment :: Console", "Intended Audience :: Developers", "License :: OSI Approved :: Apache Software License", "Operating System :: OS Independent", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Libraries :: Python Modules", "Topic :: Software Development :: Libraries :: Application Frameworks", ] keywords=["ibm", "cloud", "ibm cloud services"] dependencies = [ "requests>=2.32.4,<3.0.0", "urllib3>=2.4.0,<3.0.0", "python_dateutil>=2.9.0,<3.0.0", "PyJWT>=2.10.1,<3.0.0", ] [project.urls] Repository = "https://github.com/IBM/python-sdk-core" Documentation = "https://github.com/IBM/python-sdk-core/blob/main/README.md" Issues = "https://github.com/IBM/python-sdk-core/issues" Changelog = "https://github.com/IBM/python-sdk-core/blob/main/CHANGELOG.md" Contributing = "https://github.com/IBM/python-sdk-core/blob/main/CONTRIBUTING.md" License = "https://github.com/IBM/python-sdk-core/blob/main/LICENSE" [project.optional-dependencies] dev = [ "coverage>=7.9.0,<8.0.0", "pylint>=3.3.7,<4.0.0", "pytest>=7.4.4,<8.0.0", "pytest-cov>=4.1.0,<5.0.0", "responses>=0.25.7,<1.0.0", "black>=25.0.0,<26.0.0", ] publish = [ "build", "twine" ] [build-system] requires = ["setuptools>=67.7.2"] build-backend = "setuptools.build_meta" [tool.setuptools] packages = ["ibm_cloud_sdk_core", "ibm_cloud_sdk_core.authenticators", "ibm_cloud_sdk_core.token_managers"] [tool.black] line-length = 120 skip-string-normalization = true IBM-python-sdk-core-7ebd71d/resources/000077500000000000000000000000001502256645000176635ustar00rootroot00000000000000IBM-python-sdk-core-7ebd71d/resources/cr-token.txt000066400000000000000000000000121502256645000221370ustar00rootroot00000000000000cr-token-1IBM-python-sdk-core-7ebd71d/resources/ibm-credentials-basic.env000066400000000000000000000001151502256645000245130ustar00rootroot00000000000000WATSON_USERNAME=my_username WATSON_PASSWORD=my_password WATSON_AUTHTYPE=basicIBM-python-sdk-core-7ebd71d/resources/ibm-credentials-bearer.env000066400000000000000000000000711502256645000246730ustar00rootroot00000000000000WATSON_BEARER_TOKEN=my_token WATSON_AUTHTYPE=bearerToken IBM-python-sdk-core-7ebd71d/resources/ibm-credentials-container.env000066400000000000000000000006451502256645000254240ustar00rootroot00000000000000SERVICE_1_AUTH_TYPE=container SERVICE_1_CR_TOKEN_FILENAME=crtoken.txt SERVICE_1_IAM_PROFILE_NAME=iam-user-123 SERVICE_1_IAM_PROFILE_ID=iam-id-123 SERVICE_1_AUTH_URL=https://iamhost/iam/api SERVICE_1_SCOPE=scope1 SERVICE_1_CLIENT_ID=iam-client-123 SERVICE_1_CLIENT_SECRET=iam-secret-123 SERVICE_1_AUTH_DISABLE_SSL=true SERVICE_2_AUTH_TYPE=container SERVICE_2_IAM_PROFILE_NAME=iam-user-123 SERVICE_2_AUTH_DISABLE_SSL=falseIBM-python-sdk-core-7ebd71d/resources/ibm-credentials-cp4d.env000066400000000000000000000002431502256645000242660ustar00rootroot00000000000000WATSON_USERNAME=my_username WATSON_PASSWORD=my_password WATSON_AUTH_URL=https://my_url WATSON_AUTH_TYPE=cp4d WATSON_AUTH_DISABLE_SSL=False WATSON_DISABLE_SSL=True IBM-python-sdk-core-7ebd71d/resources/ibm-credentials-cp4dtest.env.example000066400000000000000000000010101502256645000266110ustar00rootroot00000000000000CP4D_PASSWORD_TEST_AUTH_URL= e.g. https://cpd350-cpd-cpd350.apps.wml-kf-cluster.os.fyre.ibm.com/icp4d-api CP4D_PASSWORD_TEST_AUTH_TYPE=cp4d CP4D_PASSWORD_TEST_USERNAME= CP4D_PASSWORD_TEST_PASSWORD= CP4D_PASSWORD_TEST_AUTH_DISABLE_SSL=true CP4D_APIKEY_TEST_AUTH_URL= e.g. https://cpd350-cpd-cpd350.apps.wml-kf-cluster.os.fyre.ibm.com/icp4d-api CP4D_APIKEY_TEST_AUTH_TYPE=cp4d CP4D_APIKEY_TEST_USERNAME= CP4D_APIKEY_TEST_APIKEY= CP4D_APIKEY_TEST_AUTH_DISABLE_SSL=true IBM-python-sdk-core-7ebd71d/resources/ibm-credentials-external.env000066400000000000000000000002671502256645000252640ustar00rootroot00000000000000INCLUDE_EXTERNAL_CONFIG_APIKEY=5678efgh INCLUDE_EXTERNAL_CONFIG_AUTH_TYPE=iam INCLUDE_EXTERNAL_CONFIG_URL=https://externallyconfigured.com/api INCLUDE_EXTERNAL_CONFIG_DISABLE_SSL=TrueIBM-python-sdk-core-7ebd71d/resources/ibm-credentials-gzip.env000066400000000000000000000002411502256645000244030ustar00rootroot00000000000000INCLUDE_EXTERNAL_CONFIG_APIKEY=mockkey INCLUDE_EXTERNAL_CONFIG_AUTH_TYPE=iam INCLUDE_EXTERNAL_CONFIG_URL=https://mockurl INCLUDE_EXTERNAL_CONFIG_ENABLE_GZIP=TrueIBM-python-sdk-core-7ebd71d/resources/ibm-credentials-iam-assume.env000066400000000000000000000002471502256645000255010ustar00rootroot00000000000000SERVICE_1_AUTH_TYPE=iamAssume SERVICE_1_APIKEY=my-api-key SERVICE_1_IAM_PROFILE_ID=iam-profile-1 SERVICE_1_URL=https://iamhost/iamassume/api SERVICE_1_DISABLE_SSL=TrueIBM-python-sdk-core-7ebd71d/resources/ibm-credentials-iam.env000066400000000000000000000002171502256645000242030ustar00rootroot00000000000000IBM_WATSON_APIKEY=5678efgh IBM_WATSON_AUTH_TYPE=iam IBM_WATSON_URL=https://gateway-s.watsonplatform.net/watson/api IBM_WATSON_DISABLE_SSL=FalseIBM-python-sdk-core-7ebd71d/resources/ibm-credentials-mcsp.env000066400000000000000000000001321502256645000243730ustar00rootroot00000000000000SERVICE1_AUTH_TYPE=mcsp SERVICE1_APIKEY=my-api-key SERVICE1_AUTH_URL=https://mcsp.ibm.com IBM-python-sdk-core-7ebd71d/resources/ibm-credentials-mcspv2.env000066400000000000000000000015071502256645000246520ustar00rootroot00000000000000# MCSP v2 with only required properties SERVICE1_AUTH_TYPE=mcspv2 SERVICE1_APIKEY=my-api-key SERVICE1_AUTH_URL=https://mcspv2.ibm.com SERVICE1_SCOPE_COLLECTION_TYPE=accounts SERVICE1_SCOPE_ID=global_account # MCSP v2 with all properties SERVICE2_AUTH_TYPE=mcspv2 SERVICE2_APIKEY=my-api-key SERVICE2_AUTH_URL=https://mcspv2.ibm.com SERVICE2_SCOPE_COLLECTION_TYPE=accounts SERVICE2_SCOPE_ID=global_account SERVICE2_INCLUDE_BUILTIN_ACTIONS=true SERVICE2_INCLUDE_CUSTOM_ACTIONS=true SERVICE2_INCLUDE_ROLES=false SERVICE2_PREFIX_ROLES=true SERVICE2_CALLER_EXT_CLAIM={"productID": "prod-123"} SERVICE2_AUTH_DISABLE_SSL=true # MCSP v with error config ERROR1_AUTH_TYPE=mcspv2 ERROR1_APIKEY=my-api-key ERROR1_AUTH_URL=https://mcspv2.ibm.com ERROR1_SCOPE_COLLECTION_TYPE=accounts ERROR1_SCOPE_ID=global_account ERROR1_CALLER_EXT_CLAIM={not json} IBM-python-sdk-core-7ebd71d/resources/ibm-credentials-no-auth.env000066400000000000000000000000271502256645000250070ustar00rootroot00000000000000WATSON_AUTH_TYPE=noAuthIBM-python-sdk-core-7ebd71d/resources/ibm-credentials-retry.env000066400000000000000000000003661502256645000246070ustar00rootroot00000000000000INCLUDE_EXTERNAL_CONFIG_APIKEY=mockkey INCLUDE_EXTERNAL_CONFIG_AUTH_TYPE=iam INCLUDE_EXTERNAL_CONFIG_URL=https://mockurl INCLUDE_EXTERNAL_CONFIG_MAX_RETRIES=3 INCLUDE_EXTERNAL_CONFIG_RETRY_INTERVAL=25.0 INCLUDE_EXTERNAL_CONFIG_ENABLE_RETRIES=trueIBM-python-sdk-core-7ebd71d/resources/ibm-credentials-vpc.env000066400000000000000000000004301502256645000242220ustar00rootroot00000000000000# VPC auth with default config SERVICE1_AUTH_TYPE=vpc # VPC auth with profile CRN SERVICE2_AUTH_TYPE=vpc SERVICE2_IAM_PROFILE_CRN=crn:iam-profile1 SERVICE2_AUTH_URL=http://vpc.imds.com/api # VPC auth with profile ID SERVICE3_AUTH_TYPE=vpc SERVICE3_IAM_PROFILE_ID=iam-profile1-idIBM-python-sdk-core-7ebd71d/resources/ibm-credentials.env000066400000000000000000000012761502256645000234450ustar00rootroot00000000000000IBM_WATSON_APIKEY=5678efgh IBM_WATSON_AUTH_TYPE=iam IBM_WATSON_URL=https://cwdserviceurl IBM_WATSON_DISABLE_SSL=False # Service1 auth properties configured with IAM and a token containing '=' SERVICE_1_AUTH_TYPE=iam SERVICE_1_APIKEY=V4HXmoUtMjohnsnow=KotN SERVICE_1_CLIENT_ID=somefake========id SERVICE_1_CLIENT_SECRET===my-client-secret== SERVICE_1_AUTH_URL=https://iamhost/iam/api= SERVICE_1_URL=service1.com/api # Service2 configured with IAM w/scope SERVICE_2_AUTH_TYPE=iam SERVICE_2_APIKEY=V4HXmoUtMjohnsnow=KotN SERVICE_2_CLIENT_ID=somefake========id SERVICE_2_CLIENT_SECRET===my-client-secret== SERVICE_2_AUTH_URL=https://iamhost/iam/api= SERVICE_2_URL=service1.com/api SERVICE_2_SCOPE=A B C DIBM-python-sdk-core-7ebd71d/resources/test_ssl.crt000066400000000000000000000021431502256645000222350ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIDETCCAfmgAwIBAgIUVQfATsBxBkHqAgmrv9Eb/KQhA2IwDQYJKoZIhvcNAQEL BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MCAXDTI0MDcwMjA5MTkyN1oYDzIxMjQw NjA4MDkxOTI3WjAUMRIwEAYDVQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEB AQUAA4IBDwAwggEKAoIBAQCx7eyXu0A1IH18U0fmVbHlC0OpzpGfGOQayrlrxyxN kNc8T2ehnt7W33FHI2tjcmO11wgsN+U2+uB2aq0q1OYaqo3OlZtTF4A91CgAdbGd Ix5aEEzjogiQIBvrBQhaU6uFTzUBQ5tWs+pLcorVrp8G/ONN/1e4Z3NCg036ibSs Vkfdw1zX6vTR674uTq8aIG7sH3DCF1Q+CzvxhQrhjkZOha+u0H+OhZ9yd30hU/xy AKZsoGHNY65bVSYAPxP6XLw5inF534TLriggFDonEk16eHjAi7SxcjdKcyxhfX7u DefD/s9cKUY4Tf1JeAx2F1Y++ffqWlQSde5DKkfC6xU5AgMBAAGjWTBXMBQGA1Ud EQQNMAuCCWxvY2FsaG9zdDALBgNVHQ8EBAMCB4AwEwYDVR0lBAwwCgYIKwYBBQUH AwEwHQYDVR0OBBYEFIHsshXxnhcdXRSPxHvTvfMWhfTgMA0GCSqGSIb3DQEBCwUA A4IBAQCnEU3NtmskVv2/3U0DQM8FM8jKc/V7WCfK/PWPFUZgCkj8pUg4yYxjOZ4K pFV7cPJjtArmjyU9lB0g2wQvlpXEYbDJ5K0LK9GsdhowZQatZaTB0nVZeG87mlV7 kxrQTMsMTYf5I6S3SW63SorlJQiuaQjOKwvommCS+6Q5goEOodZpGr+5sQSuRLlw XMKzcU7ZDfe1jidjjcWSyf6UMKB/mhMQVMTTDURt/jS7koA5lXiU+m0XCSi28wTr lV9ZRzZiE4mRDANlEkoqUCYPG/PDF0KOgDROiavlrBcBycsqBD5iRdGYGaYwUmSo UpaVGGLYBguqwEeQ2ixC2rTGkyg7 -----END CERTIFICATE----- IBM-python-sdk-core-7ebd71d/resources/test_ssl.key000066400000000000000000000032501502256645000222350ustar00rootroot00000000000000-----BEGIN PRIVATE KEY----- MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCx7eyXu0A1IH18 U0fmVbHlC0OpzpGfGOQayrlrxyxNkNc8T2ehnt7W33FHI2tjcmO11wgsN+U2+uB2 aq0q1OYaqo3OlZtTF4A91CgAdbGdIx5aEEzjogiQIBvrBQhaU6uFTzUBQ5tWs+pL corVrp8G/ONN/1e4Z3NCg036ibSsVkfdw1zX6vTR674uTq8aIG7sH3DCF1Q+Czvx hQrhjkZOha+u0H+OhZ9yd30hU/xyAKZsoGHNY65bVSYAPxP6XLw5inF534TLrigg FDonEk16eHjAi7SxcjdKcyxhfX7uDefD/s9cKUY4Tf1JeAx2F1Y++ffqWlQSde5D KkfC6xU5AgMBAAECggEABbqBSYlP0eYP5DbSM8pChftM3GS4L4UfovUv7xZkiMLH CzwLPBrfVc+v1/h99p+yMiKQMsxB5vlAzM82cBCWr/kZw7LxY0V4bYUtHIathz+g NIodz55h5DIEdBafZDkZZptcO4QvtiTowDEZ4zNSD2mI7/PuoRNDlLqhghV46at3 nfJmOgbWxzciOhpYUXtXl1+n1ywkXIbL58sfp5HgkuM9dL2BTZQdUwbS58TgyYla DBYZQfiZ+MgrCqJKCYEy+lycVHz9LaSCVXgEYmstfcLqdC7X3/nQuUn587rV5FDA 75bDZ7r2jvrQT7/MEGm3b2fgk0+0gMew52wrDsncuQKBgQDygvVlm6BU2cIm8qvR l1LqU06jbEVW2fzrXi73RVgCR+ZQoy+hB8LviqNO0ayAOWIH/6iMGU09bK72DlJ0 K4P58X6iJaop1ntdYa2DApPzrz3/4weMl4ZVjz5tPBUxhkfJCMfZF2q+CoHOsdlQ rTXNFQgfq4XNGpj3YX0L94pkgwKBgQC702bRFk7lHoMyn8hs8MQqumSsefpTsc6v 3fLl6DgwhU9/FAEymIt+wEIchoU0GkVfZ16OmO1whZAgj3zsBr/zG7C7Din9s7hB KV57o7KIRP3IUo+N6qQ2xNPG8lrCd7Xz+430RHT5BRudqNXWsZziFOrPXZz7Yh8/ TMy4ZwzKkwKBgQCOTToh/Uf/gifjItKfkeQdi/TBAG9Pn2pB0mpMvmv+KqKC/r6c Bynj1b4uKerG8uULPIFydAZW3MdtqsnHUSGIMKTWELPhCPIqwX5HOeQHQfVniZiM bv1shzlib7cf8GN/G5/pS0xfZ1r0JngWVw0S4hx6OPOyfsDzqEjwFLkocQKBgBOQ 2xYO19sgSZR9dph6oES/M/uPnVcYn6pMWaA/h5LuYDChudo2b9mdV4W3MasSzYU5 tGzwW1OsZi4uJFpF/brqeIeT2yX1kc0f7Rq+G7v8S9+RUij7d23JJTKFTpUReV/Y JZp7gx/pu026J8R8rhYTDb7aRp8dQpoKewz+lyOHAoGAUGOfsLybzE2w/gL1gZ8W L6sdkQ7zq/IF04cqM5jw1g82f9CGdZlWrT6wv5D++O9zpZSmuwJ2p/os5Tsygmb4 M0IjG23Mw5IWLCC6n2riYwpQ8sjuL3SOhqgt4k5mnu4H0RpVLz69JPBJkaGuE1Ld k4zeVfI1+X2clcHVfDS6xhc= -----END PRIVATE KEY----- IBM-python-sdk-core-7ebd71d/test/000077500000000000000000000000001502256645000166305ustar00rootroot00000000000000IBM-python-sdk-core-7ebd71d/test/__init__.py000066400000000000000000000000001502256645000207270ustar00rootroot00000000000000IBM-python-sdk-core-7ebd71d/test/test_api_exception.py000066400000000000000000000063201502256645000230710ustar00rootroot00000000000000# coding=utf-8 import json import logging import requests import responses from ibm_cloud_sdk_core import ApiException from .utils.logger_utils import setup_test_logger setup_test_logger(logging.ERROR) @responses.activate def test_api_exception(): """Test APIException class""" responses.add( responses.GET, 'https://test.com', status=500, body=json.dumps({'error': 'sorry', 'msg': 'serious error'}), content_type='application/json', ) mock_response = requests.get('https://test.com', timeout=None) exception = ApiException(500, http_response=mock_response) assert exception is not None assert exception.message == 'sorry' responses.add( responses.GET, 'https://test-again.com', status=500, body=json.dumps( { "errors": [ { "message": "sorry again", } ], } ), content_type='application/json', ) mock_response = requests.get('https://test-again.com', timeout=None) exception = ApiException(500, http_response=mock_response) assert exception.message == 'sorry again' responses.add( responses.GET, 'https://test-once-more.com', status=500, body=json.dumps({'message': 'sorry once more'}), content_type='application/json', ) mock_response = requests.get('https://test-once-more.com', timeout=None) exception = ApiException(500, http_response=mock_response) assert exception.message == 'sorry once more' responses.add( responses.GET, 'https://test-msg.com', status=500, body=json.dumps({'msg': 'serious error'}), content_type='application/json', ) mock_response = requests.get('https://test-msg.com', timeout=None) exception = ApiException(500, http_response=mock_response) assert exception.message == 'Internal Server Error' responses.add( responses.GET, 'https://test-errormessage.com', status=500, body=json.dumps({'errorMessage': 'IAM error message'}), content_type='application/json', ) mock_response = requests.get('https://test-errormessage.com', timeout=None) exception = ApiException(500, http_response=mock_response) assert exception.message == 'IAM error message' assert exception.http_response.text == '{"errorMessage": "IAM error message"}' assert exception.http_response.content == b'{"errorMessage": "IAM error message"}' assert exception.http_response.json() == {'errorMessage': 'IAM error message'} responses.add( responses.GET, 'https://test-for-text.com', status=500, headers={'X-Global-Transaction-ID': 'xx'}, body="plain text error", ) mock_response = requests.get('https://test-for-text.com', timeout=None) exception = ApiException(500, http_response=mock_response) assert exception.message == 'plain text error' assert str(exception) == 'Error: plain text error, Status code: 500 , X-global-transaction-id: xx' assert exception.http_response.text == 'plain text error' assert exception.http_response.content == b'plain text error' IBM-python-sdk-core-7ebd71d/test/test_authenticator.py000066400000000000000000000012601502256645000231120ustar00rootroot00000000000000# coding=utf-8 # pylint: disable=missing-docstring from requests import Request from ibm_cloud_sdk_core.authenticators import Authenticator class TestAuthenticator(Authenticator): """A test of the Authenticator base class""" def validate(self) -> None: """Simulated validate() method.""" def authenticate(self, req: Request) -> None: """Simulated authenticate() method.""" def test_authenticator(): authenticator = TestAuthenticator() assert authenticator is not None assert authenticator.authentication_type() == Authenticator.AUTHTYPE_UNKNOWN assert authenticator.validate() is None assert authenticator.authenticate(None) is None IBM-python-sdk-core-7ebd71d/test/test_base_service.py000066400000000000000000001202671502256645000227030ustar00rootroot00000000000000# coding: utf-8 # Copyright 2019, 2024 IBM All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # pylint: disable=missing-docstring,protected-access,too-few-public-methods,too-many-lines import gzip import json import logging import os import ssl import tempfile import time from shutil import copyfile from typing import Optional from urllib3.exceptions import ConnectTimeoutError, MaxRetryError import jwt import pytest import responses import requests from ibm_cloud_sdk_core import ApiException from ibm_cloud_sdk_core import BaseService, DetailedResponse from ibm_cloud_sdk_core import CP4DTokenManager from ibm_cloud_sdk_core import get_authenticator_from_environment from ibm_cloud_sdk_core.authenticators import ( IAMAuthenticator, NoAuthAuthenticator, Authenticator, BasicAuthenticator, CloudPakForDataAuthenticator, ) from .utils.logger_utils import setup_test_logger # Note that the test_reserved_keys method below will fail if the log level is not WARNING. setup_test_logger(logging.WARNING) class IncludeExternalConfigService(BaseService): default_service_url = 'https://servicesthatincludeexternalconfig.com/api' def __init__( self, api_version: str, authenticator: Optional[Authenticator] = None, trace_id: Optional[str] = None ) -> None: BaseService.__init__( self, service_url=self.default_service_url, authenticator=authenticator, disable_ssl_verification=False ) self.api_version = api_version self.trace_id = trace_id self.configure_service('include-external-config') class AnyServiceV1(BaseService): default_url = 'https://gateway.watsonplatform.net/test/api' def __init__( self, version: str, service_url: str = default_url, authenticator: Optional[Authenticator] = None, disable_ssl_verification: bool = False, ) -> None: BaseService.__init__( self, service_url=service_url, authenticator=authenticator, disable_ssl_verification=disable_ssl_verification, ) self.version = version def op_with_path_params(self, path0: str, path1: str) -> DetailedResponse: if path0 is None: raise ValueError('path0 must be provided') if path1 is None: raise ValueError('path1 must be provided') params = {'version': self.version} url = '/v1/foo/{0}/bar/{1}/baz'.format(*self._encode_path_vars(path0, path1)) request = self.prepare_request(method='GET', url=url, params=params) response = self.send(request) return response def with_http_config(self, http_config: dict) -> DetailedResponse: self.set_http_config(http_config) request = self.prepare_request(method='GET', url='') response = self.send(request) return response def any_service_call(self) -> DetailedResponse: request = self.prepare_request(method='GET', url='') response = self.send(request) return response def head_request(self) -> DetailedResponse: request = self.prepare_request(method='HEAD', url='') response = self.send(request) return response def get_document_as_stream(self) -> DetailedResponse: params = {'version': self.version} url = '/v1/streamjson' request = self.prepare_request(method='GET', url=url, params=params) response = self.send(request, stream=True) return response def get_access_token() -> str: access_token_layout = { "username": "dummy", "role": "Admin", "permissions": ["administrator", "manage_catalog"], "sub": "admin", "iss": "sss", "aud": "sss", "uid": "sss", "iat": 3600, "exp": int(time.time()), } access_token = jwt.encode( access_token_layout, 'secret', algorithm='HS256', headers={'kid': '230498151c214b788dd97f22b85410a5'} ) return access_token def test_invalid_authenticator(): with pytest.raises(ValueError) as err: AnyServiceV1('2021-08-18') assert str(err.value) == 'authenticator must be provided' @responses.activate def test_url_encoding(): service = AnyServiceV1('2017-07-07', authenticator=NoAuthAuthenticator()) # All characters in path0 _must_ be encoded in path segments path0 = ' \"<>^`{}|/\\?#%[]' path0_encoded = '%20%22%3C%3E%5E%60%7B%7D%7C%2F%5C%3F%23%25%5B%5D' # All non-ASCII chars _must_ be encoded in path segments path1 = '比萨浇头'.encode('utf8') # "pizza toppings" path1_encoded = '%E6%AF%94%E8%90%A8%E6%B5%87%E5%A4%B4' path_encoded = '/v1/foo/' + path0_encoded + '/bar/' + path1_encoded + '/baz' test_url = service.default_url + path_encoded responses.add( responses.GET, test_url, status=200, body=json.dumps({"foobar": "baz"}), content_type='application/json' ) # Set Host as a default header on the service. service.set_default_headers({'Host': 'alternatehost.ibm.com:443'}) response = service.op_with_path_params(path0, path1) assert response is not None assert len(responses.calls) == 1 assert path_encoded in responses.calls[0].request.url assert 'version=2017-07-07' in responses.calls[0].request.url # Verify that the Host header was set in the request. assert responses.calls[0].request.headers.get('Host') == 'alternatehost.ibm.com:443' @responses.activate def test_stream_json_response(): service = AnyServiceV1('2017-07-07', authenticator=NoAuthAuthenticator()) path = '/v1/streamjson' test_url = service.default_url + path expected_response = json.dumps({"id": 1, "rev": "v1", "content": "this is a document"}) # print("Expected response: ", expected_response) # Simulate a JSON response responses.add(responses.GET, test_url, status=200, body=expected_response, content_type='application/json') # Invoke the operation and receive an "iterable" as the response response = service.get_document_as_stream() assert response is not None assert len(responses.calls) == 1 # retrieve the requests.Response object from the DetailedResponse resp = response.get_result() assert isinstance(resp, requests.Response) assert hasattr(resp, "iter_content") # Retrieve the response body, one chunk at a time. actual_response = '' for chunk in resp.iter_content(chunk_size=3): actual_response += chunk.decode("utf-8") # print("Actual response: ", actual_response) assert actual_response == expected_response @responses.activate def test_http_config(): service = AnyServiceV1('2017-07-07', authenticator=NoAuthAuthenticator()) responses.add( responses.GET, service.default_url, status=200, body=json.dumps({"foobar": "baz"}), content_type='application/json', ) response = service.with_http_config({'timeout': 100}) assert response is not None assert len(responses.calls) == 1 def test_fail_http_config(): service = AnyServiceV1('2017-07-07', authenticator=NoAuthAuthenticator()) with pytest.raises(TypeError): service.with_http_config(None) @responses.activate def test_cwd(): file_path = os.path.join(os.path.dirname(__file__), '../resources/ibm-credentials.env') # Try changing working directories to test getting creds from cwd cwd = os.getcwd() os.chdir(os.path.dirname(file_path)) iam_authenticator = get_authenticator_from_environment('ibm_watson') service = AnyServiceV1('2017-07-07', authenticator=iam_authenticator) service.configure_service('ibm_watson') os.chdir(cwd) assert service.service_url == 'https://cwdserviceurl' assert service.authenticator is not None # Copy credentials file to cwd to test loading from current working directory temp_env_path = os.getcwd() + '/ibm-credentials.env' copyfile(file_path, temp_env_path) iam_authenticator = get_authenticator_from_environment('ibm_watson') service = AnyServiceV1('2017-07-07', authenticator=iam_authenticator) service.configure_service('ibm_watson') os.remove(temp_env_path) assert service.service_url == 'https://cwdserviceurl' assert service.authenticator is not None @responses.activate def test_iam(): file_path = os.path.join(os.path.dirname(__file__), '../resources/ibm-credentials-iam.env') os.environ['IBM_CREDENTIALS_FILE'] = file_path iam_authenticator = get_authenticator_from_environment('ibm-watson') service = AnyServiceV1('2017-07-07', authenticator=iam_authenticator) assert service.service_url == 'https://gateway.watsonplatform.net/test/api' del os.environ['IBM_CREDENTIALS_FILE'] assert service.authenticator is not None response = { "access_token": get_access_token(), "token_type": "Bearer", "expires_in": 3600, "expiration": int(time.time()), "refresh_token": "jy4gl91BQ", } responses.add(responses.POST, url='https://iam.cloud.ibm.com/identity/token', body=json.dumps(response), status=200) responses.add( responses.GET, url='https://gateway.watsonplatform.net/test/api', body=json.dumps({"foobar": "baz"}), content_type='application/json', ) service.any_service_call() assert "grant-type%3Aapikey" in responses.calls[0].request.body def test_no_auth(): class MadeUp: def __init__(self): self.lazy = 'made up' with pytest.raises(ValueError) as err: service = AnyServiceV1('2017-07-07', authenticator=MadeUp()) service.prepare_request( responses.GET, url='https://gateway.watsonplatform.net/test/api', ) assert str(err.value) == 'authenticator should be of type Authenticator' service = AnyServiceV1('2017-07-07', authenticator=NoAuthAuthenticator()) service.prepare_request( responses.GET, url='https://gateway.watsonplatform.net/test/api', ) assert service.authenticator is not None assert isinstance(service.authenticator, Authenticator) def test_for_cp4d(): cp4d_authenticator = CloudPakForDataAuthenticator('my_username', 'my_password', 'my_url') service = AnyServiceV1('2017-07-07', authenticator=cp4d_authenticator) assert service.authenticator.token_manager is not None assert service.authenticator.token_manager.username == 'my_username' assert service.authenticator.token_manager.password == 'my_password' assert service.authenticator.token_manager.url == 'my_url/v1/authorize' assert isinstance(service.authenticator.token_manager, CP4DTokenManager) def test_disable_ssl_verification(): service1 = AnyServiceV1('2017-07-07', authenticator=NoAuthAuthenticator(), disable_ssl_verification=True) assert service1.disable_ssl_verification is True service1.set_disable_ssl_verification(False) assert service1.disable_ssl_verification is False cp4d_authenticator = CloudPakForDataAuthenticator('my_username', 'my_password', 'my_url') service2 = AnyServiceV1('2017-07-07', authenticator=cp4d_authenticator) assert service2.disable_ssl_verification is False cp4d_authenticator.set_disable_ssl_verification(True) assert service2.authenticator.token_manager.disable_ssl_verification is True @responses.activate def test_http_head(): service = AnyServiceV1('2018-11-20', authenticator=NoAuthAuthenticator()) expected_headers = {'Test-Header1': 'value1', 'Test-Header2': 'value2'} responses.add(responses.HEAD, service.default_url, status=200, headers=expected_headers, content_type=None) response = service.head_request() assert response is not None assert len(responses.calls) == 1 assert response.headers is not None assert response.headers == expected_headers @responses.activate def test_response_with_no_body(): service = AnyServiceV1('2018-11-20', authenticator=NoAuthAuthenticator()) responses.add(responses.GET, service.default_url, status=200, body=None) response = service.any_service_call() assert response is not None assert len(responses.calls) == 1 assert response.get_result() is None def test_has_bad_first_or_last_char(): with pytest.raises(ValueError) as err: basic_authenticator = BasicAuthenticator('{my_username}', 'my_password') AnyServiceV1('2018-11-20', authenticator=basic_authenticator).prepare_request( responses.GET, 'https://gateway.watsonplatform.net/test/api' ) assert ( str(err.value) == 'The username and password shouldn\'t start or end with curly brackets or quotes. ' 'Please remove any surrounding {, }, or \" characters.' ) @responses.activate def test_request_server_error(): with pytest.raises(ApiException, match=r'internal server error') as err: responses.add( responses.GET, 'https://gateway.watsonplatform.net/test/api', status=500, body=json.dumps({'error': 'internal server error'}), content_type='application/json', ) service = AnyServiceV1('2018-11-20', authenticator=NoAuthAuthenticator()) prepped = service.prepare_request('GET', url='') service.send(prepped) assert err.value.code == 500 assert err.value.status_code == 500 assert err.value.http_response.headers['Content-Type'] == 'application/json' assert err.value.message == 'internal server error' @responses.activate def test_request_success_json(): responses.add( responses.GET, 'https://gateway.watsonplatform.net/test/api', status=200, body=json.dumps({'foo': 'bar'}), content_type='application/json', ) service = AnyServiceV1('2018-11-20', authenticator=NoAuthAuthenticator()) prepped = service.prepare_request('GET', url='') detailed_response = service.send(prepped) assert detailed_response.get_result() == {'foo': 'bar'} service = AnyServiceV1('2018-11-20', authenticator=BasicAuthenticator('my_username', 'my_password')) service.set_default_headers({'test': 'header'}) service.set_disable_ssl_verification(True) prepped = service.prepare_request('GET', url='') detailed_response = service.send(prepped) assert detailed_response.get_result() == {'foo': 'bar'} @responses.activate def test_request_success_invalid_json(): # expect an ApiException with JSONDecodeError as the cause when a "success" # response contains invalid JSON in response body. with pytest.raises(ApiException, match=r'Error processing the HTTP response') as err: responses.add( responses.GET, 'https://gateway.watsonplatform.net/test/api', status=200, body='{ "invalid": "json", "response"', content_type='application/json; charset=utf8', ) service = AnyServiceV1('2018-11-20', authenticator=NoAuthAuthenticator()) prepped = service.prepare_request('GET', url='') service.send(prepped) assert err.value.code == 200 assert err.value.status_code == 200 assert err.value.http_response.headers['Content-Type'] == 'application/json; charset=utf8' assert isinstance(err.value.__cause__, requests.exceptions.JSONDecodeError) assert "Expecting ':' delimiter: line 1" in str(err.value.__cause__) @responses.activate def test_request_success_response(): expected_body = '{"foo": "bar", "description": "this\nis\na\ndescription"}' responses.add( responses.GET, 'https://gateway.watsonplatform.net/test/api', status=200, body=expected_body, content_type='application/json', ) service = AnyServiceV1('2018-11-20', authenticator=NoAuthAuthenticator()) prepped = service.prepare_request('GET', url='') detailed_response = service.send(prepped) assert detailed_response.get_result() == {"foo": "bar", "description": "this\nis\na\ndescription"} @responses.activate def test_request_success_nonjson(): responses.add( responses.GET, 'https://gateway.watsonplatform.net/test/api', status=200, body='

Hola, amigo!

', content_type='text/html', ) service = AnyServiceV1('2018-11-20', authenticator=NoAuthAuthenticator()) prepped = service.prepare_request('GET', url='') detailed_response = service.send(prepped) # It's odd that we have to call ".text" to get the string value # (see issue 3557) assert detailed_response.get_result().text == '

Hola, amigo!

' @responses.activate def test_request_fail_401_nonerror_json(): # response body not an error object, so we expect the default error message. error_msg = 'Unauthorized: Access is denied due to invalid credentials' with pytest.raises(ApiException, match=error_msg) as err: responses.add( responses.GET, 'https://gateway.watsonplatform.net/test/api', status=401, body=json.dumps({'foo': 'bar'}), content_type='application/json', ) service = AnyServiceV1('2018-11-20', authenticator=NoAuthAuthenticator()) prepped = service.prepare_request('GET', url='') service.send(prepped) assert err.value.code == 401 assert err.value.status_code == 401 assert err.value.http_response.headers['Content-Type'] == 'application/json' assert err.value.message == error_msg @responses.activate def test_request_fail_401_error_json(): # response body is an error object, so we expect to get the message from there. error_msg = 'You dont need to know...' with pytest.raises(ApiException, match=error_msg) as err: responses.add( responses.GET, 'https://gateway.watsonplatform.net/test/api', status=401, body=json.dumps({'message': error_msg}), content_type='application/json', ) service = AnyServiceV1('2018-11-20', authenticator=NoAuthAuthenticator()) prepped = service.prepare_request('GET', url='') service.send(prepped) assert err.value.code == 401 assert err.value.status_code == 401 assert err.value.http_response.headers['Content-Type'] == 'application/json' assert err.value.message == error_msg @responses.activate def test_request_fail_401_nonjson(): response_body = 'You dont have a need to know...' with pytest.raises(ApiException, match=response_body) as err: responses.add( responses.GET, 'https://gateway.watsonplatform.net/test/api', status=401, body=response_body, content_type='text/plain', ) service = AnyServiceV1('2018-11-20', authenticator=NoAuthAuthenticator()) prepped = service.prepare_request('GET', url='') service.send(prepped) assert err.value.code == 401 assert err.value.status_code == 401 assert err.value.http_response.headers['Content-Type'] == 'text/plain' assert err.value.message == response_body @responses.activate def test_request_fail_401_badjson(): # if an error response contains invalid JSON, then we should # end up with 'Unknown error' as the message since we couldn't get # the actual error message from the response body. response_body = 'This is not a JSON object' with pytest.raises(ApiException, match=response_body) as err: responses.add( responses.GET, 'https://gateway.watsonplatform.net/test/api', status=401, body=response_body, content_type='application/json', ) service = AnyServiceV1('2018-11-20', authenticator=NoAuthAuthenticator()) prepped = service.prepare_request('GET', url='') service.send(prepped) assert err.value.code == 401 assert err.value.status_code == 401 assert err.value.http_response.headers['Content-Type'] == 'application/json' assert err.value.message == response_body def test_misc_methods(): class MockModel: def __init__(self, xyz=None): self.xyz = xyz def _to_dict(self): _dict = {} if hasattr(self, 'xyz') and self.xyz is not None: _dict['xyz'] = self.xyz return _dict @classmethod def _from_dict(cls, _dict): args = {} if 'xyz' in _dict: args['xyz'] = _dict.get('xyz') return cls(**args) mock = MockModel('foo') service = AnyServiceV1('2018-11-20', authenticator=NoAuthAuthenticator()) model1 = service._convert_model(mock) assert model1 == {'xyz': 'foo'} model2 = service._convert_model("{\"xyz\": \"foo\"}") assert model2 is not None assert model2['xyz'] == 'foo' temp = ['default', '123'] res_str = service._convert_list(temp) assert res_str == 'default,123' temp2 = 'default123' res_str2 = service._convert_list(temp2) assert res_str2 == temp2 def test_default_headers(): service = AnyServiceV1('2018-11-20', authenticator=NoAuthAuthenticator()) service.set_default_headers({'xxx': 'yyy'}) assert service.default_headers == {'xxx': 'yyy'} with pytest.raises(TypeError): service.set_default_headers('xxx') def test_set_service_url(): service = AnyServiceV1('2018-11-20', authenticator=NoAuthAuthenticator()) with pytest.raises(ValueError) as err: service.set_service_url('{url}') assert ( str(err.value) == 'The service url shouldn\'t start or end with curly brackets or quotes. ' 'Be sure to remove any {} and \" characters surrounding your service url' ) service.set_service_url('my_url') def test_http_client(): auth = BasicAuthenticator('my_username', 'my_password') service = AnyServiceV1('2018-11-20', authenticator=auth) assert isinstance(service.get_http_client(), requests.sessions.Session) assert service.get_http_client().headers.get('Accept-Encoding') == 'gzip, deflate' new_http_client = requests.Session() new_http_client.headers.update({'Accept-Encoding': 'gzip'}) service.set_http_client(http_client=new_http_client) assert service.get_http_client().headers.get('Accept-Encoding') == 'gzip' with pytest.raises(TypeError): service.set_http_client("bad_argument_type") def test_get_authenticator(): auth = BasicAuthenticator('my_username', 'my_password') service = AnyServiceV1('2018-11-20', authenticator=auth) assert service.get_authenticator() is not None def test_gzip_compression(): # Should return uncompressed data when gzip is off service = AnyServiceV1('2018-11-20', authenticator=NoAuthAuthenticator()) assert not service.get_enable_gzip_compression() prepped = service.prepare_request('GET', url='', data=json.dumps({"foo": "bar"})) assert prepped['data'] == b'{"foo": "bar"}' assert prepped['headers'].get('content-encoding') != 'gzip' # Should return compressed data when gzip is on service.set_enable_gzip_compression(True) assert service.get_enable_gzip_compression() prepped = service.prepare_request('GET', url='', data=json.dumps({"foo": "bar"})) assert prepped['data'] == gzip.compress(b'{"foo": "bar"}') assert prepped['headers'].get('content-encoding') == 'gzip' # Should return compressed data when gzip is on for non-json data assert service.get_enable_gzip_compression() prepped = service.prepare_request('GET', url='', data=b'rawdata') assert prepped['data'] == gzip.compress(b'rawdata') assert prepped['headers'].get('content-encoding') == 'gzip' # Should return compressed data when gzip is on for gzip file data assert service.get_enable_gzip_compression() with tempfile.TemporaryFile(mode='w+b') as t_f: with gzip.GzipFile(mode='wb', fileobj=t_f) as gz_f: gz_f.write(json.dumps({"foo": "bar"}).encode()) with gzip.GzipFile(mode='rb', fileobj=t_f) as gz_f: gzip_data = gz_f.read() prepped = service.prepare_request('GET', url='', data=gzip_data) assert prepped['data'] == gzip.compress(t_f.read()) assert prepped['headers'].get('content-encoding') == 'gzip' # Should return compressed json data when gzip is on for gzip file json data assert service.get_enable_gzip_compression() with tempfile.TemporaryFile(mode='w+b') as t_f: with gzip.GzipFile(mode='wb', fileobj=t_f) as gz_f: gz_f.write("rawdata".encode()) with gzip.GzipFile(mode='rb', fileobj=t_f) as gz_f: gzip_data = gz_f.read() prepped = service.prepare_request('GET', url='', data=gzip_data) assert prepped['data'] == gzip.compress(t_f.read()) assert prepped['headers'].get('content-encoding') == 'gzip' # Should return uncompressed data when content-encoding is set assert service.get_enable_gzip_compression() prepped = service.prepare_request( 'GET', url='', headers={"content-encoding": "gzip"}, data=json.dumps({"foo": "bar"}) ) assert prepped['data'] == b'{"foo": "bar"}' assert prepped['headers'].get('content-encoding') == 'gzip' def test_gzip_compression_file_input(): service = AnyServiceV1('2018-11-20', authenticator=NoAuthAuthenticator()) service.set_enable_gzip_compression(True) # Should return file-like object with the compressed data when compression is on # and the input is a file, opened for reading in binary mode. raw_data = b'rawdata' with tempfile.TemporaryFile(mode='w+b') as tmp_file: tmp_file.write(raw_data) tmp_file.seek(0) prepped = service.prepare_request('GET', url='', data=tmp_file) assert prepped['data'].read() == gzip.compress(raw_data) assert prepped['headers'].get('content-encoding') == 'gzip' assert prepped['data'].read() == b'' # Simulate the requests (urllib3) package reading method for binary files. with tempfile.TemporaryFile(mode='w+b') as tmp_file: tmp_file.write(raw_data) tmp_file.seek(0) prepped = service.prepare_request('GET', url='', data=tmp_file) compressed = b'' for chunk in prepped['data']: compressed += chunk assert compressed == gzip.compress(raw_data) # Make sure the decompression works fine. assert gzip.decompress(compressed) == raw_data # Should return file-like object with the compressed data when compression is on # and the input is a file, opened for reading in text mode. assert service.get_enable_gzip_compression() text_data = 'textdata' with tempfile.TemporaryFile(mode='w+') as tmp_file: tmp_file.write(text_data) tmp_file.seek(0) prepped = service.prepare_request('GET', url='', data=tmp_file) assert prepped['data'].read() == gzip.compress(text_data.encode()) assert prepped['headers'].get('content-encoding') == 'gzip' assert prepped['data'].read() == b'' # Simulate the requests (urllib3) package reading method for text files. with tempfile.TemporaryFile(mode='w+') as tmp_file: tmp_file.write(text_data) tmp_file.seek(0) prepped = service.prepare_request('GET', url='', data=tmp_file) compressed = b'' for chunk in prepped['data']: compressed += chunk assert compressed == gzip.compress(text_data.encode()) # Make sure the decompression works fine. assert gzip.decompress(compressed).decode() == text_data def test_gzip_compression_external(): # Should set gzip compression from external config file_path = os.path.join(os.path.dirname(__file__), '../resources/ibm-credentials-gzip.env') os.environ['IBM_CREDENTIALS_FILE'] = file_path service = IncludeExternalConfigService('v1', authenticator=NoAuthAuthenticator()) assert service.service_url == 'https://mockurl' assert service.get_enable_gzip_compression() is True prepped = service.prepare_request('GET', url='', data=json.dumps({"foo": "bar"})) assert prepped['data'] == gzip.compress(b'{"foo": "bar"}') assert prepped['headers'].get('content-encoding') == 'gzip' def test_retry_config_default(): service = BaseService(service_url='https://mockurl/', authenticator=NoAuthAuthenticator()) service.enable_retries() assert service.retry_config.total == 4 assert service.retry_config.backoff_factor == 1.0 assert service.retry_config.backoff_max == 30.0 assert service.http_client.get_adapter('https://').max_retries.total == 4 # Ensure retries fail after 4 retries error = ConnectTimeoutError() retry = service.http_client.get_adapter('https://').max_retries retry = retry.increment(error=error) retry = retry.increment(error=error) retry = retry.increment(error=error) retry = retry.increment(error=error) with pytest.raises(MaxRetryError) as retry_err: retry.increment(error=error) assert retry_err.value.reason == error def test_retry_config_disable(): # Test disabling retries service = BaseService(service_url='https://mockurl/', authenticator=NoAuthAuthenticator()) service.enable_retries() service.disable_retries() assert service.retry_config is None assert service.http_client.get_adapter('https://').max_retries.total == 0 # Ensure retries are not started after one connection attempt error = ConnectTimeoutError() retry = service.http_client.get_adapter('https://').max_retries with pytest.raises(MaxRetryError) as retry_err: retry.increment(error=error) assert retry_err.value.reason == error def test_retry_config_non_default(): service = BaseService(service_url='https://mockurl/', authenticator=NoAuthAuthenticator()) service.enable_retries(2, 10.0) assert service.retry_config.total == 2 assert service.retry_config.backoff_factor == 1.0 assert service.retry_config.backoff_max == 10.0 # Ensure retries fail after 2 retries error = ConnectTimeoutError() retry = service.http_client.get_adapter('https://').max_retries retry = retry.increment(error=error) retry = retry.increment(error=error) with pytest.raises(MaxRetryError) as retry_err: retry.increment(error=error) assert retry_err.value.reason == error def test_retry_config_external(): file_path = os.path.join(os.path.dirname(__file__), '../resources/ibm-credentials-retry.env') os.environ['IBM_CREDENTIALS_FILE'] = file_path service = IncludeExternalConfigService('v1', authenticator=NoAuthAuthenticator()) assert service.retry_config.total == 3 assert service.retry_config.backoff_factor == 1.0 assert service.retry_config.backoff_max == 25.0 # Ensure retries fail after 3 retries error = ConnectTimeoutError() retry = service.http_client.get_adapter('https://').max_retries retry = retry.increment(error=error) retry = retry.increment(error=error) retry = retry.increment(error=error) with pytest.raises(MaxRetryError) as retry_err: retry.increment(error=error) assert retry_err.value.reason == error @responses.activate def test_user_agent_header(): service = AnyServiceV1('2018-11-20', authenticator=NoAuthAuthenticator()) user_agent_header = service.user_agent_header assert user_agent_header is not None assert user_agent_header['User-Agent'].startswith('ibm-python-sdk-core-') responses.add(responses.GET, 'https://gateway.watsonplatform.net/test/api', status=200, body='some text') prepped = service.prepare_request('GET', url='', headers={'user-agent': 'my_user_agent'}) response = service.send(prepped) assert response.get_result().request.headers.get('user-agent') == 'my_user_agent' prepped = service.prepare_request('GET', url='', headers=None) response = service.send(prepped) assert response.get_result().request.headers.get('user-agent') == user_agent_header['User-Agent'] # Note that this test will fail if the log level is not set to WARNING above. # This is due to the fact that we are checking that the expected warning messages # are located in specific locations within the captured logs, and this only works correctly # if only warning and error messages are being logged. @responses.activate def test_reserved_keys(caplog): service = AnyServiceV1('2021-07-02', authenticator=NoAuthAuthenticator()) responses.add(responses.GET, 'https://gateway.watsonplatform.net/test/api', status=200, body='some text') prepped = service.prepare_request('GET', url='', headers={'key': 'OK'}) response = service.send( prepped, headers={'key': 'bad'}, method='POST', url='localhost', cookies=None, hooks={'response': []} ) assert response.get_result().request.headers.get('key') == 'OK' assert response.get_result().request.url == 'https://gateway.watsonplatform.net/test/api' assert response.get_result().request.method == 'GET' assert response.get_result().request.hooks == {'response': []} assert caplog.record_tuples[0][2] == '"method" has been removed from the request' assert caplog.record_tuples[1][2] == '"url" has been removed from the request' assert caplog.record_tuples[2][2] == '"cookies" has been removed from the request' @responses.activate def test_ssl_error(): responses.add(responses.GET, 'https://gateway.watsonplatform.net/test/api', body=requests.exceptions.SSLError()) service = AnyServiceV1('2021-08-18', authenticator=NoAuthAuthenticator()) with pytest.raises(requests.exceptions.SSLError): prepped = service.prepare_request('GET', url='') service.send(prepped) def test_files_dict(): service = AnyServiceV1('2018-11-20', authenticator=NoAuthAuthenticator()) form_data = {} with open( os.path.join(os.path.dirname(__file__), '../resources/ibm-credentials-iam.env'), 'r', encoding='utf-8' ) as file: form_data['file1'] = (None, file, 'application/octet-stream') form_data['string1'] = (None, 'hello', 'text/plain') request = service.prepare_request('GET', url='', headers={'X-opt-out': True}, files=form_data) files = request['files'] assert isinstance(files, list) assert len(files) == 2 files_dict = dict(files) file1 = files_dict['file1'] assert file1[0] == 'ibm-credentials-iam.env' string1 = files_dict['string1'] assert string1[0] is None def test_files_list(): service = AnyServiceV1('2018-11-20', authenticator=NoAuthAuthenticator()) form_data = [] with open( os.path.join(os.path.dirname(__file__), '../resources/ibm-credentials-iam.env'), 'r', encoding='utf-8' ) as file: form_data.append(('file1', (None, file, 'application/octet-stream'))) form_data.append(('string1', (None, 'hello', 'text/plain'))) request = service.prepare_request('GET', url='', headers={'X-opt-out': True}, files=form_data) files = request['files'] assert isinstance(files, list) assert len(files) == 2 files_dict = dict(files) file1 = files_dict['file1'] assert file1[0] == 'ibm-credentials-iam.env' string1 = files_dict['string1'] assert string1[0] is None def test_files_duplicate_parts(): service = AnyServiceV1('2018-11-20', authenticator=NoAuthAuthenticator()) form_data = [] with open( os.path.join(os.path.dirname(__file__), '../resources/ibm-credentials-iam.env'), 'r', encoding='utf-8' ) as file: form_data.append(('creds_file', (None, file, 'application/octet-stream'))) with open( os.path.join(os.path.dirname(__file__), '../resources/ibm-credentials-basic.env'), 'r', encoding='utf-8' ) as file: form_data.append(('creds_file', (None, file, 'application/octet-stream'))) with open( os.path.join(os.path.dirname(__file__), '../resources/ibm-credentials-bearer.env'), 'r', encoding='utf-8' ) as file: form_data.append(('creds_file', (None, file, 'application/octet-stream'))) request = service.prepare_request('GET', url='', headers={'X-opt-out': True}, files=form_data) files = request['files'] assert isinstance(files, list) assert len(files) == 3 for part_name, file_tuple in files: assert part_name == 'creds_file' assert file_tuple[0] is not None def test_json(): service = AnyServiceV1('2018-11-20', authenticator=NoAuthAuthenticator()) req = service.prepare_request('POST', url='', headers={'X-opt-out': True}, data={'hello': 'world', 'fóó': 'bår'}) assert req.get('data') == b'{"hello": "world", "f\\u00f3\\u00f3": "b\\u00e5r"}' def test_service_url_handling(): service = AnyServiceV1('2018-11-20', service_url='https://host///////', authenticator=NoAuthAuthenticator()) assert service.service_url == 'https://host' service.set_service_url('https://host/') assert service.service_url == 'https://host' req = service.prepare_request('POST', url='/path/', headers={'X-opt-out': True}, data={'hello': 'world'}) assert req.get('url') == 'https://host/path/' service = AnyServiceV1('2018-11-20', service_url='https://host/', authenticator=NoAuthAuthenticator()) assert service.service_url == 'https://host' service.set_service_url('https://host/') assert service.service_url == 'https://host' req = service.prepare_request('POST', url='/', headers={'X-opt-out': True}, data={'hello': 'world'}) assert req.get('url') == 'https://host/' req = service.prepare_request('POST', url='////', headers={'X-opt-out': True}, data={'hello': 'world'}) assert req.get('url') == 'https://host/' service.set_service_url(None) assert service.service_url is None service = AnyServiceV1('2018-11-20', service_url='/', authenticator=NoAuthAuthenticator()) assert service.service_url == '' service.set_service_url('/') assert service.service_url == '' with pytest.raises(ValueError) as err: service.prepare_request('POST', url='/', headers={'X-opt-out': True}, data={'hello': 'world'}) assert str(err.value) == 'The service_url is required' def test_service_url_slash(): service = AnyServiceV1('2018-11-20', service_url='/', authenticator=NoAuthAuthenticator()) assert service.service_url == '' with pytest.raises(ValueError) as err: service.prepare_request('POST', url='/', headers={'X-opt-out': True}, data={'hello': 'world'}) assert str(err.value) == 'The service_url is required' def test_service_url_not_set(): service = BaseService(service_url='', authenticator=NoAuthAuthenticator()) with pytest.raises(ValueError) as err: service.prepare_request('POST', url='') assert str(err.value) == 'The service_url is required' def test_setting_proxy(): service = BaseService(service_url='test', authenticator=IAMAuthenticator('wonder woman')) assert service.authenticator is not None assert service.authenticator.token_manager.http_config == {} http_config = {"proxies": {"http": "user:password@host:port"}} service.set_http_config(http_config) assert service.authenticator.token_manager.http_config == http_config service2 = BaseService(service_url='test', authenticator=BasicAuthenticator('marvellous', 'mrs maisel')) service2.set_http_config(http_config) assert service2.authenticator is not None def test_configure_service(): file_path = os.path.join(os.path.dirname(__file__), '../resources/ibm-credentials-external.env') os.environ['IBM_CREDENTIALS_FILE'] = file_path service = IncludeExternalConfigService('v1', authenticator=NoAuthAuthenticator()) assert service.service_url == 'https://externallyconfigured.com/api' assert service.disable_ssl_verification is True # The authenticator should not be changed as a result of configure_service() assert isinstance(service.get_authenticator(), NoAuthAuthenticator) def test_configure_service_error(): service = BaseService(service_url='v1', authenticator=NoAuthAuthenticator()) with pytest.raises(ValueError) as err: service.configure_service(None) assert str(err.value) == 'Service_name must be of type string.' def test_min_ssl_version(): service = AnyServiceV1('2022-03-08', authenticator=NoAuthAuthenticator()) adapter = service.http_client.get_adapter('https://') ssl_context = adapter.poolmanager.connection_pool_kw.get('ssl_context', None) assert ssl_context is not None assert ssl_context.minimum_version == ssl.TLSVersion.TLSv1_2 IBM-python-sdk-core-7ebd71d/test/test_basic_authenticator.py000066400000000000000000000032341502256645000242560ustar00rootroot00000000000000# pylint: disable=missing-docstring import logging import pytest from ibm_cloud_sdk_core.authenticators import BasicAuthenticator, Authenticator from .utils.logger_utils import setup_test_logger setup_test_logger(logging.ERROR) def test_basic_authenticator(): authenticator = BasicAuthenticator('my_username', 'my_password') assert authenticator is not None assert authenticator.authentication_type() == Authenticator.AUTHTYPE_BASIC assert authenticator.username == 'my_username' assert authenticator.password == 'my_password' request = {'headers': {}} authenticator.authenticate(request) assert request['headers']['Authorization'] == 'Basic bXlfdXNlcm5hbWU6bXlfcGFzc3dvcmQ=' def test_basic_authenticator_validate_failed(): with pytest.raises(ValueError) as err: BasicAuthenticator('my_username', None) assert str(err.value) == 'The username and password shouldn\'t be None.' with pytest.raises(ValueError) as err: BasicAuthenticator(None, 'my_password') assert str(err.value) == 'The username and password shouldn\'t be None.' with pytest.raises(ValueError) as err: BasicAuthenticator('{my_username}', 'my_password') assert ( str(err.value) == 'The username and password shouldn\'t start or end with curly brackets or quotes. ' 'Please remove any surrounding {, }, or \" characters.' ) with pytest.raises(ValueError) as err: BasicAuthenticator('my_username', '{my_password}') assert ( str(err.value) == 'The username and password shouldn\'t start or end with curly brackets or quotes. ' 'Please remove any surrounding {, }, or \" characters.' ) IBM-python-sdk-core-7ebd71d/test/test_bearer_authenticator.py000066400000000000000000000022201502256645000244270ustar00rootroot00000000000000# pylint: disable=missing-docstring import logging import pytest from ibm_cloud_sdk_core.authenticators import BearerTokenAuthenticator, Authenticator from .utils.logger_utils import setup_test_logger setup_test_logger(logging.ERROR) def test_bearer_authenticator(): authenticator = BearerTokenAuthenticator('my_bearer_token') assert authenticator is not None assert authenticator.authentication_type() == Authenticator.AUTHTYPE_BEARERTOKEN assert authenticator.bearer_token == 'my_bearer_token' authenticator.set_bearer_token('james bond') assert authenticator.bearer_token == 'james bond' request = {'headers': {}} authenticator.authenticate(request) assert request['headers']['Authorization'] == 'Bearer james bond' def test_bearer_validate_failed(): with pytest.raises(ValueError) as err: BearerTokenAuthenticator(None) assert str(err.value) == 'The bearer token shouldn\'t be None.' authenticator = BearerTokenAuthenticator('my_bearer_token') with pytest.raises(ValueError) as err: authenticator.set_bearer_token(None) assert str(err.value) == 'The bearer token shouldn\'t be None.' IBM-python-sdk-core-7ebd71d/test/test_container_authenticator.py000066400000000000000000000103451502256645000251600ustar00rootroot00000000000000# pylint: disable=missing-docstring import pytest from ibm_cloud_sdk_core.authenticators import ContainerAuthenticator, Authenticator def test_container_authenticator(): authenticator = ContainerAuthenticator(iam_profile_name='iam-user-123') assert authenticator is not None assert authenticator.authentication_type() == Authenticator.AUTHTYPE_CONTAINER assert authenticator.token_manager.cr_token_filename is None assert authenticator.token_manager.iam_profile_name == 'iam-user-123' assert authenticator.token_manager.iam_profile_id is None assert authenticator.token_manager.client_id is None assert authenticator.token_manager.client_secret is None assert authenticator.token_manager.disable_ssl_verification is False assert authenticator.token_manager.headers is None assert authenticator.token_manager.proxies is None assert authenticator.token_manager.scope is None authenticator.set_cr_token_filename('path/to/token') assert authenticator.token_manager.cr_token_filename == 'path/to/token' # Set the IAM profile to None to trigger a validation which will fail, # because both of the profile and ID are None. with pytest.raises(ValueError) as err: authenticator.set_iam_profile_name(None) assert str(err.value) == 'At least one of iam_profile_name or iam_profile_id must be specified.' authenticator.set_iam_profile_id('iam-id-123') assert authenticator.token_manager.iam_profile_id == 'iam-id-123' authenticator.set_iam_profile_name('iam-user-123') assert authenticator.token_manager.iam_profile_name == 'iam-user-123' authenticator.set_client_id_and_secret('tom', 'jerry') assert authenticator.token_manager.client_id == 'tom' assert authenticator.token_manager.client_secret == 'jerry' authenticator.set_scope('scope1 scope2 scope3') assert authenticator.token_manager.scope == 'scope1 scope2 scope3' with pytest.raises(TypeError) as err: authenticator.set_headers('dummy') assert str(err.value) == 'headers must be a dictionary' authenticator.set_headers({'dummy': 'headers'}) assert authenticator.token_manager.headers == {'dummy': 'headers'} with pytest.raises(TypeError) as err: authenticator.set_proxies('dummy') assert str(err.value) == 'proxies must be a dictionary' authenticator.set_proxies({'dummy': 'proxies'}) assert authenticator.token_manager.proxies == {'dummy': 'proxies'} def test_disable_ssl_verification(): authenticator = ContainerAuthenticator(iam_profile_name='iam-user-123', disable_ssl_verification=True) assert authenticator.token_manager.disable_ssl_verification is True authenticator.set_disable_ssl_verification(False) assert authenticator.token_manager.disable_ssl_verification is False def test_invalid_disable_ssl_verification_type(): with pytest.raises(TypeError) as err: authenticator = ContainerAuthenticator(iam_profile_name='iam-user-123', disable_ssl_verification='True') assert str(err.value) == 'disable_ssl_verification must be a bool' authenticator = ContainerAuthenticator(iam_profile_name='iam-user-123') assert authenticator.token_manager.disable_ssl_verification is False with pytest.raises(TypeError) as err: authenticator.set_disable_ssl_verification('True') assert str(err.value) == 'status must be a bool' def test_container_authenticator_with_scope(): authenticator = ContainerAuthenticator(iam_profile_name='iam-user-123', scope='scope1 scope2') assert authenticator is not None assert authenticator.token_manager.scope == 'scope1 scope2' def test_authenticator_validate_failed(): with pytest.raises(ValueError) as err: ContainerAuthenticator() assert str(err.value) == 'At least one of iam_profile_name or iam_profile_id must be specified.' with pytest.raises(ValueError) as err: ContainerAuthenticator(iam_profile_name='iam-user-123', client_id='my_client_id') assert str(err.value) == 'Both client_id and client_secret should be initialized.' with pytest.raises(ValueError) as err: ContainerAuthenticator(iam_profile_name='iam-user-123', client_secret='my_client_secret') assert str(err.value) == 'Both client_id and client_secret should be initialized.' IBM-python-sdk-core-7ebd71d/test/test_container_token_manager.py000066400000000000000000000314071502256645000251220ustar00rootroot00000000000000# coding: utf-8 # Copyright 2021, 2024 IBM All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # pylint: disable=missing-docstring import logging import json import os import time from urllib.parse import parse_qs import responses import pytest from ibm_cloud_sdk_core import ApiException, ContainerTokenManager from ibm_cloud_sdk_core.authenticators import ContainerAuthenticator from .utils.logger_utils import setup_test_logger setup_test_logger(logging.WARNING) # pylint: disable=line-too-long TEST_ACCESS_TOKEN_1 = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImhlbGxvIiwicm9sZSI6InVzZXIiLCJwZXJtaXNzaW9ucyI6WyJhZG1pbmlzdHJhdG9yIiwiZGVwbG95bWVudF9hZG1pbiJdLCJzdWIiOiJoZWxsbyIsImlzcyI6IkpvaG4iLCJhdWQiOiJEU1giLCJ1aWQiOiI5OTkiLCJpYXQiOjE1NjAyNzcwNTEsImV4cCI6MTU2MDI4MTgxOSwianRpIjoiMDRkMjBiMjUtZWUyZC00MDBmLTg2MjMtOGNkODA3MGI1NDY4In0.cIodB4I6CCcX8vfIImz7Cytux3GpWyObt9Gkur5g1QI' TEST_ACCESS_TOKEN_2 = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImtpZCI6IjIzMDQ5ODE1MWMyMTRiNzg4ZGQ5N2YyMmI4NTQxMGE1In0.eyJ1c2VybmFtZSI6ImR1bW15Iiwicm9sZSI6IkFkbWluIiwicGVybWlzc2lvbnMiOlsiYWRtaW5pc3RyYXRvciIsIm1hbmFnZV9jYXRhbG9nIl0sInN1YiI6ImFkbWluIiwiaXNzIjoic3NzIiwiYXVkIjoic3NzIiwidWlkIjoic3NzIiwiaWF0IjozNjAwLCJleHAiOjE2MjgwMDcwODF9.zvUDpgqWIWs7S1CuKv40ERw1IZ5FqSFqQXsrwZJyfRM' TEST_REFRESH_TOKEN = 'Xj7Gle500MachEOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImhlbGxvIiwicm9sZSI6InVzZXIiLCJwZXJtaXNzaW9ucyI6WyJhZG1pbmlzdHJhdG9yIiwiZGVwbG95bWVudF9hZG1pbiJdLCJzdWIiOiJoZWxsbyIsImlzcyI6IkpvaG4iLCJhdWQiOiJEU1giLCJ1aWQiOiI5OTkiLCJpYXQiOjE1NjAyNzcwNTEsImV4cCI6MTU2MDI4MTgxOSwianRpIjoiMDRkMjBiMjUtZWUyZC00MDBmLTg2MjMtOGNkODA3MGI1NDY4In0.cIodB4I6CCcX8vfIImz7Cytux3GpWyObt9Gkur5g1QI' MOCK_IAM_PROFILE_NAME = 'iam-user-123' MOCK_CLIENT_ID = 'client-id-1' MOCK_CLIENT_SECRET = 'client-secret-1' EXPIRATION_WINDOW = 10 cr_token_file = os.path.join(os.path.dirname(__file__), '../resources/cr-token.txt') def _get_current_time() -> int: return int(time.time()) def mock_iam_response(func): """This is decorator function which extends `responses.activate`. This sets up all the mock response stuffs. """ def callback(request): assert request.headers['Accept'] == 'application/json' assert request.headers['Content-Type'] == 'application/x-www-form-urlencoded' payload = parse_qs(request.body) assert payload['cr_token'][0] == 'cr-token-1' assert payload['grant_type'][0] == 'urn:ibm:params:oauth:grant-type:cr-token' assert payload.get('profile_name', [None])[0] or payload.get('profile_id', [None])[0] status_code = 200 scope = payload.get('scope')[0] if payload.get('scope') else None if scope == 'send-second-token': access_token = TEST_ACCESS_TOKEN_2 elif scope == 'status-bad-request': access_token = None status_code = 400 elif scope == 'check-basic-auth': assert request.headers['Authorization'] == 'Basic Y2xpZW50LWlkLTE6Y2xpZW50LXNlY3JldC0x' access_token = TEST_ACCESS_TOKEN_1 else: access_token = TEST_ACCESS_TOKEN_1 response = json.dumps( { 'access_token': access_token, 'token_type': 'Bearer', 'expires_in': 3600, 'expiration': _get_current_time() + 3600, 'refresh_token': TEST_REFRESH_TOKEN, } ) return (status_code, {}, response) @responses.activate def wrapper(): response = responses.CallbackResponse( method=responses.POST, url='https://iam.cloud.ibm.com/identity/token', callback=callback, ) responses.add(response) func() return wrapper @mock_iam_response def test_request_token_auth_default(): iam_url = "https://iam.cloud.ibm.com/identity/token" token_manager = ContainerTokenManager( cr_token_filename=cr_token_file, iam_profile_name=MOCK_IAM_PROFILE_NAME, ) token_manager.request_token() assert len(responses.calls) == 1 assert responses.calls[0].request.url == iam_url assert responses.calls[0].request.headers.get('Authorization') is None assert ( responses.calls[0].request.headers.get('User-Agent').startswith('ibm-python-sdk-core/container-authenticator') ) assert json.loads(responses.calls[0].response.text)['access_token'] == TEST_ACCESS_TOKEN_1 @mock_iam_response def test_request_token_auth_in_ctor(): default_auth_header = 'Basic Yng6Yng=' token_manager = ContainerTokenManager( cr_token_filename=cr_token_file, iam_profile_name=MOCK_IAM_PROFILE_NAME, client_id='foo', client_secret='bar' ) token_manager.request_token() assert len(responses.calls) == 1 assert responses.calls[0].request.headers['Authorization'] != default_auth_header assert json.loads(responses.calls[0].response.text)['access_token'] == TEST_ACCESS_TOKEN_1 assert 'scope' not in responses.calls[0].response.request.body @mock_iam_response def test_request_token_auth_in_ctor_with_scope(): default_auth_header = 'Basic Yng6Yng=' token_manager = ContainerTokenManager( cr_token_filename=cr_token_file, iam_profile_name=MOCK_IAM_PROFILE_NAME, client_id='foo', client_secret='bar', scope='john snow', ) token_manager.request_token() assert len(responses.calls) == 1 assert responses.calls[0].request.headers['Authorization'] != default_auth_header assert json.loads(responses.calls[0].response.text)['access_token'] == TEST_ACCESS_TOKEN_1 assert 'scope=john+snow' in responses.calls[0].response.request.body def test_retrieve_cr_token_success(): token_manager = ContainerTokenManager( cr_token_filename=cr_token_file, ) cr_token = token_manager.retrieve_cr_token() assert cr_token == 'cr-token-1' def test_retrieve_cr_token_with_no_filename_1_success(): token_manager = ContainerTokenManager() # Override the constants with the default locations # just for testing purposes. setattr(token_manager, 'DEFAULT_CR_TOKEN_FILENAME1', cr_token_file) setattr(token_manager, 'DEFAULT_CR_TOKEN_FILENAME2', '') cr_token = token_manager.retrieve_cr_token() assert cr_token == 'cr-token-1' def test_retrieve_cr_token_with_no_filename_2_success(): token_manager = ContainerTokenManager() # Override the constants with the default locations # just for testing purposes. setattr(token_manager, 'DEFAULT_CR_TOKEN_FILENAME1', '') setattr(token_manager, 'DEFAULT_CR_TOKEN_FILENAME2', cr_token_file) cr_token = token_manager.retrieve_cr_token() assert cr_token == 'cr-token-1' def test_retrieve_cr_token_fail(): token_manager = ContainerTokenManager( cr_token_filename='bogus-cr-token-file', ) with pytest.raises(Exception) as err: token_manager.retrieve_cr_token() assert ( str(err.value) == 'Unable to retrieve the CR token: Error reading CR token from file bogus-cr-token-file: [Errno 2] No such file or directory: \'bogus-cr-token-file\'' ) @mock_iam_response def test_get_token_success(): token_manager = ContainerTokenManager( cr_token_filename=cr_token_file, iam_profile_name=MOCK_IAM_PROFILE_NAME, ) access_token = token_manager.access_token assert access_token is None access_token = token_manager.get_token() assert access_token == TEST_ACCESS_TOKEN_1 assert token_manager.access_token == TEST_ACCESS_TOKEN_1 # Verify that the token manager returns the cached value. # Before we call `get_token` again, set the expiration and refresh time # so that we do not fetch a new access token. # This is necessary because we are using a fixed JWT response. token_manager.expire_time = _get_current_time() + 1000 token_manager.refresh_time = _get_current_time() + 1000 token_manager.set_scope('send-second-token') access_token = token_manager.get_token() assert access_token == TEST_ACCESS_TOKEN_1 assert token_manager.access_token == TEST_ACCESS_TOKEN_1 # Force expiration to get the second token. # We'll set the expiration time to be current-time + EXPIRATION_WINDOW (10 secs) # because we want the access token to be considered as "expired" # when we reach the IAM-server reported expiration time minus 10 secs. token_manager.expire_time = _get_current_time() + EXPIRATION_WINDOW access_token = token_manager.get_token() assert access_token == TEST_ACCESS_TOKEN_2 assert token_manager.access_token == TEST_ACCESS_TOKEN_2 @mock_iam_response def test_request_token_success(): token_manager = ContainerTokenManager( cr_token_filename=cr_token_file, iam_profile_name=MOCK_IAM_PROFILE_NAME, ) token_response = token_manager.request_token() assert token_response['access_token'] == TEST_ACCESS_TOKEN_1 @mock_iam_response def test_authenticate_success(): authenticator = ContainerAuthenticator(cr_token_filename=cr_token_file, iam_profile_name=MOCK_IAM_PROFILE_NAME) request = {'headers': {}} authenticator.authenticate(request) assert request['headers']['Authorization'] == 'Bearer ' + TEST_ACCESS_TOKEN_1 # Verify that the token manager returns the cached value. # Before we call `get_token` again, set the expiration and refresh time # so that we do not fetch a new access token. # This is necessary because we are using a fixed JWT response. authenticator.token_manager.expire_time = _get_current_time() + 1000 authenticator.token_manager.refresh_time = _get_current_time() + 1000 authenticator.token_manager.set_scope('send-second-token') authenticator.authenticate(request) assert request['headers']['Authorization'] == 'Bearer ' + TEST_ACCESS_TOKEN_1 # Force expiration to get the second token. # We'll set the expiration time to be current-time + EXPIRATION_WINDOW (10 secs) # because we want the access token to be considered as "expired" # when we reach the IAM-server reported expiration time minus 10 secs. authenticator.token_manager.expire_time = _get_current_time() + EXPIRATION_WINDOW authenticator.authenticate(request) assert request['headers']['Authorization'] == 'Bearer ' + TEST_ACCESS_TOKEN_2 @mock_iam_response def test_authenticate_fail_no_cr_token(): authenticator = ContainerAuthenticator( cr_token_filename='bogus-cr-token-file', iam_profile_name=MOCK_IAM_PROFILE_NAME, url='https://bogus.iam.endpoint', ) request = {'headers': {}} with pytest.raises(Exception) as err: authenticator.authenticate(request) assert ( str(err.value) == 'Unable to retrieve the CR token: Error reading CR token from file bogus-cr-token-file: [Errno 2] No such file or directory: \'bogus-cr-token-file\'' ) @mock_iam_response def test_authenticate_fail_iam(): authenticator = ContainerAuthenticator( cr_token_filename=cr_token_file, iam_profile_name=MOCK_IAM_PROFILE_NAME, scope='status-bad-request' ) request = {'headers': {}} with pytest.raises(ApiException) as err: authenticator.authenticate(request) assert str(err.value) == 'Error: Bad Request, Status code: 400' @mock_iam_response def test_client_id_and_secret(): token_manager = ContainerTokenManager( cr_token_filename=cr_token_file, iam_profile_name=MOCK_IAM_PROFILE_NAME, ) token_manager.set_client_id_and_secret(MOCK_CLIENT_ID, MOCK_CLIENT_SECRET) token_manager.set_scope('check-basic-auth') access_token = token_manager.get_token() assert access_token == TEST_ACCESS_TOKEN_1 @mock_iam_response def test_setter_methods(): token_manager = ContainerTokenManager( cr_token_filename='bogus-cr-token-file', iam_profile_name=MOCK_IAM_PROFILE_NAME, ) assert token_manager.iam_profile_id is None assert token_manager.iam_profile_name == MOCK_IAM_PROFILE_NAME assert token_manager.cr_token_filename == 'bogus-cr-token-file' token_manager.set_iam_profile_id('iam-id-123') token_manager.set_iam_profile_name(None) token_manager.set_cr_token_filename(cr_token_file) assert token_manager.iam_profile_id == 'iam-id-123' assert token_manager.iam_profile_name is None assert token_manager.cr_token_filename == cr_token_file access_token = token_manager.get_token() assert access_token == TEST_ACCESS_TOKEN_1 IBM-python-sdk-core-7ebd71d/test/test_cp4d_authenticator.py000066400000000000000000000151301502256645000240250ustar00rootroot00000000000000# pylint: disable=missing-docstring import logging import json import jwt import pytest import responses from ibm_cloud_sdk_core.authenticators import CloudPakForDataAuthenticator, Authenticator from .utils.logger_utils import setup_test_logger setup_test_logger(logging.WARNING) def test_cp4d_authenticator(): authenticator = CloudPakForDataAuthenticator('my_username', 'my_password', 'http://my_url') assert authenticator is not None assert authenticator.authentication_type() == Authenticator.AUTHTYPE_CP4D assert authenticator.token_manager.url == 'http://my_url/v1/authorize' assert authenticator.token_manager.username == 'my_username' assert authenticator.token_manager.password == 'my_password' assert authenticator.token_manager.disable_ssl_verification is False assert authenticator.token_manager.headers == {'Content-Type': 'application/json'} assert authenticator.token_manager.proxies is None authenticator.set_disable_ssl_verification(True) assert authenticator.token_manager.disable_ssl_verification is True with pytest.raises(TypeError) as err: authenticator.set_headers('dummy') assert str(err.value) == 'headers must be a dictionary' authenticator.set_headers({'dummy': 'headers'}) assert authenticator.token_manager.headers == {'dummy': 'headers'} with pytest.raises(TypeError) as err: authenticator.set_proxies('dummy') assert str(err.value) == 'proxies must be a dictionary' authenticator.set_proxies({'dummy': 'proxies'}) assert authenticator.token_manager.proxies == {'dummy': 'proxies'} def test_disable_ssl_verification(): authenticator = CloudPakForDataAuthenticator( 'my_username', 'my_password', 'http://my_url', disable_ssl_verification=True ) assert authenticator.token_manager.disable_ssl_verification is True authenticator.set_disable_ssl_verification(False) assert authenticator.token_manager.disable_ssl_verification is False def test_invalid_disable_ssl_verification_type(): with pytest.raises(TypeError) as err: authenticator = CloudPakForDataAuthenticator( 'my_username', 'my_password', 'http://my_url', disable_ssl_verification='True' ) assert str(err.value) == 'disable_ssl_verification must be a bool' authenticator = CloudPakForDataAuthenticator('my_username', 'my_password', 'http://my_url') assert authenticator.token_manager.disable_ssl_verification is False with pytest.raises(TypeError) as err: authenticator.set_disable_ssl_verification('True') assert str(err.value) == 'status must be a bool' def test_cp4d_authenticator_validate_failed(): with pytest.raises(ValueError) as err: CloudPakForDataAuthenticator('my_username', None, 'my_url') assert str(err.value) == 'Exactly one of `apikey` or `password` must be specified.' with pytest.raises(ValueError) as err: CloudPakForDataAuthenticator(username='my_username', url='my_url') assert str(err.value) == 'Exactly one of `apikey` or `password` must be specified.' with pytest.raises(ValueError) as err: CloudPakForDataAuthenticator('my_username', None, 'my_url', apikey=None) assert str(err.value) == 'Exactly one of `apikey` or `password` must be specified.' with pytest.raises(ValueError) as err: CloudPakForDataAuthenticator(None, 'my_password', 'my_url') assert str(err.value) == 'The username shouldn\'t be None.' with pytest.raises(ValueError) as err: CloudPakForDataAuthenticator(password='my_password', url='my_url') assert str(err.value) == 'The username shouldn\'t be None.' with pytest.raises(ValueError) as err: CloudPakForDataAuthenticator('my_username', 'my_password', None) assert str(err.value) == 'The url shouldn\'t be None.' with pytest.raises(ValueError) as err: CloudPakForDataAuthenticator(username='my_username', password='my_password') assert str(err.value) == 'The url shouldn\'t be None.' with pytest.raises(ValueError) as err: CloudPakForDataAuthenticator('{my_username}', 'my_password', 'my_url') assert ( str(err.value) == 'The username and password shouldn\'t start or end with curly brackets or quotes. ' 'Please remove any surrounding {, }, or \" characters.' ) with pytest.raises(ValueError) as err: CloudPakForDataAuthenticator('my_username', '{my_password}', 'my_url') assert ( str(err.value) == 'The username and password shouldn\'t start or end with curly brackets or quotes. ' 'Please remove any surrounding {, }, or \" characters.' ) with pytest.raises(ValueError) as err: CloudPakForDataAuthenticator('my_username', 'my_password', '{my_url}') assert ( str(err.value) == 'The url shouldn\'t start or end with curly brackets or quotes. ' 'Please remove any surrounding {, }, or \" characters.' ) @responses.activate def test_get_token(): url = "https://test" access_token_layout = { "username": "dummy", "role": "Admin", "permissions": ["administrator", "manage_catalog"], "sub": "admin", "iss": "sss", "aud": "sss", "uid": "sss", "iat": 1559324664, "exp": 1559324664, } access_token = jwt.encode( access_token_layout, 'secret', algorithm='HS256', headers={'kid': '230498151c214b788dd97f22b85410a5'} ) response = { "token": access_token, "token_type": "Bearer", "expires_in": 3600, "expiration": 1524167011, "refresh_token": "jy4gl91BQ", } responses.add(responses.POST, url + '/v1/authorize', body=json.dumps(response), status=200) auth_headers = {'Host': 'cp4d.cloud.ibm.com:443'} authenticator = CloudPakForDataAuthenticator( 'my_username', 'my_password', url + '/v1/authorize', headers=auth_headers ) # Simulate an SDK API request that needs to be authenticated. request = {'headers': {}} # Trigger the "get token" processing to obtain the access token and add to the "SDK request". authenticator.authenticate(request) # Verify that the "authenticate()" method added the Authorization header assert request['headers']['Authorization'] is not None # Verify that the "get token" call contained the Host header. assert responses.calls[0].request.headers.get('Host') == 'cp4d.cloud.ibm.com:443' # Ensure '/v1/authorize' is added to the url if omitted authenticator = CloudPakForDataAuthenticator('my_username', 'my_password', url) request = {'headers': {}} authenticator.authenticate(request) assert request['headers']['Authorization'] is not None IBM-python-sdk-core-7ebd71d/test/test_cp4d_token_manager.py000066400000000000000000000046671502256645000240020ustar00rootroot00000000000000# coding: utf-8 # Copyright 2019, 2024 IBM All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # pylint: disable=missing-docstring import logging import json import time import jwt import responses from ibm_cloud_sdk_core import CP4DTokenManager from .utils.logger_utils import setup_test_logger setup_test_logger(logging.WARNING) @responses.activate def test_request_token(): url = "https://test" now = time.time() access_token_layout = { "username": "dummy", "role": "Admin", "permissions": ["administrator", "manage_catalog"], "sub": "admin", "iss": "sss", "aud": "sss", "uid": "sss", "iat": now, "exp": now + 3600, } access_token = jwt.encode( access_token_layout, 'secret', algorithm='HS256', headers={'kid': '230498151c214b788dd97f22b85410a5'} ) response = { "token": access_token, } responses.add(responses.POST, url + '/v1/authorize', body=json.dumps(response), status=200) token_manager = CP4DTokenManager("username", "password", url) token_manager.set_disable_ssl_verification(True) token = token_manager.get_token() assert len(responses.calls) == 1 assert responses.calls[0].request.url == url + '/v1/authorize' assert responses.calls[0].request.headers.get('User-Agent').startswith('ibm-python-sdk-core/cp4d-authenticator') assert token == access_token token_manager = CP4DTokenManager("username", "password", url + '/v1/authorize') token = token_manager.get_token() assert len(responses.calls) == 2 assert responses.calls[1].request.url == url + '/v1/authorize' assert token == access_token token_manager = CP4DTokenManager(username="username", apikey="fake_api_key", url=url + '/v1/authorize') token = token_manager.get_token() assert len(responses.calls) == 3 assert responses.calls[2].request.url == url + '/v1/authorize' assert token == access_token IBM-python-sdk-core-7ebd71d/test/test_detailed_response.py000066400000000000000000000043171502256645000237370ustar00rootroot00000000000000# coding=utf-8 # pylint: disable=missing-docstring import json import responses import requests from ibm_cloud_sdk_core import DetailedResponse def clean(val): """Eliminate all whitespace and convert single to double quotes""" return val.translate(str.maketrans('', '', ' \n\t\r')).replace("'", "\"") @responses.activate def test_detailed_response_dict(): responses.add( responses.GET, 'https://test.com', status=200, body=json.dumps({'foobar': 'baz'}), content_type='application/json', ) mock_response = requests.get('https://test.com', timeout=None) detailed_response = DetailedResponse( response=mock_response.json(), headers=mock_response.headers, status_code=mock_response.status_code ) assert detailed_response is not None assert detailed_response.get_result() == {'foobar': 'baz'} assert detailed_response.get_headers() == {'Content-Type': 'application/json'} assert detailed_response.get_status_code() == 200 response_str = clean(str(detailed_response)) assert clean(str(detailed_response.get_result())) in response_str # assert clean(str(detailed_response.get_headers())) in response_str assert clean(str(detailed_response.get_status_code())) in response_str @responses.activate def test_detailed_response_list(): responses.add( responses.GET, 'https://test.com', status=200, body=json.dumps(['foobar', 'baz']), content_type='application/json', ) mock_response = requests.get('https://test.com', timeout=None) detailed_response = DetailedResponse( response=mock_response.json(), headers=mock_response.headers, status_code=mock_response.status_code ) assert detailed_response is not None assert detailed_response.get_result() == ['foobar', 'baz'] assert detailed_response.get_headers() == {'Content-Type': 'application/json'} assert detailed_response.get_status_code() == 200 response_str = clean(str(detailed_response)) assert clean(str(detailed_response.get_result())) in response_str # assert clean(str(detailed_response.get_headers())) in response_str assert clean(str(detailed_response.get_status_code())) in response_str IBM-python-sdk-core-7ebd71d/test/test_get_authenticator.py000066400000000000000000000506561502256645000237660ustar00rootroot00000000000000# pylint: disable=missing-docstring # coding: utf-8 # Copyright 2019, 2024 IBM All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import logging import os import pytest from ibm_cloud_sdk_core import get_authenticator_from_environment from ibm_cloud_sdk_core.authenticators import Authenticator, BasicAuthenticator, IAMAuthenticator from .utils.logger_utils import setup_test_logger setup_test_logger(logging.ERROR) # pylint: disable=too-many-statements def test_get_authenticator_from_credential_file(): file_path = os.path.join(os.path.dirname(__file__), '../resources/ibm-credentials-iam.env') os.environ['IBM_CREDENTIALS_FILE'] = file_path authenticator = get_authenticator_from_environment('ibm watson') assert authenticator is not None assert authenticator.authentication_type() == Authenticator.AUTHTYPE_IAM assert authenticator.token_manager.apikey == '5678efgh' assert authenticator.token_manager.url == 'https://iam.cloud.ibm.com' assert authenticator.token_manager.client_id is None assert authenticator.token_manager.client_secret is None assert authenticator.token_manager.disable_ssl_verification is False assert authenticator.token_manager.scope is None del os.environ['IBM_CREDENTIALS_FILE'] file_path = os.path.join(os.path.dirname(__file__), '../resources/ibm-credentials-iam-assume.env') os.environ['IBM_CREDENTIALS_FILE'] = file_path authenticator = get_authenticator_from_environment('service 1') assert authenticator is not None assert authenticator.authentication_type() == Authenticator.AUTHTYPE_IAM_ASSUME assert authenticator.token_manager.iam_delegate.apikey == 'my-api-key' assert authenticator.token_manager.iam_profile_id == 'iam-profile-1' assert authenticator.token_manager.url == 'https://iam.cloud.ibm.com' assert authenticator.token_manager.client_id is None assert authenticator.token_manager.client_secret is None assert authenticator.token_manager.disable_ssl_verification is False assert authenticator.token_manager.scope is None del os.environ['IBM_CREDENTIALS_FILE'] file_path = os.path.join(os.path.dirname(__file__), '../resources/ibm-credentials-basic.env') os.environ['IBM_CREDENTIALS_FILE'] = file_path authenticator = get_authenticator_from_environment('watson') assert authenticator is not None assert authenticator.authentication_type() == Authenticator.AUTHTYPE_BASIC assert authenticator.username == 'my_username' del os.environ['IBM_CREDENTIALS_FILE'] file_path = os.path.join(os.path.dirname(__file__), '../resources/ibm-credentials-container.env') os.environ['IBM_CREDENTIALS_FILE'] = file_path authenticator = get_authenticator_from_environment('service 1') assert authenticator is not None assert authenticator.authentication_type() == Authenticator.AUTHTYPE_CONTAINER assert authenticator.token_manager.cr_token_filename == 'crtoken.txt' assert authenticator.token_manager.iam_profile_name == 'iam-user-123' assert authenticator.token_manager.iam_profile_id == 'iam-id-123' assert authenticator.token_manager.url == 'https://iamhost/iam/api' assert authenticator.token_manager.scope == 'scope1' assert authenticator.token_manager.client_id == 'iam-client-123' assert authenticator.token_manager.client_secret == 'iam-secret-123' assert authenticator.token_manager.disable_ssl_verification is True authenticator = get_authenticator_from_environment('service 2') assert authenticator is not None assert authenticator.authentication_type() == Authenticator.AUTHTYPE_CONTAINER assert authenticator.token_manager.cr_token_filename is None assert authenticator.token_manager.iam_profile_name == 'iam-user-123' assert authenticator.token_manager.iam_profile_id is None assert authenticator.token_manager.url == 'https://iam.cloud.ibm.com' assert authenticator.token_manager.scope is None assert authenticator.token_manager.client_id is None assert authenticator.token_manager.client_secret is None assert authenticator.token_manager.disable_ssl_verification is False del os.environ['IBM_CREDENTIALS_FILE'] file_path = os.path.join(os.path.dirname(__file__), '../resources/ibm-credentials-cp4d.env') os.environ['IBM_CREDENTIALS_FILE'] = file_path authenticator = get_authenticator_from_environment('watson') assert authenticator is not None assert authenticator.authentication_type() == Authenticator.AUTHTYPE_CP4D assert authenticator.token_manager.username == 'my_username' assert authenticator.token_manager.password == 'my_password' assert authenticator.token_manager.url == 'https://my_url/v1/authorize' assert authenticator.token_manager.apikey is None assert authenticator.token_manager.disable_ssl_verification is False del os.environ['IBM_CREDENTIALS_FILE'] file_path = os.path.join(os.path.dirname(__file__), '../resources/ibm-credentials-no-auth.env') os.environ['IBM_CREDENTIALS_FILE'] = file_path authenticator = get_authenticator_from_environment('watson') assert authenticator is not None assert authenticator.authentication_type() == Authenticator.AUTHTYPE_NOAUTH del os.environ['IBM_CREDENTIALS_FILE'] file_path = os.path.join(os.path.dirname(__file__), '../resources/ibm-credentials-bearer.env') os.environ['IBM_CREDENTIALS_FILE'] = file_path authenticator = get_authenticator_from_environment('watson') assert authenticator is not None assert authenticator.authentication_type() == Authenticator.AUTHTYPE_BEARERTOKEN assert authenticator.bearer_token is not None del os.environ['IBM_CREDENTIALS_FILE'] file_path = os.path.join(os.path.dirname(__file__), '../resources/ibm-credentials.env') os.environ['IBM_CREDENTIALS_FILE'] = file_path authenticator = get_authenticator_from_environment('service_1') assert authenticator is not None assert authenticator.authentication_type() == Authenticator.AUTHTYPE_IAM assert authenticator.token_manager.apikey == 'V4HXmoUtMjohnsnow=KotN' assert authenticator.token_manager.client_id == 'somefake========id' assert authenticator.token_manager.client_secret == '==my-client-secret==' assert authenticator.token_manager.url == 'https://iamhost/iam/api=' assert authenticator.token_manager.scope is None del os.environ['IBM_CREDENTIALS_FILE'] file_path = os.path.join(os.path.dirname(__file__), '../resources/ibm-credentials-vpc.env') os.environ['IBM_CREDENTIALS_FILE'] = file_path authenticator = get_authenticator_from_environment('service1') assert authenticator is not None assert authenticator.authentication_type() == Authenticator.AUTHTYPE_VPC assert authenticator.token_manager.iam_profile_crn is None assert authenticator.token_manager.iam_profile_id is None assert authenticator.token_manager.url == 'http://169.254.169.254' del os.environ['IBM_CREDENTIALS_FILE'] file_path = os.path.join(os.path.dirname(__file__), '../resources/ibm-credentials-vpc.env') os.environ['IBM_CREDENTIALS_FILE'] = file_path authenticator = get_authenticator_from_environment('service2') assert authenticator is not None assert authenticator.authentication_type() == Authenticator.AUTHTYPE_VPC assert authenticator.token_manager.iam_profile_crn == 'crn:iam-profile1' assert authenticator.token_manager.iam_profile_id is None assert authenticator.token_manager.url == 'http://vpc.imds.com/api' del os.environ['IBM_CREDENTIALS_FILE'] file_path = os.path.join(os.path.dirname(__file__), '../resources/ibm-credentials-vpc.env') os.environ['IBM_CREDENTIALS_FILE'] = file_path authenticator = get_authenticator_from_environment('service3') assert authenticator is not None assert authenticator.authentication_type() == Authenticator.AUTHTYPE_VPC assert authenticator.token_manager.iam_profile_crn is None assert authenticator.token_manager.iam_profile_id == 'iam-profile1-id' assert authenticator.token_manager.url == 'http://169.254.169.254' del os.environ['IBM_CREDENTIALS_FILE'] file_path = os.path.join(os.path.dirname(__file__), '../resources/ibm-credentials-mcsp.env') os.environ['IBM_CREDENTIALS_FILE'] = file_path authenticator = get_authenticator_from_environment('service1') assert authenticator is not None assert authenticator.authentication_type() == Authenticator.AUTHTYPE_MCSP assert authenticator.token_manager.url == 'https://mcsp.ibm.com' assert authenticator.token_manager.apikey == 'my-api-key' del os.environ['IBM_CREDENTIALS_FILE'] file_path = os.path.join(os.path.dirname(__file__), '../resources/ibm-credentials-mcspv2.env') os.environ['IBM_CREDENTIALS_FILE'] = file_path authenticator = get_authenticator_from_environment('service1') assert authenticator is not None assert authenticator.authentication_type() == Authenticator.AUTHTYPE_MCSPV2 assert authenticator.token_manager.url == 'https://mcspv2.ibm.com' assert authenticator.token_manager.apikey == 'my-api-key' assert authenticator.token_manager.scope_collection_type == 'accounts' assert authenticator.token_manager.scope_id == 'global_account' assert authenticator.token_manager.include_builtin_actions is False assert authenticator.token_manager.include_custom_actions is False assert authenticator.token_manager.include_roles is True assert authenticator.token_manager.prefix_roles is False assert authenticator.token_manager.caller_ext_claim is None assert authenticator.token_manager.disable_ssl_verification is False assert authenticator.token_manager.headers is None assert authenticator.token_manager.proxies is None del os.environ['IBM_CREDENTIALS_FILE'] file_path = os.path.join(os.path.dirname(__file__), '../resources/ibm-credentials-mcspv2.env') os.environ['IBM_CREDENTIALS_FILE'] = file_path authenticator = get_authenticator_from_environment('service2') assert authenticator is not None assert authenticator.authentication_type() == Authenticator.AUTHTYPE_MCSPV2 assert authenticator.token_manager.url == 'https://mcspv2.ibm.com' assert authenticator.token_manager.apikey == 'my-api-key' assert authenticator.token_manager.scope_collection_type == 'accounts' assert authenticator.token_manager.scope_id == 'global_account' assert authenticator.token_manager.include_builtin_actions is True assert authenticator.token_manager.include_custom_actions is True assert authenticator.token_manager.include_roles is False assert authenticator.token_manager.prefix_roles is True assert authenticator.token_manager.caller_ext_claim == {"productID": "prod-123"} assert authenticator.token_manager.disable_ssl_verification is True assert authenticator.token_manager.headers is None assert authenticator.token_manager.proxies is None del os.environ['IBM_CREDENTIALS_FILE'] file_path = os.path.join(os.path.dirname(__file__), '../resources/ibm-credentials-mcspv2.env') os.environ['IBM_CREDENTIALS_FILE'] = file_path with pytest.raises(Exception) as err: authenticator = get_authenticator_from_environment('error1') assert ( str(err.value) == 'An error occurred while unmarshalling the CALLER_EXT_CLAIM configuration property: {not json}' ) del os.environ['IBM_CREDENTIALS_FILE'] def test_get_authenticator_from_credential_file_scope(): file_path = os.path.join(os.path.dirname(__file__), '../resources/ibm-credentials.env') os.environ['IBM_CREDENTIALS_FILE'] = file_path authenticator = get_authenticator_from_environment('service_2') assert authenticator is not None assert authenticator.authentication_type() == Authenticator.AUTHTYPE_IAM assert authenticator.token_manager.apikey == 'V4HXmoUtMjohnsnow=KotN' assert authenticator.token_manager.client_id == 'somefake========id' assert authenticator.token_manager.client_secret == '==my-client-secret==' assert authenticator.token_manager.url == 'https://iamhost/iam/api=' assert authenticator.token_manager.scope == 'A B C D' del os.environ['IBM_CREDENTIALS_FILE'] def test_get_authenticator_from_env_variables(): os.environ['TEST_APIKEY'] = '5678efgh' authenticator = get_authenticator_from_environment('test') assert authenticator is not None assert authenticator.authentication_type() == Authenticator.AUTHTYPE_IAM assert authenticator.token_manager.apikey == '5678efgh' del os.environ['TEST_APIKEY'] os.environ['TEST_IAM_PROFILE_ID'] = 'iam-profile-id1' authenticator = get_authenticator_from_environment('test') assert authenticator is not None assert authenticator.authentication_type() == Authenticator.AUTHTYPE_CONTAINER assert authenticator.token_manager.iam_profile_id == 'iam-profile-id1' del os.environ['TEST_IAM_PROFILE_ID'] os.environ['SERVICE_1_APIKEY'] = 'V4HXmoUtMjohnsnow=KotN' authenticator = get_authenticator_from_environment('service_1') assert authenticator is not None assert authenticator.authentication_type() == Authenticator.AUTHTYPE_IAM assert authenticator.token_manager.apikey == 'V4HXmoUtMjohnsnow=KotN' del os.environ['SERVICE_1_APIKEY'] os.environ['SERVICE_2_APIKEY'] = 'johnsnow' os.environ['SERVICE_2_SCOPE'] = 'A B C D' authenticator = get_authenticator_from_environment('service_2') assert authenticator is not None assert authenticator.token_manager.apikey == 'johnsnow' assert authenticator.token_manager.scope == 'A B C D' del os.environ['SERVICE_2_APIKEY'] del os.environ['SERVICE_2_SCOPE'] os.environ['SERVICE3_AUTH_TYPE'] = 'mCsP' os.environ['SERVICE3_AUTH_URL'] = 'https://mcsp.ibm.com' os.environ['SERVICE3_APIKEY'] = 'my-api-key' authenticator = get_authenticator_from_environment('service3') assert authenticator is not None assert authenticator.authentication_type() == Authenticator.AUTHTYPE_MCSP assert authenticator.token_manager.apikey == 'my-api-key' assert authenticator.token_manager.url == 'https://mcsp.ibm.com' del os.environ['SERVICE3_APIKEY'] del os.environ['SERVICE3_AUTH_TYPE'] del os.environ['SERVICE3_AUTH_URL'] def test_vcap_credentials(): vcap_services = '{"test":[{"credentials":{ \ "url":"https://gateway.watsonplatform.net/compare-comply/api",\ "username":"bogus username", \ "password":"bogus password"}}]}' os.environ['VCAP_SERVICES'] = vcap_services authenticator = get_authenticator_from_environment('test') assert isinstance(authenticator, BasicAuthenticator) assert authenticator.authentication_type() == Authenticator.AUTHTYPE_BASIC assert authenticator.username == 'bogus username' assert authenticator.password == 'bogus password' del os.environ['VCAP_SERVICES'] vcap_services = '{"test":[{"credentials":{ \ "url":"https://gateway.watsonplatform.net/compare-comply/api",\ "apikey":"bogus apikey"}}]}' os.environ['VCAP_SERVICES'] = vcap_services authenticator = get_authenticator_from_environment('test') assert isinstance(authenticator, IAMAuthenticator) assert authenticator.authentication_type() == Authenticator.AUTHTYPE_IAM assert authenticator.token_manager.apikey == 'bogus apikey' del os.environ['VCAP_SERVICES'] vcap_services = '{"test":[{"credentials":{ \ "url":"https://gateway.watsonplatform.net/compare-comply/api",\ "iam_apikey":"bogus apikey"}}]}' os.environ['VCAP_SERVICES'] = vcap_services authenticator = get_authenticator_from_environment('test') assert isinstance(authenticator, IAMAuthenticator) assert authenticator.authentication_type() == Authenticator.AUTHTYPE_IAM assert authenticator.token_manager.apikey == 'bogus apikey' del os.environ['VCAP_SERVICES'] vcap_services = '{"test":[{"name": "testname",\ "credentials":{ \ "url":"https://gateway.watsonplatform.net/compare-comply/api",\ "username":"bogus username", \ "password":"bogus password"}}]}' os.environ['VCAP_SERVICES'] = vcap_services authenticator = get_authenticator_from_environment('testname') assert isinstance(authenticator, BasicAuthenticator) assert authenticator.authentication_type() == Authenticator.AUTHTYPE_BASIC assert authenticator.username == 'bogus username' assert authenticator.password == 'bogus password' del os.environ['VCAP_SERVICES'] def test_vcap_credentials_2(): vcap_services = '{\ "test":[{"name": "testname",\ "credentials":{ \ "url":"https://gateway.watsonplatform.net/compare-comply/api",\ "username":"bogus username2", \ "password":"bogus password2"}},\ {"name": "othertestname",\ "credentials":{ \ "url":"https://gateway.watsonplatform.net/compare-comply/api",\ "username":"bogus username3", \ "password":"bogus password3"}}],\ "testname":[{"name": "nottestname",\ "credentials":{ \ "url":"https://gateway.watsonplatform.net/compare-comply/api",\ "username":"bogus username", \ "password":"bogus password"}}],\ "equals_sign_test":[{"name": "equals_sign_test",\ "credentials":{ \ "iam_apikey": "V4HXmoUtMjohnsnow=KotN",\ "iam_apikey_description": "Auto generated apikey...",\ "iam_apikey_name": "auto-generated-apikey-111-222-333",\ "iam_role_crn": "crn:v1:bluemix:public:iam::::serviceRole:Manager",\ "iam_serviceid_crn": "crn:v1:staging:public:iam-identity::a/::serviceid:ServiceID-1234",\ "url": "https://gateway.watsonplatform.net/testService",\ "auth_url": "https://iamhost/iam/api="}}]}' os.environ['VCAP_SERVICES'] = vcap_services authenticator = get_authenticator_from_environment('testname') assert isinstance(authenticator, BasicAuthenticator) assert authenticator.username == 'bogus username2' assert authenticator.password == 'bogus password2' authenticator = get_authenticator_from_environment('equals_sign_test') assert isinstance(authenticator, IAMAuthenticator) assert authenticator.token_manager.apikey == 'V4HXmoUtMjohnsnow=KotN' del os.environ['VCAP_SERVICES'] vcap_services = '{"test":[{\ "credentials":{ \ "url":"https://gateway.watsonplatform.net/compare-comply/api",\ "username":"bogus username", \ "password":"bogus password"}},\ {"credentials":{ \ "url":"https://gateway.watsonplatform.net/compare-comply/api",\ "username":"bogus username2", \ "password":"bogus password2"}}\ ]}' os.environ['VCAP_SERVICES'] = vcap_services authenticator = get_authenticator_from_environment('test') assert isinstance(authenticator, BasicAuthenticator) assert authenticator.username == 'bogus username' assert authenticator.password == 'bogus password' del os.environ['VCAP_SERVICES'] vcap_services = '{"first":[],\ "test":[{"credentials":{ \ "url":"https://gateway.watsonplatform.net/compare-comply/api",\ "username":"bogus username", \ "password":"bogus password"}}],\ "last":[]}' os.environ['VCAP_SERVICES'] = vcap_services authenticator = get_authenticator_from_environment('test') assert isinstance(authenticator, BasicAuthenticator) assert authenticator.username == 'bogus username' assert authenticator.password == 'bogus password' del os.environ['VCAP_SERVICES'] vcap_services = '{"test":[],\ "last":[]}' os.environ['VCAP_SERVICES'] = vcap_services authenticator = get_authenticator_from_environment('test') assert authenticator is None del os.environ['VCAP_SERVICES'] def test_multi_word_service_name(): os.environ['PERSONALITY_INSIGHTS_APIKEY'] = '5678efgh' authenticator = get_authenticator_from_environment('personality-insights') assert authenticator is not None assert authenticator.authentication_type() == Authenticator.AUTHTYPE_IAM assert authenticator.token_manager.apikey == '5678efgh' del os.environ['PERSONALITY_INSIGHTS_APIKEY'] IBM-python-sdk-core-7ebd71d/test/test_http_adapter.py000066400000000000000000000064641502256645000227320ustar00rootroot00000000000000# pylint: disable=missing-docstring import logging import os from ssl import PROTOCOL_TLSv1_1, PROTOCOL_TLSv1_2 import pytest from requests import exceptions from ibm_cloud_sdk_core.base_service import BaseService from ibm_cloud_sdk_core.authenticators import NoAuthAuthenticator from .utils.logger_utils import setup_test_logger from .utils.http_utils import local_server setup_test_logger(logging.ERROR) # The certificate files that are used in this tests are generated by this command: # pylint: disable=line-too-long,pointless-string-statement """ openssl req -x509 -out test_ssl.crt -keyout test_ssl.key \ -newkey rsa:2048 -nodes -sha256 -days 36500 \ -subj '/CN=localhost' -extensions EXT -config <( \ printf "[dn]\nCN=localhost\n[req]\ndistinguished_name = dn\n[EXT]\nsubjectAltName=DNS:localhost\nkeyUsage=digitalSignature\nextendedKeyUsage=serverAuth") """ # Load the certificate and the key files. cert = os.path.join(os.path.dirname(__file__), '../resources/test_ssl.crt') key = os.path.join(os.path.dirname(__file__), '../resources/test_ssl.key') @local_server(3333, PROTOCOL_TLSv1_1, cert, key) def test_tls_v1_1(): service = BaseService(service_url='https://localhost:3333', authenticator=NoAuthAuthenticator()) prepped = service.prepare_request('GET', url='/') # The following request should fail, because the server will try # to use TLS v1.1 but that's not allowed in our client. with pytest.raises(Exception) as exception: service.send(prepped, verify=cert) # Errors can be differ based on the Python version. assert exception.type is exceptions.SSLError or exception.type is exceptions.ConnectionError @local_server(3334, PROTOCOL_TLSv1_2, cert, key) def test_tls_v1_2(): service = BaseService(service_url='https://localhost:3334', authenticator=NoAuthAuthenticator()) # First call the server with the default configuration. # It should fail due to the self-signed SSL cert. assert service.disable_ssl_verification is False prepped = service.prepare_request('GET', url='/') with pytest.raises(exceptions.SSLError, match='certificate verify failed: self-signed certificate'): res = service.send(prepped) # Next configure it to validate by using our local certificate. Should raise no exception. res = service.send(prepped, verify=cert) assert res is not None # Now disable the SSL verification. The request shouldn't raise any issue. service.set_disable_ssl_verification(True) assert service.disable_ssl_verification is True prepped = service.prepare_request('GET', url='/') res = service.send(prepped) assert res is not None # Lastly, try with an external URL. # This test case is mainly here to reproduce the regression # in the `requests` package that was introduced in `2.32.3`. # More details on the issue can be found here: https://github.com/psf/requests/issues/6730 service = BaseService(service_url='https://cloud.ibm.com', authenticator=NoAuthAuthenticator()) assert service.disable_ssl_verification is False ssl_context = service.http_adapter.poolmanager.connection_pool_kw.get("ssl_context") assert ssl_context is not None assert len(ssl_context.get_ca_certs()) > 0 prepped = service.prepare_request('GET', url='/status') res = service.send(prepped) assert res is not None IBM-python-sdk-core-7ebd71d/test/test_iam_assume_authenticator.py000066400000000000000000000175151502256645000253270ustar00rootroot00000000000000# pylint: disable=missing-docstring import logging import json import time import jwt import pytest import responses from ibm_cloud_sdk_core.authenticators import Authenticator, IAMAssumeAuthenticator from .utils.logger_utils import setup_test_logger setup_test_logger(logging.WARNING) def test_iam_assume_authenticator(): authenticator = IAMAssumeAuthenticator(apikey='my_apikey', iam_profile_crn='crn:iam-profile:123') assert authenticator is not None assert authenticator.authentication_type() == Authenticator.AUTHTYPE_IAM_ASSUME assert authenticator.token_manager.url == 'https://iam.cloud.ibm.com' assert authenticator.token_manager.client_id is None assert authenticator.token_manager.client_secret is None assert authenticator.token_manager.disable_ssl_verification is False assert authenticator.token_manager.headers is None assert authenticator.token_manager.proxies is None assert authenticator.token_manager.iam_delegate.apikey == 'my_apikey' assert authenticator.token_manager.iam_profile_id is None assert authenticator.token_manager.iam_profile_crn == 'crn:iam-profile:123' assert authenticator.token_manager.iam_profile_name is None assert authenticator.token_manager.iam_account_id is None assert authenticator.token_manager.scope is None def test_iam_assume_authenticator_disable_ssl_wrong_type(): with pytest.raises(TypeError) as err: IAMAssumeAuthenticator( apikey='my_apikey', iam_profile_crn='crn:iam-profile:123', disable_ssl_verification='yes' ) assert str(err.value) == 'disable_ssl_verification must be a bool' def test_iam_assume_authenticator_validate_failed(): with pytest.raises(ValueError) as err: IAMAssumeAuthenticator(None) assert str(err.value) == 'The apikey shouldn\'t be None.' with pytest.raises(ValueError) as err: IAMAssumeAuthenticator('{apikey}') assert ( str(err.value) == 'The apikey shouldn\'t start or end with curly brackets or quotes. ' 'Please remove any surrounding {, }, or \" characters.' ) with pytest.raises(ValueError) as err: IAMAssumeAuthenticator( 'my_apikey', iam_profile_id='my_profile_id', iam_profile_crn='my_profile_crn', iam_profile_name='my_profile_name', iam_account_id='my_account_id', ) assert ( str(err.value) == 'Exactly one of `iam_profile_id`, `iam_profile_crn`, or `iam_profile_name` must be specified.' ) with pytest.raises(ValueError) as err: IAMAssumeAuthenticator( 'my_apikey', iam_profile_id='my_profile_id', iam_profile_crn='my_profile_crn', iam_profile_name='my_profile_name', ) assert ( str(err.value) == 'Exactly one of `iam_profile_id`, `iam_profile_crn`, or `iam_profile_name` must be specified.' ) with pytest.raises(ValueError) as err: IAMAssumeAuthenticator('my_apikey', iam_profile_id='my_profile_id', iam_profile_crn='my_profile_crn') assert ( str(err.value) == 'Exactly one of `iam_profile_id`, `iam_profile_crn`, or `iam_profile_name` must be specified.' ) with pytest.raises(ValueError) as err: IAMAssumeAuthenticator('my_apikey', iam_profile_id='my_profile_id', iam_profile_name='my_profile_name') assert ( str(err.value) == 'Exactly one of `iam_profile_id`, `iam_profile_crn`, or `iam_profile_name` must be specified.' ) with pytest.raises(ValueError) as err: IAMAssumeAuthenticator('my_apikey', iam_profile_crn='my_profile_crn', iam_profile_name='my_profile_name') assert ( str(err.value) == 'Exactly one of `iam_profile_id`, `iam_profile_crn`, or `iam_profile_name` must be specified.' ) with pytest.raises(ValueError) as err: IAMAssumeAuthenticator('my_apikey', iam_profile_name='my_profile_name') assert str(err.value) == '`iam_profile_name` and `iam_account_id` must be provided together, or not at all.' with pytest.raises(ValueError) as err: IAMAssumeAuthenticator('my_apikey', iam_account_id='my_account_id') assert ( str(err.value) == 'Exactly one of `iam_profile_id`, `iam_profile_crn`, or `iam_profile_name` must be specified.' ) with pytest.raises(ValueError) as err: IAMAssumeAuthenticator('my_apikey', client_id='my_client_id') assert str(err.value) == 'Both client_id and client_secret should be initialized.' with pytest.raises(ValueError) as err: IAMAssumeAuthenticator('my_apikey', client_secret='my_client_secret') assert str(err.value) == 'Both client_id and client_secret should be initialized.' @responses.activate def test_get_token(): current_time = time.time() url = "https://iam.cloud.ibm.com/identity/token" access_token_layout = { "username": "dummy", "role": "Admin", "permissions": ["administrator", "manage_catalog"], "sub": "admin", "iss": "sss", "aud": "sss", "uid": "sss", "iat": current_time, "exp": current_time + 3600, } access_token = jwt.encode( access_token_layout, 'secret', algorithm='HS256', headers={'kid': '230498151c214b788dd97f22b85410a5'} ) response = { "access_token": access_token, "token_type": "Bearer", "expires_in": 3600, "expiration": current_time, "refresh_token": "jy4gl91BQ", } responses.add(responses.POST, url=url, body=json.dumps(response), status=200) auth_headers = {'Host': 'iam.cloud.ibm.com:443'} authenticator = IAMAssumeAuthenticator('my_apikey', iam_profile_id='my_profile_id', headers=auth_headers) # Simulate an SDK API request that needs to be authenticated. request = {'headers': {}} # Trigger the "get token" processing to obtain the access token and add to the "SDK request". authenticator.authenticate(request) # Verify that the "authenticate()" method added the Authorization header assert request['headers']['Authorization'] is not None # Verify that the "get token" call contained the Host header. assert len(responses.calls) == 2 assert responses.calls[0].request.headers.get('Host') == 'iam.cloud.ibm.com:443' assert 'profile_id=my_profile_id' in responses.calls[1].request.body def test_multiple_iam_assume_authenticators(): authenticator_1 = IAMAssumeAuthenticator('my_apikey', iam_profile_id='my_profile_id') assert authenticator_1.token_manager.iam_delegate.request_payload['apikey'] == 'my_apikey' authenticator_2 = IAMAssumeAuthenticator('my_other_apikey', iam_profile_id='my_profile_id_2') assert authenticator_2.token_manager.iam_delegate.request_payload['apikey'] == 'my_other_apikey' assert authenticator_1.token_manager.iam_delegate.request_payload['apikey'] == 'my_apikey' def test_iam_assume_authenticator_unsupported_methods(): authenticator = IAMAssumeAuthenticator('my_apikey', iam_profile_id='my_profile_id') with pytest.raises(AttributeError) as err: authenticator.set_scope('my_scope') assert str(err.value) == "'IAMAssumeAuthenticator' has no attribute 'set_scope'" with pytest.raises(AttributeError) as err: authenticator.set_client_id_and_secret('my_client_id', 'my_client_secret') assert str(err.value) == "'IAMAssumeAuthenticator' has no attribute 'set_client_id_and_secret'" with pytest.raises(AttributeError) as err: authenticator.set_headers({}) assert str(err.value) == "'IAMAssumeAuthenticator' has no attribute 'set_headers'" with pytest.raises(AttributeError) as err: authenticator.set_proxies({}) assert str(err.value) == "'IAMAssumeAuthenticator' has no attribute 'set_proxies'" with pytest.raises(AttributeError) as err: authenticator.set_disable_ssl_verification(True) assert str(err.value) == "'IAMAssumeAuthenticator' has no attribute 'set_disable_ssl_verification'" IBM-python-sdk-core-7ebd71d/test/test_iam_assume_token_manager.py000066400000000000000000000225361502256645000252660ustar00rootroot00000000000000# coding: utf-8 # Copyright 2019, 2024 IBM All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # pylint: disable=missing-docstring import json import logging import time import urllib import jwt import pytest import responses from ibm_cloud_sdk_core import IAMAssumeTokenManager from ibm_cloud_sdk_core.api_exception import ApiException from .utils.logger_utils import setup_test_logger setup_test_logger(logging.ERROR) def _get_current_time() -> int: return int(time.time()) IAM_URL = "https://iam.cloud.ibm.com/identity/token" MY_PROFILE_ID = 'my-profile-id' MY_PROFILE_CRN = 'my-profile-crn' MY_PROFILE_NAME = 'my-profile-name' MY_ACCOUNT_ID = 'my-account_id' # The base layout of an access token. ACCESS_TOKEN_LAYOUT = { "username": "dummy", "role": "Admin", "permissions": ["administrator", "manage_catalog"], "sub": "admin", "iss": "sss", "aud": "sss", "uid": "sss", "iat": _get_current_time(), "exp": _get_current_time() + 3600, } # Create two different access tokens by using different secrets for the encoding. ACCESS_TOKEN = jwt.encode( ACCESS_TOKEN_LAYOUT, 'secret', algorithm='HS256', headers={'kid': '230498151c214b788dd97f22b85410a5'} ) OTHER_ACCESS_TOKEN = jwt.encode( ACCESS_TOKEN_LAYOUT, 'other_secret', algorithm='HS256', headers={'kid': '230498151c214b788dd97f22b85410a5'} ) # Create a base response and serialize it to a JSON string to avoid doing that in each test case. BASE_RESPONSE = { "access_token": ACCESS_TOKEN, "token_type": "Bearer", "expires_in": 3600, "expiration": _get_current_time() + 3600, "refresh_token": "not_available", } BASE_RESPONSE_JSON = json.dumps(BASE_RESPONSE) # Create a second base response just like we did above, but use the other access token. OTHER_BASE_RESPONSE = BASE_RESPONSE.copy() OTHER_BASE_RESPONSE['access_token'] = OTHER_ACCESS_TOKEN OTHER_BASE_RESPONSE_JSON = json.dumps(OTHER_BASE_RESPONSE) def request_callback(request): """Parse the form data, and return a response based on the `grant_type` value.""" form_data = urllib.parse.unquote(request.body) params = dict(param.split('=') for param in form_data.split('&')) if params.get('grant_type') == 'urn:ibm:params:oauth:grant-type:apikey': return (200, {}, BASE_RESPONSE_JSON) if params.get('grant_type') == 'urn:ibm:params:oauth:grant-type:assume': return (200, {}, OTHER_BASE_RESPONSE_JSON) raise ApiException(400) @responses.activate def test_request_token_with_profile_id(): responses.add_callback(responses.POST, url=IAM_URL, callback=request_callback) token_manager = IAMAssumeTokenManager("apikey", iam_profile_id=MY_PROFILE_ID) # Make sure we don't have an access token yet. assert token_manager.request_payload.get('access_token') is None token_manager.request_token() # Now the access token should be set along with the profile ID. assert token_manager.request_payload.get('access_token') == ACCESS_TOKEN assert token_manager.request_payload.get('profile_id') == MY_PROFILE_ID assert token_manager.request_payload.get('profile_crn') is None assert token_manager.request_payload.get('profile_name') is None assert token_manager.request_payload.get('account_id') is None @responses.activate def test_request_token_with_profile_crn(): responses.add_callback(responses.POST, url=IAM_URL, callback=request_callback) token_manager = IAMAssumeTokenManager("apikey", iam_profile_crn=MY_PROFILE_CRN) # Make sure we don't have an access token yet. assert token_manager.request_payload.get('access_token') is None token_manager.request_token() # Now the access token should be set along with the profile ID. assert token_manager.request_payload.get('access_token') == ACCESS_TOKEN assert token_manager.request_payload.get('profile_id') is None assert token_manager.request_payload.get('profile_crn') == MY_PROFILE_CRN assert token_manager.request_payload.get('profile_name') is None assert token_manager.request_payload.get('account_id') is None @responses.activate def test_request_token_with_profile_name_and_account_id(): responses.add_callback(responses.POST, url=IAM_URL, callback=request_callback) token_manager = IAMAssumeTokenManager("apikey", iam_profile_name=MY_PROFILE_NAME, iam_account_id=MY_ACCOUNT_ID) # Make sure we don't have an access token yet. assert token_manager.request_payload.get('access_token') is None token_manager.request_token() # Now the access token should be set along with the profile ID. assert token_manager.request_payload.get('access_token') == ACCESS_TOKEN assert token_manager.request_payload.get('profile_id') is None assert token_manager.request_payload.get('profile_crn') is None assert token_manager.request_payload.get('profile_name') == MY_PROFILE_NAME assert token_manager.request_payload.get('account') == MY_ACCOUNT_ID @responses.activate def test_request_token_uses_the_correct_grant_types(): responses.add(responses.POST, url=IAM_URL, body=BASE_RESPONSE_JSON, status=200) token_manager = IAMAssumeTokenManager("apikey", iam_profile_id='my_profile_id') token_manager.request_token() assert token_manager.request_payload.get('grant_type') == 'urn:ibm:params:oauth:grant-type:assume' assert token_manager.iam_delegate.request_payload.get('grant_type') == 'urn:ibm:params:oauth:grant-type:apikey' @responses.activate def test_request_token_uses_the_correct_headers(): responses.add_callback(responses.POST, url=IAM_URL, callback=request_callback) token_manager = IAMAssumeTokenManager("apikey", iam_profile_id='my_profile_id') token_manager.request_token() assert len(responses.calls) == 2 assert responses.calls[0].request.headers.get('User-Agent').startswith('ibm-python-sdk-core/iam-authenticator') assert ( responses.calls[1].request.headers.get('User-Agent').startswith('ibm-python-sdk-core/iam-assume-authenticator') ) @responses.activate def test_get_token(): responses.add_callback(responses.POST, url=IAM_URL, callback=request_callback) token_manager = IAMAssumeTokenManager("apikey", iam_profile_id=MY_PROFILE_ID) # Make sure we don't have an access token yet. assert token_manager.request_payload.get('access_token') is None access_token = token_manager.get_token() # Now the access token should be set along with the profile ID. assert token_manager.request_payload.get('access_token') == ACCESS_TOKEN assert token_manager.request_payload.get('profile_id') == MY_PROFILE_ID assert token_manager.request_payload.get('profile_crn') is None assert token_manager.request_payload.get('profile_name') is None assert token_manager.request_payload.get('account_id') is None # The final result should be the other access token, which belong to the "assume" request. assert access_token == OTHER_ACCESS_TOKEN # Make sure `refresh_token` is None. assert token_manager.refresh_token is None @responses.activate def test_correct_properties_used_in_calls(): responses.add_callback(responses.POST, url=IAM_URL, callback=request_callback) token_manager = IAMAssumeTokenManager( "apikey", iam_profile_id=MY_PROFILE_ID, client_id='my_client_id', client_secret='my_client_secret', scope='my_scope', ) token_manager.get_token() # Make sure the all properties were used in the first request via the IAM delegate, assert responses.calls[0].request.headers.get('Authorization') == 'Basic bXlfY2xpZW50X2lkOm15X2NsaWVudF9zZWNyZXQ=' assert 'scope=my_scope' in responses.calls[0].request.body # but those were not included in the second, assume type request. assert responses.calls[1].request.headers.get('Authorization') is None assert 'scope=my_scope' not in responses.calls[1].request.body @responses.activate def test_iam_assume_authenticator_unsupported_methods(): token_manager = IAMAssumeTokenManager('my_apikey', iam_profile_id='my_profile_id') with pytest.raises(AttributeError) as err: token_manager.set_scope('my_scope') assert str(err.value) == "'IAMAssumeTokenManager' has no attribute 'set_scope'" with pytest.raises(AttributeError) as err: token_manager.set_client_id_and_secret('my_client_id', 'my_client_secret') assert str(err.value) == "'IAMAssumeTokenManager' has no attribute 'set_client_id_and_secret'" with pytest.raises(AttributeError) as err: token_manager.set_headers({}) assert str(err.value) == "'IAMAssumeTokenManager' has no attribute 'set_headers'" with pytest.raises(AttributeError) as err: token_manager.set_proxies({}) assert str(err.value) == "'IAMAssumeTokenManager' has no attribute 'set_proxies'" with pytest.raises(AttributeError) as err: token_manager.set_disable_ssl_verification(True) assert str(err.value) == "'IAMAssumeTokenManager' has no attribute 'set_disable_ssl_verification'" IBM-python-sdk-core-7ebd71d/test/test_iam_authenticator.py000066400000000000000000000131451502256645000237450ustar00rootroot00000000000000# pylint: disable=missing-docstring import logging import json import jwt import pytest import responses from ibm_cloud_sdk_core.authenticators import IAMAuthenticator, Authenticator from .utils.logger_utils import setup_test_logger setup_test_logger(logging.WARNING) def test_iam_authenticator(): authenticator = IAMAuthenticator(apikey='my_apikey') assert authenticator is not None assert authenticator.authentication_type() == Authenticator.AUTHTYPE_IAM assert authenticator.token_manager.url == 'https://iam.cloud.ibm.com' assert authenticator.token_manager.client_id is None assert authenticator.token_manager.client_secret is None assert authenticator.token_manager.disable_ssl_verification is False assert authenticator.token_manager.headers is None assert authenticator.token_manager.proxies is None assert authenticator.token_manager.apikey == 'my_apikey' assert authenticator.token_manager.scope is None authenticator.set_client_id_and_secret('tom', 'jerry') assert authenticator.token_manager.client_id == 'tom' assert authenticator.token_manager.client_secret == 'jerry' authenticator.set_scope('scope1 scope2 scope3') assert authenticator.token_manager.scope == 'scope1 scope2 scope3' with pytest.raises(TypeError) as err: authenticator.set_headers('dummy') assert str(err.value) == 'headers must be a dictionary' authenticator.set_headers({'dummy': 'headers'}) assert authenticator.token_manager.headers == {'dummy': 'headers'} with pytest.raises(TypeError) as err: authenticator.set_proxies('dummy') assert str(err.value) == 'proxies must be a dictionary' authenticator.set_proxies({'dummy': 'proxies'}) assert authenticator.token_manager.proxies == {'dummy': 'proxies'} authenticator.set_disable_ssl_verification(True) assert authenticator.token_manager.disable_ssl_verification def test_disable_ssl_verification(): authenticator = IAMAuthenticator(apikey='my_apikey', disable_ssl_verification=True) assert authenticator.token_manager.disable_ssl_verification is True authenticator.set_disable_ssl_verification(False) assert authenticator.token_manager.disable_ssl_verification is False def test_invalid_disable_ssl_verification_type(): with pytest.raises(TypeError) as err: authenticator = IAMAuthenticator(apikey='my_apikey', disable_ssl_verification='True') assert str(err.value) == 'disable_ssl_verification must be a bool' authenticator = IAMAuthenticator(apikey='my_apikey') assert authenticator.token_manager.disable_ssl_verification is False with pytest.raises(TypeError) as err: authenticator.set_disable_ssl_verification('True') assert str(err.value) == 'status must be a bool' def test_iam_authenticator_with_scope(): authenticator = IAMAuthenticator(apikey='my_apikey', scope='scope1 scope2') assert authenticator is not None assert authenticator.token_manager.scope == 'scope1 scope2' def test_iam_authenticator_validate_failed(): with pytest.raises(ValueError) as err: IAMAuthenticator(None) assert str(err.value) == 'The apikey shouldn\'t be None.' with pytest.raises(ValueError) as err: IAMAuthenticator('{apikey}') assert ( str(err.value) == 'The apikey shouldn\'t start or end with curly brackets or quotes. ' 'Please remove any surrounding {, }, or \" characters.' ) with pytest.raises(ValueError) as err: IAMAuthenticator('my_apikey', client_id='my_client_id') assert str(err.value) == 'Both client_id and client_secret should be initialized.' with pytest.raises(ValueError) as err: IAMAuthenticator('my_apikey', client_secret='my_client_secret') assert str(err.value) == 'Both client_id and client_secret should be initialized.' @responses.activate def test_get_token(): url = "https://iam.cloud.ibm.com/identity/token" access_token_layout = { "username": "dummy", "role": "Admin", "permissions": ["administrator", "manage_catalog"], "sub": "admin", "iss": "sss", "aud": "sss", "uid": "sss", "iat": 1559324664, "exp": 1559324664, } access_token = jwt.encode( access_token_layout, 'secret', algorithm='HS256', headers={'kid': '230498151c214b788dd97f22b85410a5'} ) response = { "access_token": access_token, "token_type": "Bearer", "expires_in": 3600, "expiration": 1524167011, "refresh_token": "jy4gl91BQ", } responses.add(responses.POST, url=url, body=json.dumps(response), status=200) auth_headers = {'Host': 'iam.cloud.ibm.com:443'} authenticator = IAMAuthenticator('my_apikey', headers=auth_headers) # Simulate an SDK API request that needs to be authenticated. request = {'headers': {}} # Trigger the "get token" processing to obtain the access token and add to the "SDK request". authenticator.authenticate(request) # Verify that the "authenticate()" method added the Authorization header assert request['headers']['Authorization'] is not None # Verify that the "get token" call contained the Host header. assert responses.calls[0].request.headers.get('Host') == 'iam.cloud.ibm.com:443' def test_multiple_iam_authenticators(): authenticator_1 = IAMAuthenticator('my_apikey') assert authenticator_1.token_manager.request_payload['apikey'] == 'my_apikey' authenticator_2 = IAMAuthenticator('my_other_apikey') assert authenticator_2.token_manager.request_payload['apikey'] == 'my_other_apikey' assert authenticator_1.token_manager.request_payload['apikey'] == 'my_apikey' IBM-python-sdk-core-7ebd71d/test/test_iam_token_manager.py000066400000000000000000000406111502256645000237030ustar00rootroot00000000000000# coding: utf-8 # Copyright 2019, 2024 IBM All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # pylint: disable=missing-docstring import logging import os import time import jwt import pytest import responses from ibm_cloud_sdk_core import IAMTokenManager, ApiException, get_authenticator_from_environment from .utils.logger_utils import setup_test_logger setup_test_logger(logging.ERROR) # pylint: disable=line-too-long TEST_ACCESS_TOKEN_1 = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImhlbGxvIiwicm9sZSI6InVzZXIiLCJwZXJtaXNzaW9ucyI6WyJhZG1pbmlzdHJhdG9yIiwiZGVwbG95bWVudF9hZG1pbiJdLCJzdWIiOiJoZWxsbyIsImlzcyI6IkpvaG4iLCJhdWQiOiJEU1giLCJ1aWQiOiI5OTkiLCJpYXQiOjE1NjAyNzcwNTEsImV4cCI6MTU2MDI4MTgxOSwianRpIjoiMDRkMjBiMjUtZWUyZC00MDBmLTg2MjMtOGNkODA3MGI1NDY4In0.cIodB4I6CCcX8vfIImz7Cytux3GpWyObt9Gkur5g1QI' TEST_ACCESS_TOKEN_2 = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImtpZCI6IjIzMDQ5ODE1MWMyMTRiNzg4ZGQ5N2YyMmI4NTQxMGE1In0.eyJ1c2VybmFtZSI6ImR1bW15Iiwicm9sZSI6IkFkbWluIiwicGVybWlzc2lvbnMiOlsiYWRtaW5pc3RyYXRvciIsIm1hbmFnZV9jYXRhbG9nIl0sInN1YiI6ImFkbWluIiwiaXNzIjoic3NzIiwiYXVkIjoic3NzIiwidWlkIjoic3NzIiwiaWF0IjozNjAwLCJleHAiOjE2MjgwMDcwODF9.zvUDpgqWIWs7S1CuKv40ERw1IZ5FqSFqQXsrwZJyfRM' TEST_REFRESH_TOKEN = 'Xj7Gle500MachEOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImhlbGxvIiwicm9sZSI6InVzZXIiLCJwZXJtaXNzaW9ucyI6WyJhZG1pbmlzdHJhdG9yIiwiZGVwbG95bWVudF9hZG1pbiJdLCJzdWIiOiJoZWxsbyIsImlzcyI6IkpvaG4iLCJhdWQiOiJEU1giLCJ1aWQiOiI5OTkiLCJpYXQiOjE1NjAyNzcwNTEsImV4cCI6MTU2MDI4MTgxOSwianRpIjoiMDRkMjBiMjUtZWUyZC00MDBmLTg2MjMtOGNkODA3MGI1NDY4In0.cIodB4I6CCcX8vfIImz7Cytux3GpWyObt9Gkur5g1QI' EXPIRATION_WINDOW = 10 def _get_current_time() -> int: return int(time.time()) def get_access_token() -> str: access_token_layout = { "username": "dummy", "role": "Admin", "permissions": ["administrator", "manage_catalog"], "sub": "admin", "iss": "sss", "aud": "sss", "uid": "sss", "iat": 3600, "exp": int(time.time()), } access_token = jwt.encode( access_token_layout, 'secret', algorithm='HS256', headers={'kid': '230498151c214b788dd97f22b85410a5'} ) return access_token @responses.activate def test_request_token_auth_default(): iam_url = "https://iam.cloud.ibm.com/identity/token" response = """{ "access_token": "oAeisG8yqPY7sFR_x66Z15", "token_type": "Bearer", "expires_in": 3600, "expiration": 1524167011, "refresh_token": "jy4gl91BQ" }""" responses.add(responses.POST, url=iam_url, body=response, status=200) token_manager = IAMTokenManager("apikey") token_manager.request_token() assert len(responses.calls) == 1 assert responses.calls[0].request.url == iam_url assert responses.calls[0].request.headers.get('Authorization') is None assert responses.calls[0].request.headers.get('User-Agent').startswith('ibm-python-sdk-core/iam-authenticator') assert responses.calls[0].response.text == response @responses.activate def test_request_token_auth_in_ctor(): iam_url = "https://iam.cloud.ibm.com/identity/token" response = """{ "access_token": "oAeisG8yqPY7sFR_x66Z15", "token_type": "Bearer", "expires_in": 3600, "expiration": 1524167011, "refresh_token": "jy4gl91BQ" }""" default_auth_header = 'Basic Yng6Yng=' responses.add(responses.POST, url=iam_url, body=response, status=200) token_manager = IAMTokenManager("apikey", url=iam_url, client_id='foo', client_secret='bar') token_manager.request_token() assert len(responses.calls) == 1 assert responses.calls[0].request.url == iam_url assert responses.calls[0].request.headers['Authorization'] != default_auth_header assert responses.calls[0].response.text == response assert 'scope' not in responses.calls[0].response.request.body @responses.activate def test_request_token_auth_in_ctor_with_scope(): iam_url = "https://iam.cloud.ibm.com/identity/token" response = """{ "access_token": "oAeisG8yqPY7sFR_x66Z15", "token_type": "Bearer", "expires_in": 3600, "expiration": 1524167011, "refresh_token": "jy4gl91BQ" }""" default_auth_header = 'Basic Yng6Yng=' responses.add(responses.POST, url=iam_url, body=response, status=200) token_manager = IAMTokenManager("apikey", url=iam_url, client_id='foo', client_secret='bar', scope='john snow') token_manager.request_token() assert len(responses.calls) == 1 assert responses.calls[0].request.url == iam_url assert responses.calls[0].request.headers['Authorization'] != default_auth_header assert responses.calls[0].response.text == response assert 'scope=john+snow' in responses.calls[0].response.request.body @responses.activate def test_request_token_unsuccessful(): iam_url = "https://iam.cloud.ibm.com/identity/token" response = """{ "context": { "requestId": "38a0e9c226d94764820d92aa623eb0f6", "requestType": "incoming.Identity_Token", "userAgent": "ibm-python-sdk-core-1.0.0", "url": "https://iam.cloud.ibm.com", "instanceId": "iamid-4.5-6788-90b137c-75f48695b5-kl4wx", "threadId": "169de5", "host": "iamid-4.5-6788-90b137c-75f48695b5-kl4wx", "startTime": "29.10.2019 12:31:00:300 GMT", "endTime": "29.10.2019 12:31:00:381 GMT", "elapsedTime": "81", "locale": "en_US", "clusterName": "iam-id-prdal12-8brn" }, "errorCode": "BXNIM0415E", "errorMessage": "Provided API key could not be found" } """ responses.add(responses.POST, url=iam_url, body=response, status=400) token_manager = IAMTokenManager("apikey") with pytest.raises(ApiException): token_manager.request_token() assert len(responses.calls) == 1 assert responses.calls[0].request.url == iam_url assert responses.calls[0].response.text == response @responses.activate def test_request_token_auth_in_ctor_client_id_only(): iam_url = "https://iam.cloud.ibm.com/identity/token" response = """{ "access_token": "oAeisG8yqPY7sFR_x66Z15", "token_type": "Bearer", "expires_in": 3600, "expiration": 1524167011, "refresh_token": "jy4gl91BQ" }""" responses.add(responses.POST, url=iam_url, body=response, status=200) token_manager = IAMTokenManager("iam_apikey", url=iam_url, client_id='foo') token_manager.request_token() assert len(responses.calls) == 1 assert responses.calls[0].request.url == iam_url assert responses.calls[0].request.headers.get('Authorization') is None assert responses.calls[0].response.text == response assert 'scope' not in responses.calls[0].response.request.body @responses.activate def test_request_token_auth_in_ctor_secret_only(): iam_url = "https://iam.cloud.ibm.com/identity/token" response = """{ "access_token": "oAeisG8yqPY7sFR_x66Z15", "token_type": "Bearer", "expires_in": 3600, "expiration": 1524167011, "refresh_token": "jy4gl91BQ" }""" responses.add(responses.POST, url=iam_url, body=response, status=200) token_manager = IAMTokenManager("iam_apikey", url=iam_url, client_id=None, client_secret='bar') token_manager.request_token() assert len(responses.calls) == 1 assert responses.calls[0].request.url == iam_url assert responses.calls[0].request.headers.get('Authorization') is None assert responses.calls[0].response.text == response assert 'scope' not in responses.calls[0].response.request.body @responses.activate def test_request_token_auth_in_setter(): iam_url = "https://iam.cloud.ibm.com/identity/token" response = """{ "access_token": "oAeisG8yqPY7sFR_x66Z15", "token_type": "Bearer", "expires_in": 3600, "expiration": 1524167011, "refresh_token": "jy4gl91BQ" }""" default_auth_header = 'Basic Yng6Yng=' responses.add(responses.POST, url=iam_url, body=response, status=200) token_manager = IAMTokenManager("iam_apikey") token_manager.set_client_id_and_secret('foo', 'bar') token_manager.request_token() assert len(responses.calls) == 1 assert responses.calls[0].request.url == iam_url assert responses.calls[0].request.headers['Authorization'] != default_auth_header assert responses.calls[0].response.text == response assert 'scope' not in responses.calls[0].response.request.body @responses.activate def test_request_token_auth_in_setter_client_id_only(): iam_url = "https://iam.cloud.ibm.com/identity/token" response = """{ "access_token": "oAeisG8yqPY7sFR_x66Z15", "token_type": "Bearer", "expires_in": 3600, "expiration": 1524167011, "refresh_token": "jy4gl91BQ" }""" responses.add(responses.POST, url=iam_url, body=response, status=200) token_manager = IAMTokenManager("iam_apikey") token_manager.set_client_id_and_secret('foo', None) token_manager.request_token() assert len(responses.calls) == 1 assert responses.calls[0].request.url == iam_url assert responses.calls[0].request.headers.get('Authorization') is None assert responses.calls[0].response.text == response assert 'scope' not in responses.calls[0].response.request.body @responses.activate def test_request_token_auth_in_setter_secret_only(): iam_url = "https://iam.cloud.ibm.com/identity/token" response = """{ "access_token": "oAeisG8yqPY7sFR_x66Z15", "token_type": "Bearer", "expires_in": 3600, "expiration": 1524167011, "refresh_token": "jy4gl91BQ" }""" responses.add(responses.POST, url=iam_url, body=response, status=200) token_manager = IAMTokenManager("iam_apikey") token_manager.set_client_id_and_secret(None, 'bar') token_manager.set_headers({'user': 'header'}) token_manager.request_token() assert len(responses.calls) == 1 assert responses.calls[0].request.url == iam_url assert responses.calls[0].request.headers.get('Authorization') is None assert responses.calls[0].response.text == response assert 'scope' not in responses.calls[0].response.request.body @responses.activate def test_request_token_auth_in_setter_scope(): iam_url = "https://iam.cloud.ibm.com/identity/token" response = """{ "access_token": "oAeisG8yqPY7sFR_x66Z15", "token_type": "Bearer", "expires_in": 3600, "expiration": 1524167011, "refresh_token": "jy4gl91BQ" }""" responses.add(responses.POST, url=iam_url, body=response, status=200) token_manager = IAMTokenManager("iam_apikey") token_manager.set_client_id_and_secret(None, 'bar') token_manager.set_headers({'user': 'header'}) token_manager.set_scope('john snow') token_manager.request_token() assert len(responses.calls) == 1 assert responses.calls[0].request.url == iam_url assert responses.calls[0].request.headers.get('Authorization') is None assert responses.calls[0].response.text == response assert 'scope=john+snow' in responses.calls[0].response.request.body @responses.activate def test_get_token_success(): iam_url = "https://iam.cloud.ibm.com/identity/token" # Create two mock responses with different access tokens. response1 = """{ "access_token": "%s", "token_type": "Bearer", "expires_in": 3600, "expiration": 1600003600, "refresh_token": "jy4gl91BQ" }""" % ( TEST_ACCESS_TOKEN_1 ) response2 = """{ "access_token": "%s", "token_type": "Bearer", "expires_in": 3600, "expiration": 1600007200, "refresh_token": "jy4gl91BQ" }""" % ( TEST_ACCESS_TOKEN_2 ) token_manager = IAMTokenManager("iam_apikey") access_token = token_manager.access_token assert access_token is None responses.add(responses.POST, url=iam_url, body=response1, status=200) access_token = token_manager.get_token() assert access_token == TEST_ACCESS_TOKEN_1 assert token_manager.access_token == TEST_ACCESS_TOKEN_1 # Verify that the token manager returns the cached value. # Before we call `get_token` again, set the expiration and refresh time # so that we do not fetch a new access token. # This is necessary because we are using a fixed JWT response. token_manager.expire_time = _get_current_time() + 1000 token_manager.refresh_time = _get_current_time() + 1000 access_token = token_manager.get_token() assert access_token == TEST_ACCESS_TOKEN_1 assert token_manager.access_token == TEST_ACCESS_TOKEN_1 # Force expiration to get the second token. # We'll set the expiration time to be current-time + EXPIRATION_WINDOW (10 secs) # because we want the access token to be considered as "expired" # when we reach the IAM-server reported expiration time minus 10 secs. responses.add(responses.POST, url=iam_url, body=response2, status=200) token_manager.expire_time = _get_current_time() + EXPIRATION_WINDOW token_manager.refresh_time = _get_current_time() + 1000 access_token = token_manager.get_token() assert access_token == TEST_ACCESS_TOKEN_2 assert token_manager.access_token == TEST_ACCESS_TOKEN_2 @responses.activate def test_get_refresh_token(): iam_url = "https://iam.cloud.ibm.com/identity/token" access_token_str = get_access_token() response = """{ "access_token": "%s", "token_type": "Bearer", "expires_in": 3600, "expiration": 1524167011, "refresh_token": "jy4gl91BQ" }""" % ( access_token_str ) responses.add(responses.POST, url=iam_url, body=response, status=200) token_manager = IAMTokenManager("iam_apikey") token_manager.get_token() assert len(responses.calls) == 2 assert token_manager.refresh_token == "jy4gl91BQ" # # In order to run the following integration test with a live IAM server: # # 1. Create file "iamtest.env" in the project root. # It should look like this: # IAMTEST1_AUTH_URL= e.g. https://iam.cloud.ibm.com # IAMTEST1_AUTH_TYPE=iam # IAMTEST1_APIKEY= # IAMTEST2_AUTH_URL= e.g. https://iam.test.cloud.ibm.com # IAMTEST2_AUTH_TYPE=iam # IAMTEST2_APIKEY= # IAMTEST2_CLIENT_ID= # IAMTEST2_CLIENT_SECRET= # # 2. Comment out the "@pytest.mark.skip" decorator below. # # 3. Run this command: # python3 -m pytest -s test -k "test_iam_live_token_server" # (or just run tests like normal and this test function will be invoked) # @pytest.mark.skip(reason="avoid integration test in automated builds") def test_iam_live_token_server(): # Get two iam authenticators from the environment. # "iamtest1" uses the production IAM token server # "iamtest2" uses the staging IAM token server os.environ['IBM_CREDENTIALS_FILE'] = "iamtest.env" # Test "iamtest1" service auth1 = get_authenticator_from_environment("iamtest1") assert auth1 is not None assert auth1.token_manager is not None assert auth1.token_manager.url is not None request = {'method': "GET"} request["url"] = "" request["headers"] = {} assert auth1.token_manager.refresh_token is None auth1.authenticate(request) assert request.get("headers") is not None assert request["headers"].get("Authorization") is not None assert "Bearer " in request["headers"].get("Authorization") # Test "iamtest2" service auth2 = get_authenticator_from_environment("iamtest2") assert auth2 is not None assert auth2.token_manager is not None assert auth2.token_manager.url is not None request = {'method': "GET"} request["url"] = "" request["headers"] = {} assert auth2.token_manager.refresh_token is None auth2.authenticate(request) assert auth2.token_manager.refresh_token is not None assert request.get("headers") is not None assert request["headers"].get("Authorization") is not None assert "Bearer " in request["headers"].get("Authorization") # print('Refresh token: ', auth2.token_manager.refresh_token) IBM-python-sdk-core-7ebd71d/test/test_jwt_token_manager.py000066400000000000000000000070711502256645000237440ustar00rootroot00000000000000# pylint: disable=missing-docstring,protected-access,abstract-class-instantiated import logging import time import threading from typing import Optional import jwt import pytest from ibm_cloud_sdk_core import JWTTokenManager, DetailedResponse from .utils.logger_utils import setup_test_logger setup_test_logger(logging.ERROR) class JWTTokenManagerMockImpl(JWTTokenManager): def __init__(self, url: Optional[str] = None, access_token: Optional[str] = None) -> None: self.url = url self.access_token = access_token self.request_count = 0 # just for tests to see how many times request was called super().__init__(url, disable_ssl_verification=access_token, token_name='access_token') def request_token(self) -> DetailedResponse: self.request_count += 1 current_time = int(time.time()) token_layout = { "username": "dummy", "role": "Admin", "permissions": ["administrator", "manage_catalog"], "sub": "admin", "iss": "sss", "aud": "sss", "uid": "sss", "iat": current_time, "exp": current_time + 3600, } access_token = jwt.encode( token_layout, 'secret', algorithm='HS256', headers={'kid': '230498151c214b788dd97f22b85410a5'} ) response = { "access_token": access_token, "token_type": "Bearer", "expires_in": 3600, "expiration": current_time + 3600, "refresh_token": "jy4gl91BQ", "from_token_manager": True, } time.sleep(0.5) return response def _get_current_time() -> int: return int(time.time()) def test_get_token(): url = "https://iam.cloud.ibm.com/identity/token" token_manager = JWTTokenManagerMockImpl(url) old_token = token_manager.get_token() assert token_manager.token_info.get('expires_in') == 3600 assert token_manager._is_token_expired() is False token_manager.token_info = { "access_token": "old_dummy", "token_type": "Bearer", "expires_in": 3600, "expiration": time.time(), "refresh_token": "jy4gl91BQ", } token = token_manager.get_token() assert token == old_token # expired token: token_manager.expire_time = _get_current_time() - 300 token = token_manager.get_token() assert token != "old_dummy" assert token_manager.request_count == 2 def test_paced_get_token(): url = "https://iam.cloud.ibm.com/identity/token" token_manager = JWTTokenManagerMockImpl(url) threads = [] for _ in range(10): thread = threading.Thread(target=token_manager.get_token) thread.start() threads.append(thread) for thread in threads: thread.join() assert token_manager.request_count == 1 def test_is_token_expired(): token_manager = JWTTokenManagerMockImpl(None, access_token=None) assert token_manager._is_token_expired() is True token_manager.expire_time = _get_current_time() + 3600 assert token_manager._is_token_expired() is False token_manager.expire_time = _get_current_time() - 3600 assert token_manager._is_token_expired() def test_abstract_class_instantiation(): with pytest.raises(TypeError, match=r"^Can't instantiate abstract class JWTTokenManager.*$"): JWTTokenManager(None) def test_disable_ssl_verification(): token_manager = JWTTokenManagerMockImpl('https://iam.cloud.ibm.com/identity/token') token_manager.set_disable_ssl_verification(True) assert token_manager.disable_ssl_verification is True IBM-python-sdk-core-7ebd71d/test/test_logger.py000066400000000000000000000110741502256645000215230ustar00rootroot00000000000000# coding: utf-8 # Copyright 2024 IBM All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # pylint: disable=missing-docstring import logging from ibm_cloud_sdk_core import base_service from ibm_cloud_sdk_core.authenticators import NoAuthAuthenticator from ibm_cloud_sdk_core.logger import LoggingFilter from .utils.http_utils import local_server def test_redact_secrets(): redact_secrets = LoggingFilter.redact_secrets assert "secret" not in redact_secrets("Authorization: Bearer secret") assert "secret" not in redact_secrets("Authorization: Basic secret") assert "secret" not in redact_secrets("X-Authorization: secret") assert "secret" not in redact_secrets("ApIKey=secret") assert "secret" not in redact_secrets("ApI_Key=secret") assert "secret" not in redact_secrets("passCode=secret") assert "secret" not in redact_secrets("PASSword=secret") assert "secret" not in redact_secrets("toKen=secret") assert "secret" not in redact_secrets("client_id=secret") assert "secret" not in redact_secrets("client_x509_cert_url=secret") assert "secret" not in redact_secrets("client_id=secret") assert "secret" not in redact_secrets("key=secret") assert "secret" not in redact_secrets("project_id=secret") assert "DaSecret" not in redact_secrets("secret=DaSecret") assert "secret" not in redact_secrets("subscriptionId=secret") assert "secret" not in redact_secrets("tenantId=secret") assert "secret" not in redact_secrets("thumbprint=secret") assert "secret" not in redact_secrets("token_uri=secret") assert "secret" not in redact_secrets('xxx "apIKEy": "secret",xxx') assert "secret" not in redact_secrets('xxx "apI_KEy": "secret",xxx') assert "secret" not in redact_secrets('xxx "pAsSCoDe": "secret",xxx') assert "secret" not in redact_secrets('xxx "passWORD": "secret",xxx') assert "secret" not in redact_secrets('xxx {"token": "secret"},xxx') assert "secret" not in redact_secrets('xxx "aadClientId": "secret",xxx') assert "secret" not in redact_secrets('xxx "aadClientSecret": "secret",xxx') assert "secret" not in redact_secrets('xxx "auth": "secret",xxx') assert "secret" not in redact_secrets('xxx "auth_provider_x509_cert_url": "secret",xxx') assert "secret" not in redact_secrets('xxx "auth_uri": "secret",xxx') assert "secret" not in redact_secrets('xxx "client_email": "secret",xxx') # Now use a real-world example, to validate the correct behavior. assert ( redact_secrets( 'GET / HTTP/1.1\r\nHost: localhost:3335\r\n' + 'User-Agent: ibm-python-sdk-core-3.20.6 os.name=Darwin os.version=23.6.0 python.version=3.11.10\r\n' + 'Accept-Encoding: gzip, deflate\r\nAccept: */*\r\nAuthorization: token-foo-bar-123\r\n' + 'Connection: keep-alive\r\n\r\n' ) == 'GET / HTTP/1.1\r\nHost: localhost:3335\r\n' + 'User-Agent: ibm-python-sdk-core-3.20.6 os.name=Darwin os.version=23.6.0 python.version=3.11.10\r\n' + 'Accept-Encoding: gzip, deflate\r\nAccept: */*\r\nAuthorization: [redacted]\r\nConnection: keep-alive\r\n\r\n' ) # Simulate a real-world scenario. @local_server(3335) def test_redact_secrets_log(caplog): # Since we use a real BaseService here, we need to set the logging level # to DEBUG in its module, to simulate the real behavior. original_logging_level = base_service.logger.level base_service.logger.setLevel(logging.DEBUG) try: service = base_service.BaseService(service_url="http://localhost:3335", authenticator=NoAuthAuthenticator()) prepped = service.prepare_request('GET', url='/', headers={'Authorization': 'token-foo-bar-123'}) res = service.send(prepped) except Exception as ex: raise ex finally: # And now we restore the logger's level to the original value. base_service.logger.setLevel(original_logging_level) assert res is not None # Make sure the request has been logged and the token is redacted. assert "Authorization" in caplog.text assert "token" not in caplog.text IBM-python-sdk-core-7ebd71d/test/test_mcsp_authenticator.py000066400000000000000000000161121502256645000241360ustar00rootroot00000000000000# pylint: disable=missing-docstring import logging import json import time import jwt import pytest import responses from ibm_cloud_sdk_core.authenticators import MCSPAuthenticator, Authenticator from .utils.logger_utils import setup_test_logger setup_test_logger(logging.ERROR) OPERATION_PATH = '/siusermgr/api/1.0/apikeys/token' MOCK_URL = 'https://mcsp.ibm.com' def test_mcsp_authenticator(): authenticator = MCSPAuthenticator('my-api-key', MOCK_URL) assert authenticator is not None assert authenticator.authentication_type() == Authenticator.AUTHTYPE_MCSP assert authenticator.token_manager.url == MOCK_URL assert authenticator.token_manager.disable_ssl_verification is False assert authenticator.token_manager.headers == { 'Accept': 'application/json', 'Content-Type': 'application/json', } assert authenticator.token_manager.proxies is None authenticator.set_disable_ssl_verification(True) assert authenticator.token_manager.disable_ssl_verification is True with pytest.raises(TypeError) as err: authenticator.set_headers('dummy') assert str(err.value) == 'headers must be a dictionary' authenticator.set_headers({'dummy': 'headers'}) assert authenticator.token_manager.headers == {'dummy': 'headers'} with pytest.raises(TypeError) as err: authenticator.set_proxies('dummy') assert str(err.value) == 'proxies must be a dictionary' authenticator.set_proxies({'dummy': 'proxies'}) assert authenticator.token_manager.proxies == {'dummy': 'proxies'} def test_disable_ssl_verification(): authenticator = MCSPAuthenticator('my-api-key', MOCK_URL, disable_ssl_verification=True) assert authenticator.token_manager.disable_ssl_verification is True authenticator.set_disable_ssl_verification(False) assert authenticator.token_manager.disable_ssl_verification is False def test_invalid_disable_ssl_verification_type(): with pytest.raises(TypeError) as err: authenticator = MCSPAuthenticator('my-api-key', MOCK_URL, disable_ssl_verification='True') assert str(err.value) == 'disable_ssl_verification must be a bool' authenticator = MCSPAuthenticator('my-api-key', MOCK_URL) assert authenticator.token_manager.disable_ssl_verification is False with pytest.raises(TypeError) as err: authenticator.set_disable_ssl_verification('True') assert str(err.value) == 'status must be a bool' def test_mcsp_authenticator_validate_failed(): with pytest.raises(ValueError) as err: MCSPAuthenticator(apikey=None, url=MOCK_URL) assert str(err.value) == 'The apikey shouldn\'t be None.' with pytest.raises(ValueError) as err: MCSPAuthenticator(apikey='my-api-key', url=None) assert str(err.value) == 'The url shouldn\'t be None.' # utility function to construct a mock token server response containing an access token. def get_mock_token_response(issued_at, time_to_live) -> str: access_token_layout = { "username": "dummy", "role": "Admin", "permissions": ["administrator", "manage_catalog"], "sub": "admin", "iss": "sss", "aud": "sss", "uid": "sss", "iat": issued_at, "exp": issued_at + time_to_live, } access_token = jwt.encode( access_token_layout, 'secret', algorithm='HS256', headers={'kid': '230498151c214b788dd97f22b85410a5'} ) token_server_response = {"token": access_token, "token_type": "jwt", "expires_in": time_to_live} # For convenience, return both the server response and the access_token. return (json.dumps(token_server_response), access_token) @responses.activate def test_get_token(): (response, access_token) = get_mock_token_response(time.time(), 7200) responses.add(responses.POST, MOCK_URL + OPERATION_PATH, body=response, status=200) auth_headers = {'Host': 'mcsp.cloud.ibm.com:443'} authenticator = MCSPAuthenticator(apikey='my-api-key', url=MOCK_URL, headers=auth_headers) # Authenticate the request and verify the Authorization header. request = {'headers': {}} authenticator.authenticate(request) assert request['headers']['Authorization'] == 'Bearer ' + access_token # Verify that the "get token" request contained the Host header. assert responses.calls[0].request.headers.get('Host') == 'mcsp.cloud.ibm.com:443' @responses.activate def test_get_token_cached(): (response, access_token) = get_mock_token_response(time.time(), 7200) responses.add(responses.POST, MOCK_URL + OPERATION_PATH, body=response, status=200) authenticator = MCSPAuthenticator(apikey='my-api-key', url=MOCK_URL) # Authenticate the request and verify the Authorization header. request = {'headers': {}} authenticator.authenticate(request) assert request['headers']['Authorization'] == 'Bearer ' + access_token # Authenticate a second request and verify that we used the same access token. request = {'headers': {}} authenticator.authenticate(request) assert request['headers']['Authorization'] == 'Bearer ' + access_token @responses.activate def test_get_token_background_refresh(): t1 = time.time() t2 = t1 + 7200 # Setup the first token response. (response1, access_token1) = get_mock_token_response(t1, 7200) responses.add(responses.POST, MOCK_URL + OPERATION_PATH, body=response1, status=200) # Setup the second token response. (response2, access_token2) = get_mock_token_response(t2, 7200) responses.add(responses.POST, MOCK_URL + OPERATION_PATH, body=response2, status=200) authenticator = MCSPAuthenticator(apikey="my-api-key", url=MOCK_URL) # Authenticate the request and verify that the first access_token is used. request = {'headers': {}} authenticator.authenticate(request) assert request['headers']['Authorization'] == 'Bearer ' + access_token1 # Now put the token manager in the refresh window to trigger a background refresh scenario. authenticator.token_manager.refresh_time = t1 - 1 # Authenticate a second request and verify that the correct access token is used. # Note: Ideally, the token manager would trigger the refresh in a separate thread # and it "should" return the first access token for this second authentication request # while the token manager is obtaining a new access token. # Unfortunately, the TokenManager class method does the refresh request synchronously, # so we get back the second access token here instead. # If we "fix" the TokenManager class to refresh asynchronously, we'll need to # change this test case to expect the first access token here. request = {'headers': {}} authenticator.authenticate(request) assert request['headers']['Authorization'] == 'Bearer ' + access_token2 # Wait for the background refresh to finish. # No need to wait due to the synchronous logic in the TokenManager class mentioned above. # time.sleep(2) # Authenticate another request and verify that the second access token is used again. request = {'headers': {}} authenticator.authenticate(request) assert request['headers']['Authorization'] == 'Bearer ' + access_token2 IBM-python-sdk-core-7ebd71d/test/test_mcsp_token_manager.py000066400000000000000000000057121502256645000241020ustar00rootroot00000000000000# coding: utf-8 # Copyright 2023, 2024 IBM All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # pylint: disable=missing-docstring import logging import json import time import jwt import pytest import responses from ibm_cloud_sdk_core import MCSPTokenManager, ApiException from .utils.logger_utils import setup_test_logger setup_test_logger(logging.ERROR) OPERATION_PATH = '/siusermgr/api/1.0/apikeys/token' MOCK_URL = 'https://mcsp.ibm.com' # utility function to construct a mock token server response containing an access token. def get_mock_token_response(issued_at, time_to_live) -> str: access_token_layout = { "username": "dummy", "role": "Admin", "permissions": ["administrator", "manage_catalog"], "sub": "admin", "iss": "sss", "aud": "sss", "uid": "sss", "iat": issued_at, "exp": issued_at + time_to_live, } access_token = jwt.encode( access_token_layout, 'secret', algorithm='HS256', headers={'kid': '230498151c214b788dd97f22b85410a5'} ) token_server_response = {"token": access_token, "token_type": "jwt", "expires_in": time_to_live} # For convenience, return both the server response and the access_token. return (json.dumps(token_server_response), access_token) @responses.activate def test_request_token(): (response, access_token) = get_mock_token_response(time.time(), 30) responses.add(responses.POST, MOCK_URL + OPERATION_PATH, body=response, status=200) token_manager = MCSPTokenManager(apikey="my-api-key", url=MOCK_URL, disable_ssl_verification=True) token = token_manager.get_token() assert len(responses.calls) == 1 assert responses.calls[0].request.url == MOCK_URL + OPERATION_PATH assert responses.calls[0].request.headers.get('User-Agent').startswith('ibm-python-sdk-core/mcsp-authenticator') assert token == access_token @responses.activate def test_request_token_unsuccessful(): response = """{ "errorCode": "BXNIM0415E", "errorMessage": "Provided API key could not be found" } """ responses.add(responses.POST, url=MOCK_URL + OPERATION_PATH, body=response, status=400) token_manager = MCSPTokenManager(apikey="bad-api-key", url=MOCK_URL) with pytest.raises(ApiException): token_manager.request_token() assert len(responses.calls) == 1 assert responses.calls[0].request.url == MOCK_URL + OPERATION_PATH assert responses.calls[0].response.text == response IBM-python-sdk-core-7ebd71d/test/test_mcspv2_authenticator.py000066400000000000000000000361021502256645000244070ustar00rootroot00000000000000# pylint: disable=missing-docstring import logging import json import time import jwt import pytest import responses from ibm_cloud_sdk_core.authenticators import MCSPV2Authenticator, Authenticator from ibm_cloud_sdk_core import MCSPV2TokenManager from .utils.logger_utils import setup_test_logger setup_test_logger(logging.ERROR) MOCK_APIKEY = 'my-api-key' MOCK_URL = 'https://mcspv2.ibm.com' MOCK_SCOPE_COLLECTION_TYPE = 'accounts' MOCK_SCOPE_ID = 'global_account' MOCK_CALLER_EXT_CLAIM = {"productID": "prod-123"} MOCK_HEADERS = {"header1": "value1", "header2": "value2"} MOCK_PROXIES = {"https": "proxy1", "http": "proxy2"} MOCK_PATH = '/api/2.0/{0}/{1}/apikeys/token'.format(MOCK_SCOPE_COLLECTION_TYPE, MOCK_SCOPE_ID) # pylint: disable=too-many-statements def test_mcspv2_authenticator1(): # Use only required properties. authenticator = MCSPV2Authenticator( apikey=MOCK_APIKEY, url=MOCK_URL, scope_collection_type=MOCK_SCOPE_COLLECTION_TYPE, scope_id=MOCK_SCOPE_ID, ) assert authenticator is not None assert authenticator.authentication_type() == Authenticator.AUTHTYPE_MCSPV2 assert authenticator.token_manager.apikey == MOCK_APIKEY assert authenticator.token_manager.url == MOCK_URL assert authenticator.token_manager.scope_collection_type == MOCK_SCOPE_COLLECTION_TYPE assert authenticator.token_manager.scope_id == MOCK_SCOPE_ID assert authenticator.token_manager.include_builtin_actions is False assert authenticator.token_manager.include_custom_actions is False assert authenticator.token_manager.include_roles is True assert authenticator.token_manager.prefix_roles is False assert authenticator.token_manager.caller_ext_claim is None assert authenticator.token_manager.disable_ssl_verification is False assert authenticator.token_manager.headers is None assert authenticator.token_manager.proxies is None # Test setter functions. authenticator.set_scope_collection_type("subscriptions") assert authenticator.token_manager.scope_collection_type == "subscriptions" with pytest.raises(TypeError) as err: authenticator.set_scope_collection_type(None) assert str(err.value) == '"scope_collection_type" must be a string' authenticator.set_scope_id("new_id") assert authenticator.token_manager.scope_id == "new_id" with pytest.raises(TypeError) as err: authenticator.set_scope_id(None) assert str(err.value) == '"scope_id" must be a string' authenticator.set_include_builtin_actions(True) assert authenticator.token_manager.include_builtin_actions is True with pytest.raises(TypeError) as err: authenticator.set_include_builtin_actions('True') assert str(err.value) == '"include_builtin_actions" must be a bool' authenticator.set_include_custom_actions(True) assert authenticator.token_manager.include_custom_actions is True with pytest.raises(TypeError) as err: authenticator.set_include_custom_actions('not a bool') assert str(err.value) == '"include_custom_actions" must be a bool' authenticator.set_include_roles(True) assert authenticator.token_manager.include_roles is True with pytest.raises(TypeError) as err: authenticator.set_include_roles('nope') assert str(err.value) == '"include_roles" must be a bool' authenticator.set_prefix_roles(True) assert authenticator.token_manager.prefix_roles is True with pytest.raises(TypeError) as err: authenticator.set_prefix_roles('maybe') assert str(err.value) == '"prefix_roles" must be a bool' authenticator.set_caller_ext_claim(MOCK_CALLER_EXT_CLAIM) assert authenticator.token_manager.caller_ext_claim == MOCK_CALLER_EXT_CLAIM with pytest.raises(TypeError) as err: authenticator.set_caller_ext_claim('not a dictionary') assert str(err.value) == '"caller_ext_claim" must be a dictionary or None' authenticator.set_disable_ssl_verification(True) assert authenticator.token_manager.disable_ssl_verification is True with pytest.raises(TypeError) as err: authenticator.set_disable_ssl_verification('not a bool') assert str(err.value) == '"disable_ssl_verification" must be a bool' authenticator.set_headers(MOCK_HEADERS) assert authenticator.token_manager.headers == MOCK_HEADERS with pytest.raises(TypeError) as err: authenticator.set_headers('not a dictionary') assert str(err.value) == '"headers" must be a dictionary or None' authenticator.set_proxies(MOCK_PROXIES) assert authenticator.token_manager.proxies == MOCK_PROXIES with pytest.raises(TypeError) as err: authenticator.set_proxies('not a dictionary') assert str(err.value) == '"proxies" must be a dictionary or None' def test_mcspv2_authenticator2(): # Test with all properties. authenticator = MCSPV2Authenticator( apikey=MOCK_APIKEY, url=MOCK_URL, scope_collection_type=MOCK_SCOPE_COLLECTION_TYPE, scope_id=MOCK_SCOPE_ID, include_builtin_actions=True, include_custom_actions=True, include_roles=False, prefix_roles=True, caller_ext_claim=MOCK_CALLER_EXT_CLAIM, disable_ssl_verification=True, headers=MOCK_HEADERS, proxies=MOCK_PROXIES, ) assert authenticator.token_manager.apikey == MOCK_APIKEY assert authenticator.token_manager.url == MOCK_URL assert authenticator.token_manager.scope_collection_type == MOCK_SCOPE_COLLECTION_TYPE assert authenticator.token_manager.scope_id == MOCK_SCOPE_ID assert authenticator.token_manager.include_builtin_actions is True assert authenticator.token_manager.include_custom_actions is True assert authenticator.token_manager.include_roles is False assert authenticator.token_manager.prefix_roles is True assert authenticator.token_manager.caller_ext_claim == MOCK_CALLER_EXT_CLAIM assert authenticator.token_manager.disable_ssl_verification is True assert authenticator.token_manager.headers == MOCK_HEADERS assert authenticator.token_manager.proxies == MOCK_PROXIES def test_mcsp_authenticator_validate_failed(): # Check each property individually. with pytest.raises(TypeError) as err: MCSPV2Authenticator( apikey=None, url=MOCK_URL, scope_collection_type=MOCK_SCOPE_COLLECTION_TYPE, scope_id=MOCK_SCOPE_ID, ) assert str(err.value) == '"apikey" must be a string' with pytest.raises(TypeError) as err: MCSPV2Authenticator( apikey=MOCK_APIKEY, url=None, scope_collection_type=MOCK_SCOPE_COLLECTION_TYPE, scope_id=MOCK_SCOPE_ID, ) assert str(err.value) == '"url" must be a string' with pytest.raises(TypeError) as err: MCSPV2Authenticator( apikey=MOCK_APIKEY, url=MOCK_URL, scope_collection_type=None, scope_id=MOCK_SCOPE_ID, ) assert str(err.value) == '"scope_collection_type" must be a string' with pytest.raises(TypeError) as err: MCSPV2Authenticator( apikey=MOCK_APIKEY, url=MOCK_URL, scope_collection_type=MOCK_SCOPE_COLLECTION_TYPE, scope_id=None, ) assert str(err.value) == '"scope_id" must be a string' with pytest.raises(TypeError) as err: MCSPV2Authenticator( apikey=MOCK_APIKEY, url=MOCK_URL, scope_collection_type=MOCK_SCOPE_COLLECTION_TYPE, scope_id=MOCK_SCOPE_ID, include_builtin_actions='not a bool', ) assert str(err.value) == '"include_builtin_actions" must be a bool' with pytest.raises(TypeError) as err: MCSPV2Authenticator( apikey=MOCK_APIKEY, url=MOCK_URL, scope_collection_type=MOCK_SCOPE_COLLECTION_TYPE, scope_id=MOCK_SCOPE_ID, include_custom_actions=None, ) assert str(err.value) == '"include_custom_actions" must be a bool' with pytest.raises(TypeError) as err: MCSPV2Authenticator( apikey=MOCK_APIKEY, url=MOCK_URL, scope_collection_type=MOCK_SCOPE_COLLECTION_TYPE, scope_id=MOCK_SCOPE_ID, include_roles=382636, ) assert str(err.value) == '"include_roles" must be a bool' with pytest.raises(TypeError) as err: MCSPV2Authenticator( apikey=MOCK_APIKEY, url=MOCK_URL, scope_collection_type=MOCK_SCOPE_COLLECTION_TYPE, scope_id=MOCK_SCOPE_ID, prefix_roles=None, ) assert str(err.value) == '"prefix_roles" must be a bool' # utility function to construct a mock token server response containing an access token. def get_mock_token_response(issued_at: int, time_to_live: int) -> str: access_token_layout = { "username": "dummy", "role": "Admin", "permissions": ["administrator", "manage_catalog"], "sub": "admin", "iss": "sss", "aud": "sss", "uid": "sss", "iat": issued_at, "exp": issued_at + time_to_live, } access_token = jwt.encode( access_token_layout, 'secret', algorithm='HS256', headers={'kid': '230498151c214b788dd97f22b85410a5'} ) token_server_response = { "token": access_token, "token_type": "Bearer", "expires_in": time_to_live, "expiration": issued_at + time_to_live, } # For convenience, return both the server response and the access_token. return (json.dumps(token_server_response), access_token) @responses.activate def test_get_token(): (response, access_token) = get_mock_token_response(int(time.time()), 7200) responses.add(responses.POST, MOCK_URL + MOCK_PATH, body=response, status=200) auth_headers = {'Host': 'mcsp.cloud.ibm.com:443'} authenticator = MCSPV2Authenticator( apikey=MOCK_APIKEY, url=MOCK_URL, scope_collection_type=MOCK_SCOPE_COLLECTION_TYPE, scope_id=MOCK_SCOPE_ID, headers=auth_headers, ) # Authenticate the request and verify the Authorization header. request = {'headers': {}} authenticator.authenticate(request) assert request['headers']['Authorization'] == 'Bearer ' + access_token # Verify that the "get token" request contained the Host header. assert responses.calls[0].request.headers.get('Host') == 'mcsp.cloud.ibm.com:443' @responses.activate def test_get_token_cached(): (response, access_token) = get_mock_token_response(int(time.time()), 7200) responses.add(responses.POST, MOCK_URL + MOCK_PATH, body=response, status=200) authenticator = MCSPV2Authenticator( apikey=MOCK_APIKEY, url=MOCK_URL, scope_collection_type=MOCK_SCOPE_COLLECTION_TYPE, scope_id=MOCK_SCOPE_ID, ) # Authenticate the request and verify the Authorization header. request = {'headers': {}} authenticator.authenticate(request) assert request['headers']['Authorization'] == 'Bearer ' + access_token # Authenticate a second request and verify that we used the same access token. request = {'headers': {}} authenticator.authenticate(request) assert request['headers']['Authorization'] == 'Bearer ' + access_token @responses.activate def test_get_token_background_refresh(): t1 = time.time() t2 = t1 + 7200 # Setup the first token response. (response1, access_token1) = get_mock_token_response(int(t1), 7200) responses.add(responses.POST, MOCK_URL + MOCK_PATH, body=response1, status=200) # Setup the second token response. (response2, access_token2) = get_mock_token_response(int(t2), 7200) responses.add(responses.POST, MOCK_URL + MOCK_PATH, body=response2, status=200) authenticator = MCSPV2Authenticator( apikey=MOCK_APIKEY, url=MOCK_URL, scope_collection_type=MOCK_SCOPE_COLLECTION_TYPE, scope_id=MOCK_SCOPE_ID, ) # Authenticate the request and verify that the first access_token is used. request = {'headers': {}} authenticator.authenticate(request) assert request['headers']['Authorization'] == 'Bearer ' + access_token1 # Now put the token manager in the refresh window to trigger a background refresh scenario. authenticator.token_manager.refresh_time = t1 - 1 # Authenticate a second request and verify that the correct access token is used. # Note: Ideally, the token manager would trigger the refresh in a separate thread # and it "should" return the first access token for this second authentication request # while the token manager is obtaining a new access token. # Unfortunately, the TokenManager class method does the refresh request synchronously, # so we get back the second access token here instead. # If we "fix" the TokenManager class to refresh asynchronously, we'll need to # change this test case to expect the first access token here. request = {'headers': {}} authenticator.authenticate(request) assert request['headers']['Authorization'] == 'Bearer ' + access_token2 # Wait for the background refresh to finish. # No need to wait due to the synchronous logic in the TokenManager class mentioned above. # time.sleep(2) # Authenticate another request and verify that the second access token is used again. request = {'headers': {}} authenticator.authenticate(request) assert request['headers']['Authorization'] == 'Bearer ' + access_token2 @responses.activate def test_request_token(): (response, access_token) = get_mock_token_response(time.time(), 30) responses.add(responses.POST, MOCK_URL + MOCK_PATH, body=response, status=200) token_manager = MCSPV2TokenManager( apikey=MOCK_APIKEY, url=MOCK_URL, scope_collection_type=MOCK_SCOPE_COLLECTION_TYPE, scope_id=MOCK_SCOPE_ID, disable_ssl_verification=True, ) token = token_manager.get_token() assert len(responses.calls) == 1 assert ( responses.calls[0].request.url == MOCK_URL + MOCK_PATH + '?includeBuiltinActions=false&includeCustomActions=false&' + 'includeRoles=true&prefixRolesWithDefinitionScope=false' ) assert responses.calls[0].request.headers.get('User-Agent').startswith('ibm-python-sdk-core/mcspv2-authenticator') assert token == access_token @responses.activate def test_request_token_unsuccessful(): response = """{ "errorCode": "BXNIM0415E", "errorMessage": "Provided API key could not be found" } """ responses.add(responses.POST, url=MOCK_URL + MOCK_PATH, body=response, status=400) token_manager = MCSPV2TokenManager( apikey="bad-api-key", url=MOCK_URL, scope_collection_type=MOCK_SCOPE_COLLECTION_TYPE, scope_id=MOCK_SCOPE_ID, disable_ssl_verification=True, ) with pytest.raises(Exception): token_manager.request_token() assert len(responses.calls) == 1 assert ( responses.calls[0].request.url == MOCK_URL + MOCK_PATH + '?includeBuiltinActions=false&includeCustomActions=false&' + 'includeRoles=true&prefixRolesWithDefinitionScope=false' ) assert responses.calls[0].response.text == response IBM-python-sdk-core-7ebd71d/test/test_no_auth_authenticator.py000066400000000000000000000007001502256645000246250ustar00rootroot00000000000000# pylint: disable=missing-docstring from ibm_cloud_sdk_core.authenticators import NoAuthAuthenticator, Authenticator def test_no_auth_authenticator(): authenticator = NoAuthAuthenticator() assert authenticator is not None assert authenticator.authentication_type() == Authenticator.AUTHTYPE_NOAUTH authenticator.validate() request = {'headers': {}} authenticator.authenticate(request) assert not request['headers'] IBM-python-sdk-core-7ebd71d/test/test_token_manager.py000066400000000000000000000054001502256645000230520ustar00rootroot00000000000000# coding: utf-8 # Copyright 2020 IBM All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # pylint: disable=missing-docstring,protected-access,abstract-class-instantiated from types import SimpleNamespace from unittest import mock import pytest from ibm_cloud_sdk_core import ApiException from ibm_cloud_sdk_core.token_managers.token_manager import TokenManager class MockTokenManager(TokenManager): def request_token(self) -> None: response = self._request(method='GET', url=self.url) return response def _save_token_info(self, token_response: dict) -> None: pass def test_abstract_class_instantiation(): with pytest.raises(TypeError, match=r"^Can't instantiate abstract class TokenManager.*$"): TokenManager(None) def requests_request_spy(*args, **kwargs): return SimpleNamespace(status_code=200, request_args=args, request_kwargs=kwargs) @mock.patch('requests.request', side_effect=requests_request_spy) def test_request_passes_disable_ssl_verification(request): # pylint: disable=unused-argument mock_token_manager = MockTokenManager(url="https://example.com", disable_ssl_verification=True) assert mock_token_manager.request_token().request_kwargs['verify'] is False def requests_request_error_mock(*args, **kwargs): # pylint: disable=unused-argument return SimpleNamespace(status_code=300, headers={}, text="") @mock.patch('requests.request', side_effect=requests_request_error_mock) def test_request_raises_for_non_2xx(request): # pylint: disable=unused-argument mock_token_manager = MockTokenManager(url="https://example.com", disable_ssl_verification=True) with pytest.raises(ApiException): mock_token_manager.request_token() def test_set_disable_ssl_verification_success(): token_manager = MockTokenManager(None) assert token_manager.disable_ssl_verification is False token_manager.set_disable_ssl_verification(True) assert token_manager.disable_ssl_verification is True def test_set_disable_ssl_verification_fail(): token_manager = MockTokenManager(None) with pytest.raises(TypeError) as err: token_manager.set_disable_ssl_verification('True') assert str(err.value) == 'status must be a bool' assert token_manager.disable_ssl_verification is False IBM-python-sdk-core-7ebd71d/test/test_utils.py000066400000000000000000000330041502256645000214010ustar00rootroot00000000000000# pylint: disable=missing-docstring # coding: utf-8 # Copyright 2019, 2024 IBM All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import datetime import logging import os from typing import Optional import pytest from ibm_cloud_sdk_core import string_to_datetime, datetime_to_string from ibm_cloud_sdk_core import string_to_datetime_list, datetime_to_string_list from ibm_cloud_sdk_core import string_to_date, date_to_string from ibm_cloud_sdk_core import convert_model, convert_list from ibm_cloud_sdk_core import get_query_param from ibm_cloud_sdk_core import read_external_sources from ibm_cloud_sdk_core.utils import GzipStream, strip_extra_slashes, is_json_mimetype from .utils.logger_utils import setup_test_logger setup_test_logger(logging.ERROR) def datetime_test(datestr: str, expected: str): dt_value = string_to_datetime(datestr) assert dt_value is not None actual = datetime_to_string(dt_value) assert actual == expected def test_datetime(): # RFC 3339 with various flavors of tz-offset datetime_test('2016-06-20T04:25:16.218Z', '2016-06-20T04:25:16.218000Z') datetime_test('2016-06-20T04:25:16.218+0000', '2016-06-20T04:25:16.218000Z') datetime_test('2016-06-20T04:25:16.218+00', '2016-06-20T04:25:16.218000Z') datetime_test('2016-06-20T04:25:16.218-0000', '2016-06-20T04:25:16.218000Z') datetime_test('2016-06-20T04:25:16.218-00', '2016-06-20T04:25:16.218000Z') datetime_test('2016-06-20T00:25:16.218-0400', '2016-06-20T04:25:16.218000Z') datetime_test('2016-06-20T00:25:16.218-04', '2016-06-20T04:25:16.218000Z') datetime_test('2016-06-20T07:25:16.218+0300', '2016-06-20T04:25:16.218000Z') datetime_test('2016-06-20T07:25:16.218+03', '2016-06-20T04:25:16.218000Z') datetime_test('2016-06-20T04:25:16Z', '2016-06-20T04:25:16Z') datetime_test('2016-06-20T04:25:16+0000', '2016-06-20T04:25:16Z') datetime_test('2016-06-20T04:25:16-0000', '2016-06-20T04:25:16Z') datetime_test('2016-06-20T01:25:16-0300', '2016-06-20T04:25:16Z') datetime_test('2016-06-20T01:25:16-03:00', '2016-06-20T04:25:16Z') datetime_test('2016-06-20T08:55:16+04:30', '2016-06-20T04:25:16Z') datetime_test('2016-06-20T16:25:16+12:00', '2016-06-20T04:25:16Z') # RFC 3339 with nanoseconds for the Catalog-Managements of the world. datetime_test('2020-03-12T10:52:12.866305005-04:00', '2020-03-12T14:52:12.866305Z') datetime_test('2020-03-12T10:52:12.866305005Z', '2020-03-12T10:52:12.866305Z') datetime_test('2020-03-12T10:52:12.866305005+02:30', '2020-03-12T08:22:12.866305Z') datetime_test('2020-03-12T10:52:12.866305Z', '2020-03-12T10:52:12.866305Z') # UTC datetime with no TZ. datetime_test('2016-06-20T04:25:16.218', '2016-06-20T04:25:16.218000Z') datetime_test('2016-06-20T04:25:16', '2016-06-20T04:25:16Z') # Dialog datetime. datetime_test('2016-06-20 04:25:16', '2016-06-20T04:25:16Z') # IAM Identity Service. datetime_test('2020-11-10T12:28+0000', '2020-11-10T12:28:00Z') datetime_test('2020-11-10T07:28-0500', '2020-11-10T12:28:00Z') datetime_test('2020-11-10T12:28Z', '2020-11-10T12:28:00Z') def test_string_to_datetime(): # If the specified string does not include a timezone, it is assumed to be UTC date = string_to_datetime('2017-03-06 16:00:04.159338') assert date.day == 6 assert date.hour == 16 assert date.tzinfo.utcoffset(None) == datetime.timezone.utc.utcoffset(None) # Test date string with TZ specified as '+xxxx' date = string_to_datetime('2017-03-06 16:00:04.159338+0600') assert date.day == 6 assert date.hour == 16 assert date.tzinfo.utcoffset(None).total_seconds() == 6 * 60 * 60 # Test date string with TZ specified as 'Z' date = string_to_datetime('2017-03-06 16:00:04.159338Z') assert date.day == 6 assert date.hour == 16 assert date.tzinfo.utcoffset(None) == datetime.timezone.utc.utcoffset(None) def test_datetime_to_string(): # If specified date is None, return None assert datetime_to_string(None) is None # If the specified date is "naive", it is interpreted as a UTC date date = datetime.datetime(2017, 3, 6, 16, 0, 4, 159338) res = datetime_to_string(date) assert res == '2017-03-06T16:00:04.159338Z' # Test date with UTC timezone date = datetime.datetime(2017, 3, 6, 16, 0, 4, 159338, datetime.timezone.utc) res = datetime_to_string(date) assert res == '2017-03-06T16:00:04.159338Z' # Test date with non-UTC timezone tzn = datetime.timezone(datetime.timedelta(hours=-6)) date = datetime.datetime(2017, 3, 6, 10, 0, 4, 159338, tzn) res = datetime_to_string(date) assert res == '2017-03-06T16:00:04.159338Z' def test_string_to_datetime_list(): # Assert ValueError is raised for invalid argument type with pytest.raises(ValueError): string_to_datetime_list(None) # If the specified string does not include a timezone, it is assumed to be UTC date_list = string_to_datetime_list(['2017-03-06 16:00:04.159338']) assert date_list[0].day == 6 assert date_list[0].hour == 16 assert date_list[0].tzinfo.utcoffset(None) == datetime.timezone.utc.utcoffset(None) # Test date string with TZ specified as '+xxxx' date_list = string_to_datetime_list(['2017-03-06 16:00:04.159338+0600']) assert date_list[0].day == 6 assert date_list[0].hour == 16 assert date_list[0].tzinfo.utcoffset(None).total_seconds() == 6 * 60 * 60 # Test date string with TZ specified as 'Z' date_list = string_to_datetime_list(['2017-03-06 16:00:04.159338Z']) assert date_list[0].day == 6 assert date_list[0].hour == 16 assert date_list[0].tzinfo.utcoffset(None) == datetime.timezone.utc.utcoffset(None) # Test multiple datetimes in a list date_list = string_to_datetime_list(['2017-03-06 16:00:04.159338', '2017-03-07 17:00:04.159338']) assert date_list[0].day == 6 assert date_list[0].hour == 16 assert date_list[0].tzinfo.utcoffset(None) == datetime.timezone.utc.utcoffset(None) assert date_list[1].day == 7 assert date_list[1].hour == 17 assert date_list[1].tzinfo.utcoffset(None) == datetime.timezone.utc.utcoffset(None) def test_datetime_to_string_list(): # Assert ValueError is raised for invalid argument type with pytest.raises(ValueError): datetime_to_string_list(None) # If specified datetime list item is None, return list of None assert datetime_to_string_list([None]) == [None] # If specified datetime list is empty, return empty list assert not datetime_to_string_list([]) # If the specified date list item is "naive", it is interpreted as a UTC date date_list = [datetime.datetime(2017, 3, 6, 16, 0, 4, 159338)] res = datetime_to_string_list(date_list) assert res == ['2017-03-06T16:00:04.159338Z'] # Test date list item with UTC timezone date_list = [datetime.datetime(2017, 3, 6, 16, 0, 4, 159338, datetime.timezone.utc)] res = datetime_to_string_list(date_list) assert res == ['2017-03-06T16:00:04.159338Z'] # Test date list item with non-UTC timezone tzn = datetime.timezone(datetime.timedelta(hours=-6)) date_list = [datetime.datetime(2017, 3, 6, 10, 0, 4, 159338, tzn)] res = datetime_to_string_list(date_list) assert res == ['2017-03-06T16:00:04.159338Z'] # Test specified date list with multiple items date_list = [ datetime.datetime(2017, 3, 6, 16, 0, 4, 159338), datetime.datetime(2017, 3, 6, 16, 0, 4, 159338, datetime.timezone.utc), ] res = datetime_to_string_list(date_list) assert res == ['2017-03-06T16:00:04.159338Z', '2017-03-06T16:00:04.159338Z'] def test_date_conversion(): date = string_to_date('2017-03-06') assert date.day == 6 res = date_to_string(date) assert res == '2017-03-06' assert date_to_string(None) is None def test_get_query_param(): # Relative URL next_url = '/api/v1/offerings?start=foo&limit=10' page_token = get_query_param(next_url, 'start') assert page_token == 'foo' # Absolute URL next_url = 'https://acme.com/api/v1/offerings?start=bar&limit=10' page_token = get_query_param(next_url, 'start') assert page_token == 'bar' # Missing param next_url = 'https://acme.com/api/v1/offerings?start=bar&limit=10' page_token = get_query_param(next_url, 'token') assert page_token is None # No URL page_token = get_query_param(None, 'start') assert page_token is None # Empty URL page_token = get_query_param('', 'start') assert page_token is None # No query string next_url = '/api/v1/offerings' page_token = get_query_param(next_url, 'start') assert page_token is None # Bad query string next_url = '/api/v1/offerings?start%XXfoo' with pytest.raises(ValueError): page_token = get_query_param(next_url, 'start') # Duplicate param next_url = '/api/v1/offerings?start=foo&start=bar&limit=10' page_token = get_query_param(next_url, 'start') assert page_token == 'foo' # Bad URL - since the behavior for this case varies based on the version of Python # we allow _either_ a ValueError or that the illegal chars are just ignored next_url = 'https://foo.bar\u2100/api/v1/offerings?start=foo' try: page_token = get_query_param(next_url, 'start') assert page_token == 'foo' except ValueError: # This is okay. pass def test_convert_model(): class MockModel: def __init__(self, xyz: Optional[str] = None) -> None: self.xyz = xyz def to_dict(self) -> dict: _dict = {} if hasattr(self, 'xyz') and self.xyz is not None: _dict['xyz'] = self.xyz return _dict @classmethod def from_dict(cls, _dict): pass mock1 = MockModel('foo') mock1_dict = convert_model(mock1) assert mock1_dict == {'xyz': 'foo'} mock2 = {'foo': 'bar', 'baz': 'qux'} mock2_dict = convert_model(mock2) assert mock2_dict == mock2 mock3 = 'this is not a model' mock3_dict = convert_model(mock3) assert mock3_dict == mock3 def test_convert_list(): temp = ['default', '123'] res_str = convert_list(temp) assert res_str == 'default,123' mock2 = 'default,123' mock2_str = convert_list(mock2) assert mock2_str == mock2 mock3 = {'not': 'a list'} mock3_str = convert_list(mock3) assert mock3_str == mock3 mock4 = ['not', 0, 'list of str'] mock4_str = convert_list(mock4) assert mock4_str == mock4 def test_read_external_sources_1(): # Set IBM_CREDENTIALS_FILE to a non-existent file (should be silently ignored). bad_file_path = os.path.join(os.path.dirname(__file__), 'NOT_A_FILE') os.environ['IBM_CREDENTIALS_FILE'] = bad_file_path # This env var should take precendence since the config file wasn't found. os.environ['SERVICE_1_URL'] = 'https://good-url.com' config = read_external_sources('service_1') assert config.get('URL') == 'https://good-url.com' def test_read_external_sources_2(): # The config file should take precedence over the env variable. config_file = os.path.join(os.path.dirname(__file__), '../resources/ibm-credentials.env') os.environ['IBM_CREDENTIALS_FILE'] = config_file # This should be ignored since IBM_CREDENTIALS_FILE points to a valid file. os.environ['SERVICE_1_URL'] = 'wrong-url' config = read_external_sources('service_1') assert config.get('URL') == 'service1.com/api' def test_strip_extra_slashes(): assert strip_extra_slashes('') == '' assert strip_extra_slashes('//') == '/' assert strip_extra_slashes('/////') == '/' assert strip_extra_slashes('https://host') == 'https://host' assert strip_extra_slashes('https://host/') == 'https://host/' assert strip_extra_slashes('https://host//') == 'https://host/' assert strip_extra_slashes('https://host/path') == 'https://host/path' assert strip_extra_slashes('https://host/path/') == 'https://host/path/' assert strip_extra_slashes('https://host/path//') == 'https://host/path/' assert strip_extra_slashes('https://host//path//') == 'https://host//path/' assert strip_extra_slashes('https://host//path//////////') == 'https://host//path/' def test_is_json_mimetype(): assert is_json_mimetype(None) is False assert is_json_mimetype('') is False assert is_json_mimetype('application/octet-stream') is False assert is_json_mimetype('ApPlIcAtION/JsoN') is False assert is_json_mimetype('applicaiton/json; charset=utf8') is False assert is_json_mimetype('fooapplication/json; charset=utf8; foo=bar') is False assert is_json_mimetype('application/json') is True assert is_json_mimetype('application/json; charset=utf8') is True def test_gzip_stream_open_file(): cr_token_file = os.path.join(os.path.dirname(__file__), '../resources/cr-token.txt') with open(cr_token_file, 'r', encoding='utf-8') as f: stream = GzipStream(source=f) assert stream is not None def test_gzip_stream_open_string(): stream = GzipStream(source='foobar') assert stream is not None def test_gzip_stream_open_bytes(): stream = GzipStream(source=b'foobar') assert stream is not None IBM-python-sdk-core-7ebd71d/test/test_vpc_instance_authenticator.py000066400000000000000000000052631502256645000256550ustar00rootroot00000000000000# pylint: disable=missing-docstring import logging import pytest from ibm_cloud_sdk_core.authenticators import VPCInstanceAuthenticator, Authenticator from .utils.logger_utils import setup_test_logger setup_test_logger(logging.ERROR) TEST_IAM_PROFILE_CRN = 'crn:iam-profile:123' TEST_IAM_PROFILE_ID = 'iam-id-123' def test_constructor(): authenticator = VPCInstanceAuthenticator(iam_profile_id=TEST_IAM_PROFILE_ID, url='someurl.com') assert authenticator is not None assert authenticator.authentication_type() == Authenticator.AUTHTYPE_VPC assert authenticator.token_manager.iam_profile_crn is None assert authenticator.token_manager.iam_profile_id == TEST_IAM_PROFILE_ID assert authenticator.token_manager.url == 'someurl.com' def test_setters(): authenticator = VPCInstanceAuthenticator(iam_profile_id=TEST_IAM_PROFILE_ID, url='someurl.com') assert authenticator is not None assert authenticator.authentication_type() == Authenticator.AUTHTYPE_VPC assert authenticator.token_manager.iam_profile_crn is None assert authenticator.token_manager.iam_profile_id == TEST_IAM_PROFILE_ID assert authenticator.token_manager.url == 'someurl.com' # Set the IAM profile CRN to trigger a validation which will fail, # because at most one of iam_profile_crn or iam_profile_id may be specified. with pytest.raises(ValueError) as err: authenticator.set_iam_profile_crn(TEST_IAM_PROFILE_CRN) assert str(err.value) == 'At most one of "iam_profile_id" or "iam_profile_crn" may be specified.' authenticator.set_iam_profile_id(None) assert authenticator.token_manager.iam_profile_id is None authenticator.set_iam_profile_crn(TEST_IAM_PROFILE_CRN) assert authenticator.token_manager.iam_profile_crn == TEST_IAM_PROFILE_CRN def test_constructor_validate_failed(): with pytest.raises(ValueError) as err: VPCInstanceAuthenticator( iam_profile_crn=TEST_IAM_PROFILE_CRN, iam_profile_id=TEST_IAM_PROFILE_ID, ) assert str(err.value) == 'At most one of "iam_profile_id" or "iam_profile_crn" may be specified.' def test_authenticate(): def mock_get_token(): return 'mock_token' authenticator = VPCInstanceAuthenticator(iam_profile_crn=TEST_IAM_PROFILE_CRN) authenticator.token_manager.get_token = mock_get_token # Simulate an SDK API request that needs to be authenticated. request = {'headers': {}} # Trigger the "get token" processing to obtain the access token and add to the "SDK request". authenticator.authenticate(request) # Verify that the "authenticate()" method added the Authorization header assert request['headers']['Authorization'] == 'Bearer mock_token' IBM-python-sdk-core-7ebd71d/test/test_vpc_instance_token_manager.py000066400000000000000000000253731502256645000256210ustar00rootroot00000000000000# coding: utf-8 # Copyright 2021, 2024 IBM All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # pylint: disable=missing-docstring import json import logging import time import pytest import responses from ibm_cloud_sdk_core import ApiException, VPCInstanceTokenManager from .utils.logger_utils import setup_test_logger setup_test_logger(logging.WARNING) # pylint: disable=line-too-long TEST_ACCESS_TOKEN_1 = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6ImhlbGxvIiwicm9sZSI6InVzZXIiLCJwZXJtaXNzaW9ucyI6WyJhZG1pbmlzdHJhdG9yIiwiZGVwbG95bWVudF9hZG1pbiJdLCJzdWIiOiJoZWxsbyIsImlzcyI6IkpvaG4iLCJhdWQiOiJEU1giLCJ1aWQiOiI5OTkiLCJpYXQiOjE1NjAyNzcwNTEsImV4cCI6MTU2MDI4MTgxOSwianRpIjoiMDRkMjBiMjUtZWUyZC00MDBmLTg2MjMtOGNkODA3MGI1NDY4In0.cIodB4I6CCcX8vfIImz7Cytux3GpWyObt9Gkur5g1QI' TEST_ACCESS_TOKEN_2 = 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImtpZCI6IjIzMDQ5ODE1MWMyMTRiNzg4ZGQ5N2YyMmI4NTQxMGE1In0.eyJ1c2VybmFtZSI6ImR1bW15Iiwicm9sZSI6IkFkbWluIiwicGVybWlzc2lvbnMiOlsiYWRtaW5pc3RyYXRvciIsIm1hbmFnZV9jYXRhbG9nIl0sInN1YiI6ImFkbWluIiwiaXNzIjoic3NzIiwiYXVkIjoic3NzIiwidWlkIjoic3NzIiwiaWF0IjozNjAwLCJleHAiOjE2MjgwMDcwODF9.zvUDpgqWIWs7S1CuKv40ERw1IZ5FqSFqQXsrwZJyfRM' TEST_TOKEN = 'abc123' TEST_IAM_TOKEN = 'iam-abc123' TEST_IAM_PROFILE_CRN = 'crn:iam-profile:123' TEST_IAM_PROFILE_ID = 'iam-id-123' EXPIRATION_WINDOW = 10 def _get_current_time() -> int: return int(time.time()) def test_constructor(): token_manager = VPCInstanceTokenManager( iam_profile_crn=TEST_IAM_PROFILE_CRN, ) assert token_manager.iam_profile_crn is TEST_IAM_PROFILE_CRN assert token_manager.iam_profile_id is None assert token_manager.access_token is None def test_setters(): token_manager = VPCInstanceTokenManager( iam_profile_crn=TEST_IAM_PROFILE_CRN, ) assert token_manager.iam_profile_crn is TEST_IAM_PROFILE_CRN assert token_manager.iam_profile_id is None assert token_manager.access_token is None token_manager.set_iam_profile_crn(None) assert token_manager.iam_profile_crn is None token_manager.set_iam_profile_id(TEST_IAM_PROFILE_ID) assert token_manager.iam_profile_id == TEST_IAM_PROFILE_ID @responses.activate def test_retrieve_instance_identity_token(): token_manager = VPCInstanceTokenManager( iam_profile_crn=TEST_IAM_PROFILE_CRN, url='http://someurl.com', ) response = { 'access_token': TEST_TOKEN, } responses.add(responses.PUT, 'http://someurl.com/instance_identity/v1/token', body=json.dumps(response), status=200) ii_token = token_manager.retrieve_instance_identity_token() assert len(responses.calls) == 1 assert responses.calls[0].request.headers['Content-Type'] == 'application/json' assert responses.calls[0].request.headers['Accept'] == 'application/json' assert responses.calls[0].request.headers['Metadata-Flavor'] == 'ibm' assert responses.calls[0].request.headers['User-Agent'].startswith('ibm-python-sdk-core/vpc-instance-authenticator') assert responses.calls[0].request.params['version'] == '2022-03-01' assert responses.calls[0].request.body == '{"expires_in": 300}' assert ii_token == TEST_TOKEN @responses.activate def test_retrieve_instance_identity_token_failed(): token_manager = VPCInstanceTokenManager( iam_profile_crn=TEST_IAM_PROFILE_CRN, url='http://someurl.com', ) response = { 'errors': ['Ooops'], } responses.add(responses.PUT, 'http://someurl.com/instance_identity/v1/token', body=json.dumps(response), status=400) with pytest.raises(ApiException): token_manager.retrieve_instance_identity_token() assert len(responses.calls) == 1 @responses.activate def test_request_token_with_crn(): token_manager = VPCInstanceTokenManager( iam_profile_crn=TEST_IAM_PROFILE_CRN, ) # Mock the retrieve instance identity token method. def mock_retrieve_instance_identity_token(): return TEST_TOKEN token_manager.retrieve_instance_identity_token = mock_retrieve_instance_identity_token response = { 'access_token': TEST_IAM_TOKEN, } responses.add( responses.POST, 'http://169.254.169.254/instance_identity/v1/iam_token', body=json.dumps(response), status=200 ) response = token_manager.request_token() assert len(responses.calls) == 1 assert responses.calls[0].request.headers['Content-Type'] == 'application/json' assert responses.calls[0].request.headers['Accept'] == 'application/json' assert responses.calls[0].request.headers['Authorization'] == 'Bearer ' + TEST_TOKEN assert responses.calls[0].request.headers['User-Agent'].startswith('ibm-python-sdk-core/vpc-instance-authenticator') assert responses.calls[0].request.body == '{"trusted_profile": {"crn": "crn:iam-profile:123"}}' assert responses.calls[0].request.params['version'] == '2022-03-01' @responses.activate def test_request_token_with_id(): token_manager = VPCInstanceTokenManager( iam_profile_id=TEST_IAM_PROFILE_ID, ) # Mock the retrieve instance identity token method. def mock_retrieve_instance_identity_token(): return TEST_TOKEN token_manager.retrieve_instance_identity_token = mock_retrieve_instance_identity_token response = { 'access_token': TEST_IAM_TOKEN, } responses.add( responses.POST, 'http://169.254.169.254/instance_identity/v1/iam_token', body=json.dumps(response), status=200 ) response = token_manager.request_token() assert len(responses.calls) == 1 assert responses.calls[0].request.headers['Content-Type'] == 'application/json' assert responses.calls[0].request.headers['Accept'] == 'application/json' assert responses.calls[0].request.headers['Authorization'] == 'Bearer ' + TEST_TOKEN assert responses.calls[0].request.body == '{"trusted_profile": {"id": "iam-id-123"}}' assert responses.calls[0].request.params['version'] == '2022-03-01' @responses.activate def test_request_token(): token_manager = VPCInstanceTokenManager() # Mock the retrieve instance identity token method. def mock_retrieve_instance_identity_token(): return TEST_TOKEN token_manager.retrieve_instance_identity_token = mock_retrieve_instance_identity_token response = { 'access_token': TEST_IAM_TOKEN, } responses.add( responses.POST, 'http://169.254.169.254/instance_identity/v1/iam_token', body=json.dumps(response), status=200 ) response = token_manager.request_token() assert len(responses.calls) == 1 assert responses.calls[0].request.headers['Content-Type'] == 'application/json' assert responses.calls[0].request.headers['Accept'] == 'application/json' assert responses.calls[0].request.headers['Authorization'] == 'Bearer ' + TEST_TOKEN assert responses.calls[0].request.body is None assert responses.calls[0].request.params['version'] == '2022-03-01' @responses.activate def test_request_token_failed(): token_manager = VPCInstanceTokenManager( iam_profile_id=TEST_IAM_PROFILE_ID, ) # Mock the retrieve instance identity token method. def mock_retrieve_instance_identity_token(): return TEST_TOKEN token_manager.retrieve_instance_identity_token = mock_retrieve_instance_identity_token response = { 'errors': ['Ooops'], } responses.add( responses.POST, 'http://169.254.169.254/instance_identity/v1/iam_token', body=json.dumps(response), status=400 ) with pytest.raises(ApiException): token_manager.request_token() assert len(responses.calls) == 1 @responses.activate def test_access_token(): token_manager = VPCInstanceTokenManager( iam_profile_id=TEST_IAM_PROFILE_ID, ) response_ii = { 'access_token': TEST_TOKEN, } response_iam = { 'access_token': TEST_ACCESS_TOKEN_1, } responses.add( responses.PUT, 'http://169.254.169.254/instance_identity/v1/token', body=json.dumps(response_ii), status=200 ) responses.add( responses.POST, 'http://169.254.169.254/instance_identity/v1/iam_token', body=json.dumps(response_iam), status=200, ) assert token_manager.access_token is None assert token_manager.expire_time == 0 assert token_manager.refresh_time == 0 token_manager.get_token() assert token_manager.access_token == TEST_ACCESS_TOKEN_1 assert token_manager.expire_time > 0 assert token_manager.refresh_time > 0 @responses.activate def test_get_token_success(): token_manager = VPCInstanceTokenManager() # Mock the retrieve instance identity token method. def mock_retrieve_instance_identity_token(): return TEST_TOKEN token_manager.retrieve_instance_identity_token = mock_retrieve_instance_identity_token response1 = { 'access_token': TEST_ACCESS_TOKEN_1, } response2 = { 'access_token': TEST_ACCESS_TOKEN_2, } responses.add( responses.POST, 'http://169.254.169.254/instance_identity/v1/iam_token', body=json.dumps(response1), status=200 ) access_token = token_manager.get_token() assert access_token == TEST_ACCESS_TOKEN_1 assert token_manager.access_token == TEST_ACCESS_TOKEN_1 # Verify that the token manager returns the cached value. # Before we call `get_token` again, set the expiration and refresh time # so that we do not fetch a new access token. # This is necessary because we are using a fixed JWT response. token_manager.expire_time = _get_current_time() + 1000 token_manager.refresh_time = _get_current_time() + 1000 access_token = token_manager.get_token() assert access_token == TEST_ACCESS_TOKEN_1 assert token_manager.access_token == TEST_ACCESS_TOKEN_1 # Force expiration to get the second token. # We'll set the expiration time to be current-time + EXPIRATION_WINDOW (10 secs) # because we want the access token to be considered as "expired" # when we reach the IAM-server reported expiration time minus 10 secs. responses.add( responses.POST, 'http://169.254.169.254/instance_identity/v1/iam_token', body=json.dumps(response2), status=200 ) token_manager.expire_time = _get_current_time() + EXPIRATION_WINDOW access_token = token_manager.get_token() assert access_token == TEST_ACCESS_TOKEN_2 assert token_manager.access_token == TEST_ACCESS_TOKEN_2 IBM-python-sdk-core-7ebd71d/test/utils/000077500000000000000000000000001502256645000177705ustar00rootroot00000000000000IBM-python-sdk-core-7ebd71d/test/utils/__init__.py000066400000000000000000000000001502256645000220670ustar00rootroot00000000000000IBM-python-sdk-core-7ebd71d/test/utils/http_utils.py000066400000000000000000000045031502256645000225430ustar00rootroot00000000000000# coding: utf-8 # Copyright 2024 IBM All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import functools import threading import warnings from typing import Callable, Optional from http.server import HTTPServer, SimpleHTTPRequestHandler from ssl import SSLContext import urllib3 def local_server( port: int, tls_version: Optional[int] = None, cert: Optional[str] = None, key: Optional[str] = None ) -> Callable: """local_server helps setting up and running an HTTP(S) server for testing purposes.""" def decorator(test_function: Callable) -> Callable: @functools.wraps(test_function) def inner(*args, **kwargs): is_https = tls_version and cert and key # Disable warnings caused by the self-signed certificate. urllib3.disable_warnings() if is_https: # Build the SSL context for the server. ssl_context = SSLContext(tls_version) ssl_context.load_cert_chain(certfile=cert, keyfile=key) # Create and start the server on a separate thread. server = HTTPServer(('localhost', port), SimpleHTTPRequestHandler) if is_https: server.socket = ssl_context.wrap_socket(server.socket, server_side=True) t = threading.Thread(target=server.serve_forever) t.start() # We run everything in a big try-except-finally block to make sure we always # shutdown the HTTP server gracefully. try: test_function(*args, **kwargs) except Exception: # pylint: disable=try-except-raise raise finally: server.shutdown() t.join() # Re-enable warnings. warnings.resetwarnings() return inner return decorator IBM-python-sdk-core-7ebd71d/test/utils/logger_utils.py000066400000000000000000000023621502256645000230440ustar00rootroot00000000000000# coding: utf-8 # Copyright 2024 IBM All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import logging from http import client from ibm_cloud_sdk_core.logger import ( get_logger, LoggingFilter, ) def setup_test_logger(level: int): """Sets up logging with the specified logging level to assist testcases.""" logging.basicConfig(level=level, format='%(asctime)s [%(name)s:%(levelname)s] %(message)s', force=True) logger = get_logger() logger.setLevel(level) # If debug logging is requested, then trigger HTTP message logging as well. if logger.isEnabledFor(logging.DEBUG): client.HTTPConnection.debuglevel = 1 client.print = lambda *args: logger.debug(LoggingFilter.filter_message(" ".join(args))) IBM-python-sdk-core-7ebd71d/test_integration/000077500000000000000000000000001502256645000212335ustar00rootroot00000000000000IBM-python-sdk-core-7ebd71d/test_integration/__init__.py000066400000000000000000000000001502256645000233320ustar00rootroot00000000000000IBM-python-sdk-core-7ebd71d/test_integration/test_cp4d_authenticator_integration.py000066400000000000000000000035061502256645000310370ustar00rootroot00000000000000# pylint: disable=missing-docstring import logging import os from test.utils.logger_utils import setup_test_logger from ibm_cloud_sdk_core import get_authenticator_from_environment # To enable debug logging as well as HTTP message logging, # change WARNING to DEBUG: setup_test_logger(logging.WARNING) # Note: Only the unit tests are run by default. # # In order to test with a live CP4D server, rename "ibm-credentials-cp4dtest.env.example" to # "ibm-credentials-cp4dtest.env" in the resources folder and populate the fields. # Then run this command: # pytest test_integration/test_cp4d_authenticator_integration.py IBM_CREDENTIALS_FILE = '../resources/ibm-credentials-cp4dtest.env' def test_cp4d_authenticator_password(): file_path = os.path.join(os.path.dirname(__file__), IBM_CREDENTIALS_FILE) os.environ['IBM_CREDENTIALS_FILE'] = file_path authenticator = get_authenticator_from_environment('cp4d_password_test') assert authenticator is not None assert authenticator.token_manager.password is not None assert authenticator.token_manager.apikey is None request = {'headers': {}} authenticator.authenticate(request) assert request['headers']['Authorization'] is not None assert 'Bearer' in request['headers']['Authorization'] def test_cp4d_authenticator_apikey(): file_path = os.path.join(os.path.dirname(__file__), IBM_CREDENTIALS_FILE) os.environ['IBM_CREDENTIALS_FILE'] = file_path authenticator = get_authenticator_from_environment('cp4d_apikey_test') assert authenticator is not None assert authenticator.token_manager.password is None assert authenticator.token_manager.apikey is not None request = {'headers': {}} authenticator.authenticate(request) assert request['headers']['Authorization'] is not None assert 'Bearer' in request['headers']['Authorization'] IBM-python-sdk-core-7ebd71d/test_integration/test_iam_assume_authenticator_integration.py000066400000000000000000000021661502256645000323310ustar00rootroot00000000000000# pylint: disable=missing-docstring import os import logging from test.utils.logger_utils import setup_test_logger from ibm_cloud_sdk_core import get_authenticator_from_environment # Note: Only the unit tests are run by default. # # In order to test with a live IAM server, create file "iamassumetest.env" in the project root. # It should look like this: # # IAMASSUMETEST_AUTH_URL= e.g. https://iam.cloud.ibm.com # IAMASSUMETEST_AUTH_TYPE=iam # IAMASSUMETEST_APIKEY= # IAMASSUMETEST_PROFILE_ID= # # Then run this command: # pytest test_integration/test_iam_assume_authenticator_integration.py # To enable debug logging as well as HTTP message logging, # change WARNING to DEBUG: setup_test_logger(logging.WARNING) def test_iam_authenticator(): os.environ['IBM_CREDENTIALS_FILE'] = 'iamassumetest.env' authenticator = get_authenticator_from_environment('iamassumetest') assert authenticator is not None request = {'headers': {}} authenticator.authenticate(request) assert request['headers']['Authorization'] is not None assert 'Bearer' in request['headers']['Authorization'] IBM-python-sdk-core-7ebd71d/test_integration/test_iam_authenticator_integration.py000066400000000000000000000020461502256645000307510ustar00rootroot00000000000000# pylint: disable=missing-docstring import os import logging from test.utils.logger_utils import setup_test_logger from ibm_cloud_sdk_core import get_authenticator_from_environment # Note: Only the unit tests are run by default. # # In order to test with a live IAM server, create file "iamtest.env" in the project root. # It should look like this: # # IAMTEST1_AUTH_URL= e.g. https://iam.cloud.ibm.com # IAMTEST1_AUTH_TYPE=iam # IAMTEST1_APIKEY= # # Then run this command: # pytest test_integration/test_iam_authenticator_integration.py # To enable debug logging as well as HTTP message logging, # change WARNING to DEBUG: setup_test_logger(logging.WARNING) def test_iam_authenticator(): os.environ['IBM_CREDENTIALS_FILE'] = 'iamtest.env' authenticator = get_authenticator_from_environment('iamtest1') assert authenticator is not None request = {'headers': {}} authenticator.authenticate(request) assert request['headers']['Authorization'] is not None assert 'Bearer' in request['headers']['Authorization'] IBM-python-sdk-core-7ebd71d/test_integration/test_mcsp_authenticator_integration.py000066400000000000000000000017371502256645000311530ustar00rootroot00000000000000# pylint: disable=missing-docstring import logging import os from test.utils.logger_utils import setup_test_logger from ibm_cloud_sdk_core import get_authenticator_from_environment setup_test_logger(logging.WARNING) # Note: Only the unit tests are run by default. # # In order to test with a live MCSP token server, create file "mcsptest.env" in the project root. # It should look like this: # # MCSPTEST1_AUTH_URL= e.g. https://iam.cloud.ibm.com # MCSPTEST1_AUTH_TYPE=mcsp # MCSPTEST1_APIKEY= # # Then run this command: # pytest test_integration/test_mcsp_authenticator_integration.py def test_mcsp_authenticator(): os.environ['IBM_CREDENTIALS_FILE'] = 'mcsptest.env' authenticator = get_authenticator_from_environment('mcsptest1') assert authenticator is not None request = {'headers': {}} authenticator.authenticate(request) assert request['headers']['Authorization'] is not None assert 'Bearer' in request['headers']['Authorization'] IBM-python-sdk-core-7ebd71d/test_integration/test_mcspv2_authenticator_integration.py000066400000000000000000000030101502256645000314050ustar00rootroot00000000000000# pylint: disable=missing-docstring import logging import os from test.utils.logger_utils import setup_test_logger from ibm_cloud_sdk_core import get_authenticator_from_environment setup_test_logger(logging.WARNING) # Note: Only the unit tests are run by default. # # In order to test with a live MCSP token server, create file "mcspv2test.env" in the project root. # It should look like this: # # required properties: # # MCSPV2TEST1_AUTH_URL= e.g. https://account-iam.platform.dev.saas.ibm.com # MCSPV2TEST1_AUTH_TYPE=mcspv2 # MCSPV2TEST1_APIKEY= # MCSPV2TEST1_SCOPE_COLLECTION_TYPE=accounts (use any valid collection type value) # MCSPV2TEST1_SCOPE_ID=global_account (use any valid scope id) # # optional properties: # # MCSPV2TEST1_INCLUDE_BUILTIN_ACTIONS=true|false # MCSPV2TEST1_INCLUDE_CUSTOM_ACTIONS=true|false # MCSPV2TEST1_INCLUDE_ROLES=true|false # MCSPV2TEST1_PREFIX_ROLES=true|false # MCSPV2TEST1_CALLER_EXT_CLAIM={"productID":"prod123"} # # Then run this command: # pytest test_integration/test_mcspv2_authenticator_integration.py def test_mcspv2_authenticator(): os.environ['IBM_CREDENTIALS_FILE'] = 'mcspv2test.env' authenticator = get_authenticator_from_environment('mcspv2test1') assert authenticator is not None request = {'headers': {}} authenticator.authenticate(request) assert request['headers']['Authorization'] is not None auth_header = request['headers']['Authorization'] assert 'Bearer' in auth_header print("Authorization: ", auth_header)