├── .DS_Store ├── .github └── workflows │ ├── publish-to-pip.yaml │ └── python.yml ├── .gitignore ├── .gitlab-ci.yml ├── .openapi-generator-ignore ├── .travis.yml ├── README.md ├── config.yaml ├── git_push.sh ├── infisical_sdk ├── __init__.py ├── __pycache__ │ ├── __init__.cpython-39.pyc │ ├── aws_auth.cpython-39.pyc │ ├── client.cpython-39.pyc │ └── universal_auth.cpython-39.pyc ├── api_types.py ├── client.py ├── infisical_requests.py ├── requirements.txt ├── resources │ ├── __init__.py │ ├── auth.py │ ├── auth_methods │ │ ├── __init__.py │ │ ├── aws_auth.py │ │ └── universal_auth.py │ ├── kms.py │ └── secrets.py └── util │ ├── __init__.py │ └── secrets_cache.py ├── pyproject.toml ├── requirements.txt ├── setup.cfg ├── setup.py ├── sink ├── .env.example ├── README.md ├── cache_deletion_test.py ├── cache_expire_test.py ├── cache_test.py └── example.py ├── test-requirements.txt └── tox.ini /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Infisical/python-sdk-official/eaf1f8225ddc14a5d35bfc35be0e519b8b8ae6f8/.DS_Store -------------------------------------------------------------------------------- /.github/workflows/publish-to-pip.yaml: -------------------------------------------------------------------------------- 1 | name: Publish Python Package 2 | on: 3 | push: 4 | tags: 5 | - 'v*' # Trigger on push of tags starting with 'v' 6 | jobs: 7 | deploy: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - name: Set up Python 12 | uses: actions/setup-python@v2 13 | with: 14 | python-version: '3.x' 15 | - name: Install dependencies 16 | run: | 17 | python -m pip install --upgrade pip 18 | pip install setuptools wheel twine 19 | - name: Set version 20 | run: | 21 | VERSION=${GITHUB_REF#refs/tags/v} 22 | # Update VERSION in setup.py 23 | sed -i "s/VERSION = \".*\"/VERSION = \"$VERSION\"/" setup.py 24 | - name: Build and publish 25 | env: 26 | TWINE_USERNAME: __token__ 27 | TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} 28 | run: | 29 | python setup.py sdist bdist_wheel 30 | twine upload dist/* -------------------------------------------------------------------------------- /.github/workflows/python.yml: -------------------------------------------------------------------------------- 1 | # NOTE: This file is auto generated by OpenAPI Generator. 2 | # URL: https://openapi-generator.tech 3 | # 4 | # ref: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python 5 | 6 | name: infisicalapi_client Python package 7 | 8 | on: [push, pull_request] 9 | 10 | jobs: 11 | build: 12 | 13 | runs-on: ubuntu-latest 14 | strategy: 15 | matrix: 16 | python-version: ["3.8", "3.9", "3.10", "3.11"] 17 | 18 | steps: 19 | - uses: actions/checkout@v3 20 | - name: Set up Python ${{ matrix.python-version }} 21 | uses: actions/setup-python@v4 22 | with: 23 | python-version: ${{ matrix.python-version }} 24 | - name: Install dependencies 25 | run: | 26 | python -m pip install --upgrade pip 27 | pip install flake8 pytest 28 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 29 | if [ -f test-requirements.txt ]; then pip install -r test-requirements.txt; fi 30 | - name: Lint with flake8 31 | run: | 32 | # stop the build if there are Python syntax errors or undefined names 33 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 34 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 35 | flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 36 | 37 | # ? Currently no tests are available 38 | #- name: Test with pytest 39 | # run: | 40 | # pytest -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | venv/ 48 | .venv/ 49 | .python-version 50 | .pytest_cache 51 | 52 | # Translations 53 | *.mo 54 | *.pot 55 | 56 | # Django stuff: 57 | *.log 58 | 59 | # Sphinx documentation 60 | docs/_build/ 61 | 62 | # PyBuilder 63 | target/ 64 | 65 | #Ipython Notebook 66 | .ipynb_checkpoints 67 | 68 | # IDEs 69 | .idea 70 | 71 | .env -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | # NOTE: This file is auto generated by OpenAPI Generator. 2 | # URL: https://openapi-generator.tech 3 | # 4 | # ref: https://docs.gitlab.com/ee/ci/README.html 5 | # ref: https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Python.gitlab-ci.yml 6 | 7 | stages: 8 | - test 9 | 10 | .pytest: 11 | stage: test 12 | script: 13 | - pip install -r requirements.txt 14 | - pip install -r test-requirements.txt 15 | - pytest --cov=infisicalapi_client 16 | 17 | pytest-3.7: 18 | extends: .pytest 19 | image: python:3.7-alpine 20 | pytest-3.8: 21 | extends: .pytest 22 | image: python:3.8-alpine 23 | pytest-3.9: 24 | extends: .pytest 25 | image: python:3.9-alpine 26 | pytest-3.10: 27 | extends: .pytest 28 | image: python:3.10-alpine 29 | pytest-3.11: 30 | extends: .pytest 31 | image: python:3.11-alpine 32 | -------------------------------------------------------------------------------- /.openapi-generator-ignore: -------------------------------------------------------------------------------- 1 | # OpenAPI Generator Ignore 2 | # Generated by openapi-generator https://github.com/openapitools/openapi-generator 3 | 4 | # Use this file to prevent files from being overwritten by the generator. 5 | # The patterns follow closely to .gitignore or .dockerignore. 6 | 7 | # As an example, the C# client generator defines ApiClient.cs. 8 | # You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line: 9 | #ApiClient.cs 10 | 11 | # You can match any string of characters against a directory, file or extension with a single asterisk (*): 12 | #foo/*/qux 13 | # The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux 14 | 15 | # You can recursively match patterns against a directory, file or extension with a double asterisk (**): 16 | #foo/**/qux 17 | # This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux 18 | 19 | # You can also negate patterns with an exclamation (!). 20 | # For example, you can ignore all files in a docs folder with the file extension .md: 21 | #docs/*.md 22 | # Then explicitly reverse the ignore rule for a single file: 23 | #!docs/README.md 24 | 25 | .openapi-generator-ignore 26 | infisical_sdk 27 | .venv 28 | .git 29 | setup.py 30 | README.md 31 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # ref: https://docs.travis-ci.com/user/languages/python 2 | language: python 3 | python: 4 | - "3.7" 5 | - "3.8" 6 | - "3.9" 7 | - "3.10" 8 | - "3.11" 9 | # uncomment the following if needed 10 | #- "3.11-dev" # 3.11 development branch 11 | #- "nightly" # nightly build 12 | # command to install dependencies 13 | install: 14 | - "pip install -r requirements.txt" 15 | - "pip install -r test-requirements.txt" 16 | # command to run tests 17 | script: pytest --cov=infisicalapi_client 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Infisical Python SDK 2 | 3 | The Infisical SDK provides a convenient way to interact with the Infisical API. 4 | 5 | ### Migrating to version 1.0.3 or above 6 | 7 | We have recently rolled out our first stable version of the SDK, version `1.0.3` and above. 8 | 9 | The 1.0.3 version comes with a few key changes that may change how you're using the SDK. 10 | 1. **Removal of `rest`**: The SDK no longer exposes the entire Infisical API. This was nessecary as we have moved away from using an OpenAPI generator approach. We aim to add support for more API resources in the near future. If you have any specific requests, please [open an issue](https://github.com/Infisical/python-sdk-official/issues). 11 | 12 | 2. **New response types**: The 1.0.3 release uses return types that differ from the older versions. The new return types such as `BaseSecret`, are all exported from the Infisical SDK. 13 | 14 | 3. **Property renaming**: Some properties on the responses have been slightly renamed. An example of this would be that the `secret_key` property on the `get_secret_by_name()` method, that has been renamed to `secretKey`. 15 | 16 | With this in mind, you're ready to upgrade your SDK version to `1.0.3` or above. 17 | 18 | You can refer to our [legacy documentation](https://github.com/Infisical/python-sdk-official/tree/9b0403938ee5ae599d42c5f1fdf9158671a15606?tab=readme-ov-file#infisical-python-sdk) if need be. 19 | 20 | ## Requirements 21 | 22 | Python 3.7+ 23 | 24 | ## Installation 25 | 26 | ```bash 27 | pip install infisicalsdk 28 | ``` 29 | 30 | ## Getting Started 31 | 32 | ```python 33 | from infisical_sdk import InfisicalSDKClient 34 | 35 | # Initialize the client 36 | client = InfisicalSDKClient(host="https://app.infisical.com") 37 | 38 | # Authenticate (example using Universal Auth) 39 | client.auth.universal_auth.login( 40 | client_id="", 41 | client_secret="" 42 | ) 43 | 44 | # Use the SDK to interact with Infisical 45 | secrets = client.secrets.list_secrets(project_id="", environment_slug="dev", secret_path="/") 46 | ``` 47 | 48 | ## InfisicalSDKClient Parameters 49 | 50 | The `InfisicalSDKClient` takes the following parameters, which are used as a global configuration for the lifetime of the SDK instance. 51 | 52 | - **host** (`str`, _Optional_): The host URL for your Infisical instance. Defaults to `https://app.infisical.com`. 53 | - **token** (`str`, _Optional_): Specify an authentication token to use for all requests. If provided, you will not need to call any of the `auth` methods. Defaults to `None` 54 | - **cache_ttl** (`int`, _Optional_): The SDK has built-in client-side caching for secrets, greatly improving response times. By default, secrets are cached for 1 minute (60 seconds). You can disable caching by setting `cache_ttl` to `None`, or adjust the duration in seconds as needed. 55 | 56 | ```python 57 | client = InfisicalSDKClient( 58 | host="https://app.infisical.com", # Defaults to https://app.infisical.com 59 | token="", # If not set, use the client.auth() methods. 60 | cache_ttl = 300 # `None` to disable caching 61 | ) 62 | ``` 63 | 64 | ## Core Methods 65 | 66 | The SDK methods are organized into the following high-level categories: 67 | 68 | 1. `auth`: Handles authentication methods. 69 | 2. `secrets`: Manages CRUD operations for secrets. 70 | 3. `kms`: Perform cryptographic operations with Infisical KMS. 71 | 72 | ### `auth` 73 | 74 | The `Auth` component provides methods for authentication: 75 | 76 | #### Universal Auth 77 | 78 | ```python 79 | response = client.auth.universal_auth.login( 80 | client_id="", 81 | client_secret="" 82 | ) 83 | ``` 84 | 85 | #### AWS Auth 86 | 87 | ```python 88 | response = client.auth.aws_auth.login(identity_id="") 89 | ``` 90 | 91 | ### `secrets` 92 | 93 | This sub-class handles operations related to secrets: 94 | 95 | #### List Secrets 96 | 97 | ```python 98 | secrets = client.secrets.list_secrets( 99 | project_id="", 100 | environment_slug="dev", 101 | secret_path="/", 102 | expand_secret_references=True, # Optional 103 | view_secret_value=True, # Optional 104 | recursive=False, # Optional 105 | include_imports=True, # Optional 106 | tag_filters=[] # Optional 107 | ) 108 | ``` 109 | 110 | **Parameters:** 111 | - `project_id` (str): The ID of your project. 112 | - `environment_slug` (str): The environment in which to list secrets (e.g., "dev"). 113 | - `secret_path` (str): The path to the secrets. 114 | - `expand_secret_references` (bool): Whether to expand secret references. 115 | - `view_secret_value` (bool): Whether or not to include the secret value in the response. If set to false, the `secretValue` will be masked with ``. Defaults to true. 116 | - `recursive` (bool): Whether to list secrets recursively. 117 | - `include_imports` (bool): Whether to include imported secrets. 118 | - `tag_filters` (List[str]): Tags to filter secrets. 119 | 120 | **Returns:** 121 | - `ListSecretsResponse`: The response containing the list of secrets. 122 | 123 | #### Create Secret 124 | 125 | ```python 126 | new_secret = client.secrets.create_secret_by_name( 127 | secret_name="NEW_SECRET", 128 | project_id="", 129 | secret_path="/", 130 | environment_slug="dev", 131 | secret_value="secret_value", 132 | secret_comment="Optional comment", 133 | skip_multiline_encoding=False, 134 | secret_reminder_repeat_days=30, # Optional 135 | secret_reminder_note="Remember to update this secret" # Optional 136 | ) 137 | ``` 138 | 139 | **Parameters:** 140 | - `secret_name` (str): The name of the secret. 141 | - `project_id` (str): The ID of your project. 142 | - `secret_path` (str): The path to the secret. 143 | - `environment_slug` (str): The environment in which to create the secret. 144 | - `secret_value` (str): The value of the secret. 145 | - `secret_comment` (str, optional): A comment associated with the secret. 146 | - `skip_multiline_encoding` (bool, optional): Whether to skip encoding for multiline secrets. 147 | - `secret_reminder_repeat_days` (Union[float, int], optional): Number of days after which to repeat secret reminders. 148 | - `secret_reminder_note` (str, optional): A note for the secret reminder. 149 | 150 | **Returns:** 151 | - `BaseSecret`: The response after creating the secret. 152 | 153 | #### Update Secret 154 | 155 | ```python 156 | updated_secret = client.secrets.update_secret_by_name( 157 | current_secret_name="EXISTING_SECRET", 158 | project_id="", 159 | secret_path="/", 160 | environment_slug="dev", 161 | secret_value="new_secret_value", 162 | secret_comment="Updated comment", # Optional 163 | skip_multiline_encoding=False, 164 | secret_reminder_repeat_days=30, # Optional 165 | secret_reminder_note="Updated reminder note", # Optional 166 | new_secret_name="NEW_NAME" # Optional 167 | ) 168 | ``` 169 | 170 | **Parameters:** 171 | - `current_secret_name` (str): The current name of the secret. 172 | - `project_id` (str): The ID of your project. 173 | - `secret_path` (str): The path to the secret. 174 | - `environment_slug` (str): The environment in which to update the secret. 175 | - `secret_value` (str, optional): The new value of the secret. 176 | - `secret_comment` (str, optional): An updated comment associated with the secret. 177 | - `skip_multiline_encoding` (bool, optional): Whether to skip encoding for multiline secrets. 178 | - `secret_reminder_repeat_days` (Union[float, int], optional): Updated number of days after which to repeat secret reminders. 179 | - `secret_reminder_note` (str, optional): An updated note for the secret reminder. 180 | - `new_secret_name` (str, optional): A new name for the secret. 181 | 182 | **Returns:** 183 | - `BaseSecret`: The response after updating the secret. 184 | 185 | #### Get Secret by Name 186 | 187 | ```python 188 | secret = client.secrets.get_secret_by_name( 189 | secret_name="EXISTING_SECRET", 190 | project_id="", 191 | environment_slug="dev", 192 | secret_path="/", 193 | expand_secret_references=True, # Optional 194 | view_secret_value=True, # Optional 195 | include_imports=True, # Optional 196 | version=None # Optional 197 | ) 198 | ``` 199 | 200 | **Parameters:** 201 | - `secret_name` (str): The name of the secret. 202 | - `project_id` (str): The ID of your project. 203 | - `environment_slug` (str): The environment in which to retrieve the secret. 204 | - `secret_path` (str): The path to the secret. 205 | - `expand_secret_references` (bool): Whether to expand secret references. 206 | - `view_secret_value` (bool): Whether or not to include the secret value in the response. If set to false, the `secretValue` will be masked with ``. Defaults to true. 207 | - `include_imports` (bool): Whether to include imported secrets. 208 | - `version` (str, optional): The version of the secret to retrieve. Fetches the latest by default. 209 | 210 | **Returns:** 211 | - `BaseSecret`: The response containing the secret. 212 | 213 | #### Delete Secret by Name 214 | 215 | ```python 216 | deleted_secret = client.secrets.delete_secret_by_name( 217 | secret_name="EXISTING_SECRET", 218 | project_id="", 219 | environment_slug="dev", 220 | secret_path="/" 221 | ) 222 | ``` 223 | 224 | **Parameters:** 225 | - `secret_name` (str): The name of the secret to delete. 226 | - `project_id` (str): The ID of your project. 227 | - `environment_slug` (str): The environment in which to delete the secret. 228 | - `secret_path` (str): The path to the secret. 229 | 230 | **Returns:** 231 | - `BaseSecret`: The response after deleting the secret. 232 | 233 | ### `kms` 234 | 235 | This sub-class handles KMS related operations: 236 | 237 | #### List KMS Keys 238 | 239 | ```python 240 | kms_keys = client.kms.list_keys( 241 | project_id="", 242 | offset=0, # Optional 243 | limit=100, # Optional 244 | order_by=KmsKeysOrderBy.NAME, # Optional 245 | order_direction=OrderDirection.ASC, # Optional 246 | search=None # Optional 247 | ) 248 | ``` 249 | 250 | **Parameters:** 251 | - `project_id` (str): The ID of your project. 252 | - `offset` (int, optional): The offset to paginate from. 253 | - `limit` (int, optional): The page size for paginating. 254 | - `order_by` (KmsKeysOrderBy, optional): The key property to order the list response by. 255 | - `order_direction` (OrderDirection, optional): The direction to order the list response in. 256 | - `search` (str, optional): The text value to filter key names by. 257 | 258 | **Returns:** 259 | - `ListKmsKeysResponse`: The response containing the list of KMS keys. 260 | 261 | #### Get KMS Key by ID 262 | 263 | ```python 264 | kms_key = client.kms.get_key_by_id( 265 | key_id="" 266 | ) 267 | ``` 268 | 269 | **Parameters:** 270 | - `key_id` (str): The ID of the key to retrieve. 271 | 272 | **Returns:** 273 | - `KmsKey`: The specified key. 274 | 275 | #### Get KMS Key by Name 276 | 277 | ```python 278 | kms_key = client.kms.get_key_by_name( 279 | key_name="my-key", 280 | project_id="" 281 | ) 282 | ``` 283 | 284 | **Parameters:** 285 | - `key_name` (str): The name of the key to retrieve. 286 | - `project_id` (str): The ID of your project. 287 | 288 | **Returns:** 289 | - `KmsKey`: The specified key. 290 | 291 | #### Create KMS Key 292 | 293 | ```python 294 | kms_key = client.kms.create_key( 295 | name="my-key", 296 | project_id="", 297 | encryption_algorithm=SymmetricEncryption.AES_GCM_256, 298 | description=None # Optional 299 | ) 300 | ``` 301 | 302 | **Parameters:** 303 | - `name` (str): The name of the key (must be slug-friendly). 304 | - `project_id` (str): The ID of your project. 305 | - `encryption_algorithm` (SymmetricEncryption): The encryption alogrithm this key should use. 306 | - `description` (str, optional): A description of your key. 307 | 308 | **Returns:** 309 | - `KmsKey`: The newly created key. 310 | 311 | #### Update KMS Key 312 | 313 | ```python 314 | updated_key = client.kms.update_key( 315 | key_id="", 316 | name="my-updated-key", # Optional 317 | description="Updated description", # Optional 318 | is_disabled=True # Optional 319 | ) 320 | ``` 321 | 322 | **Parameters:** 323 | - `key_id` (str): The ID of the key to be updated. 324 | - `name` (str, optional): The updated name of the key (must be slug-friendly). 325 | - `description` (str): The updated description of the key. 326 | - `is_disabled` (str): The flag to disable operations with this key. 327 | 328 | **Returns:** 329 | - `KmsKey`: The updated key. 330 | 331 | #### Delete KMS Key 332 | 333 | ```python 334 | deleted_key = client.kms.delete_key( 335 | key_id="" 336 | ) 337 | ``` 338 | 339 | **Parameters:** 340 | - `key_id` (str): The ID of the key to be deleted. 341 | 342 | **Returns:** 343 | - `KmsKey`: The deleted key. 344 | 345 | #### Encrypt Data with KMS Key 346 | 347 | ```python 348 | encrypted_data = client.kms.encrypt_data( 349 | key_id="", 350 | base64EncodedPlaintext="TXkgc2VjcmV0IG1lc3NhZ2U=" # must be base64 encoded 351 | ) 352 | ``` 353 | 354 | **Parameters:** 355 | - `key_id` (str): The ID of the key to encrypt the data with. 356 | - `base64EncodedPlaintext` (str): The plaintext data to encrypt (must be base64 encoded). 357 | 358 | **Returns:** 359 | - `str`: The encrypted ciphertext. 360 | 361 | #### Decrypte Data with KMS Key 362 | 363 | ```python 364 | decrypted_data = client.kms.decrypt_data( 365 | key_id="", 366 | ciphertext="Aq96Ry7sMH3k/ogaIB5MiSfH+LblQRBu69lcJe0GfIvI48ZvbWY+9JulyoQYdjAx" 367 | ) 368 | ``` 369 | 370 | **Parameters:** 371 | - `key_id` (str): The ID of the key to decrypt the data with. 372 | - `ciphertext` (str): The ciphertext returned from the encrypt operation. 373 | 374 | **Returns:** 375 | - `str`: The base64 encoded plaintext. -------------------------------------------------------------------------------- /config.yaml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Infisical/python-sdk-official/eaf1f8225ddc14a5d35bfc35be0e519b8b8ae6f8/config.yaml -------------------------------------------------------------------------------- /git_push.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ref: https://help.github.com/articles/adding-an-existing-project-to-github-using-the-command-line/ 3 | # 4 | # Usage example: /bin/sh ./git_push.sh wing328 openapi-petstore-perl "minor update" "gitlab.com" 5 | 6 | git_user_id=$1 7 | git_repo_id=$2 8 | release_note=$3 9 | git_host=$4 10 | 11 | if [ "$git_host" = "" ]; then 12 | git_host="github.com" 13 | echo "[INFO] No command line input provided. Set \$git_host to $git_host" 14 | fi 15 | 16 | if [ "$git_user_id" = "" ]; then 17 | git_user_id="GIT_USER_ID" 18 | echo "[INFO] No command line input provided. Set \$git_user_id to $git_user_id" 19 | fi 20 | 21 | if [ "$git_repo_id" = "" ]; then 22 | git_repo_id="GIT_REPO_ID" 23 | echo "[INFO] No command line input provided. Set \$git_repo_id to $git_repo_id" 24 | fi 25 | 26 | if [ "$release_note" = "" ]; then 27 | release_note="Minor update" 28 | echo "[INFO] No command line input provided. Set \$release_note to $release_note" 29 | fi 30 | 31 | # Initialize the local directory as a Git repository 32 | git init 33 | 34 | # Adds the files in the local repository and stages them for commit. 35 | git add . 36 | 37 | # Commits the tracked changes and prepares them to be pushed to a remote repository. 38 | git commit -m "$release_note" 39 | 40 | # Sets the new remote 41 | git_remote=$(git remote) 42 | if [ "$git_remote" = "" ]; then # git remote not defined 43 | 44 | if [ "$GIT_TOKEN" = "" ]; then 45 | echo "[INFO] \$GIT_TOKEN (environment variable) is not set. Using the git credential in your environment." 46 | git remote add origin https://${git_host}/${git_user_id}/${git_repo_id}.git 47 | else 48 | git remote add origin https://${git_user_id}:"${GIT_TOKEN}"@${git_host}/${git_user_id}/${git_repo_id}.git 49 | fi 50 | 51 | fi 52 | 53 | git pull origin master 54 | 55 | # Pushes (Forces) the changes in the local repository up to the remote repository 56 | echo "Git pushing to https://${git_host}/${git_user_id}/${git_repo_id}.git" 57 | git push origin master 2>&1 | grep -v 'To https' 58 | -------------------------------------------------------------------------------- /infisical_sdk/__init__.py: -------------------------------------------------------------------------------- 1 | from .client import InfisicalSDKClient # noqa 2 | from .infisical_requests import InfisicalError # noqa 3 | from .api_types import SingleSecretResponse, ListSecretsResponse, BaseSecret, SymmetricEncryption # noqa -------------------------------------------------------------------------------- /infisical_sdk/__pycache__/__init__.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Infisical/python-sdk-official/eaf1f8225ddc14a5d35bfc35be0e519b8b8ae6f8/infisical_sdk/__pycache__/__init__.cpython-39.pyc -------------------------------------------------------------------------------- /infisical_sdk/__pycache__/aws_auth.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Infisical/python-sdk-official/eaf1f8225ddc14a5d35bfc35be0e519b8b8ae6f8/infisical_sdk/__pycache__/aws_auth.cpython-39.pyc -------------------------------------------------------------------------------- /infisical_sdk/__pycache__/client.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Infisical/python-sdk-official/eaf1f8225ddc14a5d35bfc35be0e519b8b8ae6f8/infisical_sdk/__pycache__/client.cpython-39.pyc -------------------------------------------------------------------------------- /infisical_sdk/__pycache__/universal_auth.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Infisical/python-sdk-official/eaf1f8225ddc14a5d35bfc35be0e519b8b8ae6f8/infisical_sdk/__pycache__/universal_auth.cpython-39.pyc -------------------------------------------------------------------------------- /infisical_sdk/api_types.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass, field, fields 2 | from typing import Optional, List, Any, Dict 3 | from enum import Enum 4 | import json 5 | 6 | 7 | class ApprovalStatus(str, Enum): 8 | """Enum for approval status""" 9 | OPEN = "open" 10 | APPROVED = "approved" 11 | REJECTED = "rejected" 12 | 13 | 14 | class BaseModel: 15 | """Base class for all models""" 16 | def to_dict(self) -> Dict: 17 | """Convert model to dictionary""" 18 | result = {} 19 | for key, value in self.__dict__.items(): 20 | if value is not None: # Skip None values 21 | if isinstance(value, BaseModel): 22 | result[key] = value.to_dict() 23 | elif isinstance(value, list): 24 | result[key] = [ 25 | item.to_dict() if isinstance(item, BaseModel) else item 26 | for item in value 27 | ] 28 | elif isinstance(value, Enum): 29 | result[key] = value.value 30 | else: 31 | result[key] = value 32 | return result 33 | 34 | @classmethod 35 | def from_dict(cls, data: Dict) -> 'BaseModel': 36 | """Create model from dictionary""" 37 | # Get only the fields that exist in the dataclass 38 | valid_fields = {f.name for f in fields(cls)} 39 | filtered_data = {k: v for k, v in data.items() if k in valid_fields} 40 | return cls(**filtered_data) 41 | 42 | def to_json(self) -> str: 43 | """Convert model to JSON string""" 44 | return json.dumps(self.to_dict()) 45 | 46 | @classmethod 47 | def from_json(cls, json_str: str) -> 'BaseModel': 48 | """Create model from JSON string""" 49 | data = json.loads(json_str) 50 | return cls.from_dict(data) 51 | 52 | 53 | @dataclass(frozen=True) 54 | class SecretTag(BaseModel): 55 | """Model for secret tags""" 56 | id: str 57 | slug: str 58 | name: str 59 | color: Optional[str] = None 60 | 61 | 62 | @dataclass 63 | class BaseSecret(BaseModel): 64 | """Infisical Secret""" 65 | id: str 66 | _id: str 67 | workspace: str 68 | environment: str 69 | version: int 70 | type: str 71 | secretKey: str 72 | secretValue: str 73 | secretComment: str 74 | createdAt: str 75 | updatedAt: str 76 | secretMetadata: Optional[Dict[str, Any]] = None 77 | secretValueHidden: Optional[bool] = False 78 | secretReminderNote: Optional[str] = None 79 | secretReminderRepeatDays: Optional[int] = None 80 | skipMultilineEncoding: Optional[bool] = False 81 | metadata: Optional[Any] = None 82 | secretPath: Optional[str] = None 83 | tags: List[SecretTag] = field(default_factory=list) 84 | 85 | 86 | @dataclass 87 | class Import(BaseModel): 88 | """Model for imports section""" 89 | secretPath: str 90 | environment: str 91 | folderId: Optional[str] = None 92 | secrets: List[BaseSecret] = field(default_factory=list) 93 | 94 | 95 | @dataclass 96 | class ListSecretsResponse(BaseModel): 97 | """Complete response model for secrets API""" 98 | secrets: List[BaseSecret] 99 | imports: List[Import] = field(default_factory=list) 100 | 101 | @classmethod 102 | def from_dict(cls, data: Dict) -> 'ListSecretsResponse': 103 | """Create model from dictionary with camelCase keys, handling nested objects""" 104 | return cls( 105 | secrets=[BaseSecret.from_dict(secret) for secret in data['secrets']], 106 | imports=[Import.from_dict(imp) for imp in data.get('imports', [])] 107 | ) 108 | 109 | 110 | @dataclass 111 | class SingleSecretResponse(BaseModel): 112 | """Response model for get secret API""" 113 | secret: BaseSecret 114 | 115 | @classmethod 116 | def from_dict(cls, data: Dict) -> 'SingleSecretResponse': 117 | return cls( 118 | secret=BaseSecret.from_dict(data['secret']), 119 | ) 120 | 121 | 122 | @dataclass 123 | class MachineIdentityLoginResponse(BaseModel): 124 | """Response model for machine identity login API""" 125 | accessToken: str 126 | expiresIn: int 127 | accessTokenMaxTTL: int 128 | tokenType: str 129 | 130 | 131 | class SymmetricEncryption(str, Enum): 132 | AES_GCM_256 = "aes-256-gcm" 133 | AES_GCM_128 = "aes-128-gcm" 134 | 135 | 136 | class OrderDirection(str, Enum): 137 | ASC = "asc" 138 | DESC = "desc" 139 | 140 | 141 | class KmsKeysOrderBy(str, Enum): 142 | NAME = "name" 143 | 144 | 145 | @dataclass 146 | class KmsKey(BaseModel): 147 | """Infisical KMS Key""" 148 | id: str 149 | description: str 150 | isDisabled: bool 151 | orgId: str 152 | name: str 153 | createdAt: str 154 | updatedAt: str 155 | projectId: str 156 | version: int 157 | encryptionAlgorithm: SymmetricEncryption 158 | 159 | 160 | @dataclass 161 | class ListKmsKeysResponse(BaseModel): 162 | """Complete response model for Kms Keys API""" 163 | keys: List[KmsKey] 164 | totalCount: int 165 | 166 | @classmethod 167 | def from_dict(cls, data: Dict) -> 'ListKmsKeysResponse': 168 | """Create model from dictionary with camelCase keys, handling nested objects""" 169 | return cls( 170 | keys=[KmsKey.from_dict(key) for key in data['keys']], 171 | totalCount=data['totalCount'] 172 | ) 173 | 174 | 175 | @dataclass 176 | class SingleKmsKeyResponse(BaseModel): 177 | """Response model for get/create/update/delete API""" 178 | key: KmsKey 179 | 180 | @classmethod 181 | def from_dict(cls, data: Dict) -> 'SingleKmsKeyResponse': 182 | return cls( 183 | key=KmsKey.from_dict(data['key']), 184 | ) 185 | 186 | 187 | @dataclass 188 | class KmsKeyEncryptDataResponse(BaseModel): 189 | """Response model for encrypt data API""" 190 | ciphertext: str 191 | 192 | 193 | @dataclass 194 | class KmsKeyDecryptDataResponse(BaseModel): 195 | """Response model for decrypt data API""" 196 | plaintext: str 197 | -------------------------------------------------------------------------------- /infisical_sdk/client.py: -------------------------------------------------------------------------------- 1 | from .infisical_requests import InfisicalRequests 2 | 3 | from infisical_sdk.resources import Auth 4 | from infisical_sdk.resources import V3RawSecrets 5 | from infisical_sdk.resources import KMS 6 | 7 | from infisical_sdk.util import SecretsCache 8 | 9 | class InfisicalSDKClient: 10 | def __init__(self, host: str, token: str = None, cache_ttl: int = 60): 11 | """ 12 | Initialize the Infisical SDK client. 13 | 14 | :param str host: The host URL for your Infisical instance. Will default to `https://app.infisical.com` if not specified. 15 | :param str token: The authentication token for the client. If not specified, you can use the `auth` methods to authenticate. 16 | :param int cache_ttl: The time to live for the secrets cache. This is the number of seconds that secrets fetched from the API will be cached for. Set to `None` to disable caching. Defaults to `60` seconds. 17 | """ 18 | 19 | self.host = host 20 | self.access_token = token 21 | 22 | self.api = InfisicalRequests(host=host, token=token) 23 | self.cache = SecretsCache(cache_ttl) 24 | self.auth = Auth(self.api, self.set_token) 25 | self.secrets = V3RawSecrets(self.api, self.cache) 26 | self.kms = KMS(self.api) 27 | 28 | def set_token(self, token: str): 29 | """ 30 | Set the access token for future requests. 31 | """ 32 | self.api.set_token(token) 33 | self.access_token = token 34 | 35 | def get_token(self): 36 | """ 37 | Set the access token for future requests. 38 | """ 39 | return self.access_token 40 | 41 | -------------------------------------------------------------------------------- /infisical_sdk/infisical_requests.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict, Generic, Optional, TypeVar, Type, Callable, List 2 | import socket 3 | import requests 4 | import functools 5 | from dataclasses import dataclass 6 | import time 7 | import random 8 | 9 | T = TypeVar("T") 10 | 11 | # List of network-related exceptions that should trigger retries 12 | NETWORK_ERRORS = [ 13 | requests.exceptions.ConnectionError, 14 | requests.exceptions.ChunkedEncodingError, 15 | requests.exceptions.ReadTimeout, 16 | requests.exceptions.ConnectTimeout, 17 | socket.gaierror, 18 | socket.timeout, 19 | ConnectionResetError, 20 | ConnectionRefusedError, 21 | ConnectionError, 22 | ConnectionAbortedError, 23 | ] 24 | 25 | def join_url(base: str, path: str) -> str: 26 | """ 27 | Join base URL and path properly, handling slashes appropriately. 28 | """ 29 | if not base.endswith('/'): 30 | base += '/' 31 | return base + path.lstrip('/') 32 | 33 | class InfisicalError(Exception): 34 | """Base exception for Infisical client errors""" 35 | pass 36 | 37 | 38 | class APIError(InfisicalError): 39 | """API-specific errors""" 40 | def __init__(self, message: str, status_code: int, response: Dict[str, Any]): 41 | self.status_code = status_code 42 | self.response = response 43 | super().__init__(f"{message} (Status: {status_code})") 44 | 45 | 46 | @dataclass 47 | class APIResponse(Generic[T]): 48 | """Generic API response wrapper""" 49 | data: T 50 | status_code: int 51 | headers: Dict[str, str] 52 | 53 | def to_dict(self) -> Dict: 54 | """Convert to dictionary with camelCase keys""" 55 | return { 56 | 'data': self.data.to_dict() if hasattr(self.data, 'to_dict') else self.data, 57 | 'statusCode': self.status_code, 58 | 'headers': self.headers 59 | } 60 | 61 | @classmethod 62 | def from_dict(cls, data: Dict) -> 'APIResponse[T]': 63 | """Create from dictionary with camelCase keys""" 64 | return cls( 65 | data=data['data'], 66 | status_code=data['statusCode'], 67 | headers=data['headers'] 68 | ) 69 | 70 | def with_retry( 71 | max_retries: int = 3, 72 | base_delay: float = 1.0, 73 | network_errors: Optional[List[Type[Exception]]] = None 74 | ) -> Callable: 75 | """ 76 | Decorator to add retry logic with exponential backoff to requests methods. 77 | """ 78 | if network_errors is None: 79 | network_errors = NETWORK_ERRORS 80 | 81 | def decorator(func: Callable) -> Callable: 82 | @functools.wraps(func) 83 | def wrapper(*args, **kwargs): 84 | retry_count = 0 85 | 86 | while True: 87 | try: 88 | return func(*args, **kwargs) 89 | except tuple(network_errors) as error: 90 | retry_count += 1 91 | if retry_count > max_retries: 92 | raise 93 | 94 | base_delay_with_backoff = base_delay * (2 ** (retry_count - 1)) 95 | 96 | # +/-20% jitter 97 | jitter = random.uniform(-0.2, 0.2) * base_delay_with_backoff 98 | delay = base_delay_with_backoff + jitter 99 | 100 | time.sleep(delay) 101 | 102 | return wrapper 103 | 104 | return decorator 105 | 106 | 107 | class InfisicalRequests: 108 | def __init__(self, host: str, token: Optional[str] = None): 109 | self.host = host.rstrip("/") 110 | self.session = requests.Session() 111 | 112 | # Set common headers 113 | self.session.headers.update({ 114 | "Content-Type": "application/json", 115 | "Accept": "application/json", 116 | }) 117 | 118 | if token: 119 | self.set_token(token) 120 | 121 | def _build_url(self, path: str) -> str: 122 | """Construct full URL from path""" 123 | return join_url(self.host, path.lstrip("/")) 124 | 125 | def set_token(self, token: str): 126 | """Set authorization token""" 127 | self.session.headers["Authorization"] = f"Bearer {token}" 128 | 129 | def _handle_response(self, response: requests.Response) -> Dict[str, Any]: 130 | """Handle API response and raise appropriate errors""" 131 | try: 132 | response.raise_for_status() 133 | return response.json() 134 | except requests.exceptions.HTTPError: 135 | try: 136 | error_data = response.json() 137 | except ValueError: 138 | error_data = {"message": response.text} 139 | 140 | raise APIError( 141 | message=error_data.get("message", "Unknown error"), 142 | status_code=response.status_code, 143 | response=error_data 144 | ) 145 | except requests.exceptions.RequestException as e: 146 | raise InfisicalError(f"Request failed: {str(e)}") 147 | except ValueError: 148 | raise InfisicalError("Invalid JSON response") 149 | 150 | @with_retry(max_retries=4, base_delay=1.0) 151 | def get( 152 | self, 153 | path: str, 154 | model: Type[T], 155 | params: Optional[Dict[str, Any]] = None 156 | ) -> APIResponse[T]: 157 | 158 | """ 159 | Make a GET request and parse response into given model 160 | 161 | Args: 162 | path: API endpoint path 163 | model: model class to parse response into 164 | params: Optional query parameters 165 | """ 166 | response = self.session.get(self._build_url(path), params=params) 167 | data = self._handle_response(response) 168 | 169 | parsed_data = model.from_dict(data) if hasattr(model, 'from_dict') else data 170 | 171 | return APIResponse( 172 | data=parsed_data, 173 | status_code=response.status_code, 174 | headers=dict(response.headers) 175 | ) 176 | 177 | @with_retry(max_retries=4, base_delay=1.0) 178 | def post( 179 | self, 180 | path: str, 181 | model: Type[T], 182 | json: Optional[Dict[str, Any]] = None 183 | ) -> APIResponse[T]: 184 | 185 | """Make a POST request with JSON data""" 186 | 187 | if json is not None: 188 | # Filter out None values 189 | json = {k: v for k, v in json.items() if v is not None} 190 | 191 | response = self.session.post(self._build_url(path), json=json) 192 | data = self._handle_response(response) 193 | 194 | parsed_data = model.from_dict(data) if hasattr(model, 'from_dict') else data 195 | 196 | return APIResponse( 197 | data=parsed_data, 198 | status_code=response.status_code, 199 | headers=dict(response.headers) 200 | ) 201 | 202 | @with_retry(max_retries=4, base_delay=1.0) 203 | def patch( 204 | self, 205 | path: str, 206 | model: Type[T], 207 | json: Optional[Dict[str, Any]] = None 208 | ) -> APIResponse[T]: 209 | 210 | """Make a PATCH request with JSON data""" 211 | 212 | if json is not None: 213 | # Filter out None values 214 | json = {k: v for k, v in json.items() if v is not None} 215 | 216 | response = self.session.patch(self._build_url(path), json=json) 217 | data = self._handle_response(response) 218 | 219 | parsed_data = model.from_dict(data) if hasattr(model, 'from_dict') else data 220 | 221 | return APIResponse( 222 | data=parsed_data, 223 | status_code=response.status_code, 224 | headers=dict(response.headers) 225 | ) 226 | 227 | @with_retry(max_retries=4, base_delay=1.0) 228 | def delete( 229 | self, 230 | path: str, 231 | model: Type[T], 232 | json: Optional[Dict[str, Any]] = None 233 | ) -> APIResponse[T]: 234 | 235 | """Make a PATCH request with JSON data""" 236 | 237 | if json is not None: 238 | # Filter out None values 239 | json = {k: v for k, v in json.items() if v is not None} 240 | 241 | response = self.session.delete(self._build_url(path), json=json) 242 | data = self._handle_response(response) 243 | 244 | parsed_data = model.from_dict(data) if hasattr(model, 'from_dict') else data 245 | 246 | return APIResponse( 247 | data=parsed_data, 248 | status_code=response.status_code, 249 | headers=dict(response.headers) 250 | ) 251 | -------------------------------------------------------------------------------- /infisical_sdk/requirements.txt: -------------------------------------------------------------------------------- 1 | requests~=2.32.3 2 | boto3~=1.35.0 3 | botocore~=1.35.0 -------------------------------------------------------------------------------- /infisical_sdk/resources/__init__.py: -------------------------------------------------------------------------------- 1 | from .secrets import V3RawSecrets 2 | from .kms import KMS 3 | from .auth import Auth -------------------------------------------------------------------------------- /infisical_sdk/resources/auth.py: -------------------------------------------------------------------------------- 1 | from infisical_sdk.infisical_requests import InfisicalRequests 2 | from infisical_sdk.resources.auth_methods import AWSAuth 3 | from infisical_sdk.resources.auth_methods import UniversalAuth 4 | 5 | from typing import Callable 6 | class Auth: 7 | def __init__(self, requests: InfisicalRequests, setToken: Callable[[str], None]): 8 | self.requests = requests 9 | self.aws_auth = AWSAuth(requests, setToken) 10 | self.universal_auth = UniversalAuth(requests, setToken) -------------------------------------------------------------------------------- /infisical_sdk/resources/auth_methods/__init__.py: -------------------------------------------------------------------------------- 1 | from .aws_auth import AWSAuth 2 | from .universal_auth import UniversalAuth 3 | -------------------------------------------------------------------------------- /infisical_sdk/resources/auth_methods/aws_auth.py: -------------------------------------------------------------------------------- 1 | from botocore.auth import SigV4Auth 2 | from botocore.awsrequest import AWSRequest 3 | from botocore.exceptions import NoCredentialsError 4 | 5 | from infisical_sdk.infisical_requests import InfisicalRequests 6 | from infisical_sdk.api_types import MachineIdentityLoginResponse 7 | 8 | from typing import Callable 9 | 10 | import requests 11 | import boto3 12 | import base64 13 | import json 14 | import os 15 | import datetime 16 | 17 | from typing import Dict, Any 18 | 19 | 20 | class AWSAuth: 21 | def __init__(self, requests: InfisicalRequests, setToken: Callable[[str], None]) -> None: 22 | self.requests = requests 23 | self.setToken = setToken 24 | 25 | def login(self, identity_id: str) -> MachineIdentityLoginResponse: 26 | """ 27 | Login with AWS Authentication. 28 | 29 | Args: 30 | identity_id (str): Your Machine Identity ID that has AWS Auth configured. 31 | 32 | Returns: 33 | Dict: A dictionary containing the access token and related information. 34 | """ 35 | 36 | identity_id = identity_id or os.getenv("INFISICAL_AWS_IAM_AUTH_IDENTITY_ID") 37 | if not identity_id: 38 | raise ValueError( 39 | "Identity ID must be provided or set in the environment variable" + 40 | "INFISICAL_AWS_IAM_AUTH_IDENTITY_ID." 41 | ) 42 | 43 | aws_region = self.get_aws_region() 44 | session = boto3.Session(region_name=aws_region) 45 | 46 | credentials = self._get_aws_credentials(session) 47 | 48 | iam_request_url = f"https://sts.{aws_region}.amazonaws.com/" 49 | iam_request_body = "Action=GetCallerIdentity&Version=2011-06-15" 50 | 51 | request_headers = self._prepare_aws_request( 52 | iam_request_url, 53 | iam_request_body, 54 | credentials, 55 | aws_region 56 | ) 57 | 58 | requestBody = { 59 | "identityId": identity_id, 60 | "iamRequestBody": base64.b64encode(iam_request_body.encode()).decode(), 61 | "iamRequestHeaders": base64.b64encode(json.dumps(request_headers).encode()).decode(), 62 | "iamHttpRequestMethod": "POST" 63 | } 64 | 65 | result = self.requests.post( 66 | path="/api/v1/auth/aws-auth/login", 67 | json=requestBody, 68 | model=MachineIdentityLoginResponse 69 | ) 70 | 71 | self.setToken(result.data.accessToken) 72 | 73 | return result.data 74 | 75 | def _get_aws_credentials(self, session: boto3.Session) -> Any: 76 | try: 77 | credentials = session.get_credentials() 78 | if credentials is None: 79 | raise NoCredentialsError("AWS credentials not found.") 80 | return credentials.get_frozen_credentials() 81 | except NoCredentialsError as e: 82 | raise RuntimeError(f"AWS IAM Auth Login failed: {str(e)}") 83 | 84 | def _prepare_aws_request( 85 | self, 86 | url: str, 87 | body: str, 88 | credentials: Any, 89 | region: str) -> Dict[str, str]: 90 | 91 | current_time = datetime.datetime.now(datetime.timezone.utc) 92 | amz_date = current_time.strftime('%Y%m%dT%H%M%SZ') 93 | 94 | request = AWSRequest(method="POST", url=url, data=body) 95 | request.headers["X-Amz-Date"] = amz_date 96 | request.headers["Host"] = f"sts.{region}.amazonaws.com" 97 | request.headers["Content-Type"] = "application/x-www-form-urlencoded; charset=utf-8" 98 | request.headers["Content-Length"] = str(len(body)) 99 | 100 | signer = SigV4Auth(credentials, "sts", region) 101 | signer.add_auth(request) 102 | 103 | return {k: v for k, v in request.headers.items() if k.lower() != "content-length"} 104 | 105 | @staticmethod 106 | def get_aws_region() -> str: 107 | region = os.getenv("AWS_REGION") # Typically found in lambda runtime environment 108 | if region: 109 | return region 110 | 111 | try: 112 | return AWSAuth._get_aws_ec2_identity_document_region() 113 | except Exception as e: 114 | raise Exception("Failed to retrieve AWS region") from e 115 | 116 | @staticmethod 117 | def _get_aws_ec2_identity_document_region(timeout: int = 5000) -> str: 118 | session = requests.Session() 119 | token_response = session.put( 120 | "http://169.254.169.254/latest/api/token", 121 | headers={"X-aws-ec2-metadata-token-ttl-seconds": "21600"}, 122 | timeout=timeout / 1000 123 | ) 124 | token_response.raise_for_status() 125 | metadata_token = token_response.text 126 | 127 | identity_response = session.get( 128 | "http://169.254.169.254/latest/dynamic/instance-identity/document", 129 | headers={"X-aws-ec2-metadata-token": metadata_token, "Accept": "application/json"}, 130 | timeout=timeout / 1000 131 | ) 132 | 133 | identity_response.raise_for_status() 134 | return identity_response.json().get("region") -------------------------------------------------------------------------------- /infisical_sdk/resources/auth_methods/universal_auth.py: -------------------------------------------------------------------------------- 1 | from infisical_sdk.api_types import MachineIdentityLoginResponse 2 | 3 | from typing import Callable 4 | from infisical_sdk.infisical_requests import InfisicalRequests 5 | class UniversalAuth: 6 | def __init__(self, requests: InfisicalRequests, setToken: Callable[[str], None]): 7 | self.requests = requests 8 | self.setToken = setToken 9 | 10 | def login(self, client_id: str, client_secret: str) -> MachineIdentityLoginResponse: 11 | """ 12 | Login with Universal Auth. 13 | 14 | Args: 15 | client_id (str): Your Machine Identity Client ID. 16 | client_secret (str): Your Machine Identity Client Secret. 17 | 18 | Returns: 19 | Dict: A dictionary containing the access token and related information. 20 | """ 21 | 22 | requestBody = { 23 | "clientId": client_id, 24 | "clientSecret": client_secret 25 | } 26 | 27 | result = self.requests.post( 28 | path="/api/v1/auth/universal-auth/login", 29 | json=requestBody, 30 | model=MachineIdentityLoginResponse 31 | ) 32 | 33 | self.setToken(result.data.accessToken) 34 | 35 | return result.data -------------------------------------------------------------------------------- /infisical_sdk/resources/kms.py: -------------------------------------------------------------------------------- 1 | from infisical_sdk.api_types import SymmetricEncryption, KmsKeysOrderBy, OrderDirection 2 | from infisical_sdk.api_types import ListKmsKeysResponse, SingleKmsKeyResponse 3 | from infisical_sdk.api_types import KmsKey, KmsKeyEncryptDataResponse, KmsKeyDecryptDataResponse 4 | 5 | from infisical_sdk.infisical_requests import InfisicalRequests 6 | 7 | 8 | class KMS: 9 | def __init__(self, requests: InfisicalRequests) -> None: 10 | self.requests = requests 11 | 12 | def list_keys( 13 | self, 14 | project_id: str, 15 | offset: int = 0, 16 | limit: int = 100, 17 | order_by: KmsKeysOrderBy = KmsKeysOrderBy.NAME, 18 | order_direction: OrderDirection = OrderDirection.ASC, 19 | search: str = None) -> ListKmsKeysResponse: 20 | 21 | params = { 22 | "projectId": project_id, 23 | "search": search, 24 | "offset": offset, 25 | "limit": limit, 26 | "orderBy": order_by, 27 | "orderDirection": order_direction, 28 | } 29 | 30 | result = self.requests.get( 31 | path="/api/v1/kms/keys", 32 | params=params, 33 | model=ListKmsKeysResponse 34 | ) 35 | 36 | return result.data 37 | 38 | def get_key_by_id( 39 | self, 40 | key_id: str) -> KmsKey: 41 | 42 | result = self.requests.get( 43 | path=f"/api/v1/kms/keys/{key_id}", 44 | model=SingleKmsKeyResponse 45 | ) 46 | 47 | return result.data.key 48 | 49 | def get_key_by_name( 50 | self, 51 | key_name: str, 52 | project_id: str) -> KmsKey: 53 | 54 | params = { 55 | "projectId": project_id, 56 | } 57 | 58 | result = self.requests.get( 59 | path=f"/api/v1/kms/keys/key-name/{key_name}", 60 | params=params, 61 | model=SingleKmsKeyResponse 62 | ) 63 | 64 | return result.data.key 65 | 66 | def create_key( 67 | self, 68 | name: str, 69 | project_id: str, 70 | encryption_algorithm: SymmetricEncryption, 71 | description: str = None) -> KmsKey: 72 | 73 | request_body = { 74 | "name": name, 75 | "projectId": project_id, 76 | "encryptionAlgorithm": encryption_algorithm, 77 | "description": description, 78 | } 79 | 80 | result = self.requests.post( 81 | path="/api/v1/kms/keys", 82 | json=request_body, 83 | model=SingleKmsKeyResponse 84 | ) 85 | 86 | return result.data.key 87 | 88 | def update_key( 89 | self, 90 | key_id: str, 91 | name: str = None, 92 | is_disabled: bool = None, 93 | description: str = None) -> KmsKey: 94 | 95 | request_body = { 96 | "name": name, 97 | "isDisabled": is_disabled, 98 | "description": description, 99 | } 100 | 101 | result = self.requests.patch( 102 | path=f"/api/v1/kms/keys/{key_id}", 103 | json=request_body, 104 | model=SingleKmsKeyResponse 105 | ) 106 | 107 | return result.data.key 108 | 109 | def delete_key( 110 | self, 111 | key_id: str) -> KmsKey: 112 | 113 | result = self.requests.delete( 114 | path=f"/api/v1/kms/keys/{key_id}", 115 | json={}, 116 | model=SingleKmsKeyResponse 117 | ) 118 | 119 | return result.data.key 120 | 121 | def encrypt_data( 122 | self, 123 | key_id: str, 124 | base64EncodedPlaintext: str) -> str: 125 | """ 126 | Encrypt data with the specified KMS key. 127 | 128 | :param key_id: The ID of the key to decrypt the ciphertext with 129 | :type key_id: str 130 | :param base64EncodedPlaintext: The base64 encoded plaintext to encrypt 131 | :type plaintext: str 132 | 133 | 134 | :return: The encrypted base64 encoded plaintext (ciphertext) 135 | :rtype: str 136 | """ 137 | 138 | request_body = { 139 | "plaintext": base64EncodedPlaintext 140 | } 141 | 142 | result = self.requests.post( 143 | path=f"/api/v1/kms/keys/{key_id}/encrypt", 144 | json=request_body, 145 | model=KmsKeyEncryptDataResponse 146 | ) 147 | 148 | return result.data.ciphertext 149 | 150 | def decrypt_data( 151 | self, 152 | key_id: str, 153 | ciphertext: str) -> str: 154 | """ 155 | Decrypt data with the specified KMS key. 156 | 157 | :param key_id: The ID of the key to decrypt the ciphertext with 158 | :type key_id: str 159 | :param ciphertext: The encrypted base64 plaintext to decrypt 160 | :type ciphertext: str 161 | 162 | 163 | :return: The base64 encoded plaintext 164 | :rtype: str 165 | """ 166 | 167 | request_body = { 168 | "ciphertext": ciphertext 169 | } 170 | 171 | result = self.requests.post( 172 | path=f"/api/v1/kms/keys/{key_id}/decrypt", 173 | json=request_body, 174 | model=KmsKeyDecryptDataResponse 175 | ) 176 | 177 | return result.data.plaintext 178 | -------------------------------------------------------------------------------- /infisical_sdk/resources/secrets.py: -------------------------------------------------------------------------------- 1 | from typing import List, Union 2 | 3 | from infisical_sdk.infisical_requests import InfisicalRequests 4 | from infisical_sdk.api_types import ListSecretsResponse, SingleSecretResponse, BaseSecret 5 | from infisical_sdk.util import SecretsCache 6 | 7 | CACHE_KEY_LIST_SECRETS = "cache-list-secrets" 8 | CACHE_KEY_SINGLE_SECRET = "cache-single-secret" 9 | 10 | class V3RawSecrets: 11 | def __init__(self, requests: InfisicalRequests, cache: SecretsCache) -> None: 12 | self.requests = requests 13 | self.cache = cache 14 | 15 | def list_secrets( 16 | self, 17 | project_id: str, 18 | environment_slug: str, 19 | secret_path: str, 20 | expand_secret_references: bool = True, 21 | view_secret_value: bool = True, 22 | recursive: bool = False, 23 | include_imports: bool = True, 24 | tag_filters: List[str] = []) -> ListSecretsResponse: 25 | 26 | params = { 27 | "workspaceId": project_id, 28 | "environment": environment_slug, 29 | "secretPath": secret_path, 30 | "viewSecretValue": str(view_secret_value).lower(), 31 | "expandSecretReferences": str(expand_secret_references).lower(), 32 | "recursive": str(recursive).lower(), 33 | "include_imports": str(include_imports).lower(), 34 | } 35 | 36 | if tag_filters: 37 | params["tagSlugs"] = ",".join(tag_filters) 38 | 39 | 40 | cache_key = self.cache.compute_cache_key(CACHE_KEY_LIST_SECRETS, **params) 41 | if self.cache.enabled: 42 | cached_response = self.cache.get(cache_key) 43 | 44 | if cached_response is not None and isinstance(cached_response, ListSecretsResponse): 45 | return cached_response 46 | 47 | result = self.requests.get( 48 | path="/api/v3/secrets/raw", 49 | params=params, 50 | model=ListSecretsResponse 51 | ) 52 | 53 | if self.cache.enabled: 54 | self.cache.set(cache_key, result.data) 55 | 56 | return result.data 57 | 58 | def get_secret_by_name( 59 | self, 60 | secret_name: str, 61 | project_id: str, 62 | environment_slug: str, 63 | secret_path: str, 64 | expand_secret_references: bool = True, 65 | include_imports: bool = True, 66 | view_secret_value: bool = True, 67 | version: str = None) -> BaseSecret: 68 | 69 | params = { 70 | "workspaceId": project_id, 71 | "viewSecretValue": str(view_secret_value).lower(), 72 | "environment": environment_slug, 73 | "secretPath": secret_path, 74 | "expandSecretReferences": str(expand_secret_references).lower(), 75 | "include_imports": str(include_imports).lower(), 76 | "version": version 77 | } 78 | 79 | cache_params = { 80 | "project_id": project_id, 81 | "environment_slug": environment_slug, 82 | "secret_path": secret_path, 83 | "secret_name": secret_name, 84 | } 85 | 86 | cache_key = self.cache.compute_cache_key(CACHE_KEY_SINGLE_SECRET, **cache_params) 87 | 88 | if self.cache.enabled: 89 | cached_response = self.cache.get(cache_key) 90 | 91 | if cached_response is not None and isinstance(cached_response, BaseSecret): 92 | return cached_response 93 | 94 | result = self.requests.get( 95 | path=f"/api/v3/secrets/raw/{secret_name}", 96 | params=params, 97 | model=SingleSecretResponse 98 | ) 99 | 100 | if self.cache.enabled: 101 | self.cache.set(cache_key, result.data.secret) 102 | 103 | return result.data.secret 104 | 105 | def create_secret_by_name( 106 | self, 107 | secret_name: str, 108 | project_id: str, 109 | secret_path: str, 110 | environment_slug: str, 111 | secret_value: str = None, 112 | secret_comment: str = None, 113 | skip_multiline_encoding: bool = False, 114 | secret_reminder_repeat_days: Union[float, int] = None, 115 | secret_reminder_note: str = None) -> BaseSecret: 116 | 117 | requestBody = { 118 | "workspaceId": project_id, 119 | "environment": environment_slug, 120 | "secretPath": secret_path, 121 | "secretValue": secret_value, 122 | "secretComment": secret_comment, 123 | "tagIds": None, 124 | "skipMultilineEncoding": skip_multiline_encoding, 125 | "type": "shared", 126 | "secretReminderRepeatDays": secret_reminder_repeat_days, 127 | "secretReminderNote": secret_reminder_note 128 | } 129 | result = self.requests.post( 130 | path=f"/api/v3/secrets/raw/{secret_name}", 131 | json=requestBody, 132 | model=SingleSecretResponse 133 | ) 134 | 135 | 136 | if self.cache.enabled: 137 | cache_params = { 138 | "project_id": project_id, 139 | "environment_slug": environment_slug, 140 | "secret_path": secret_path, 141 | "secret_name": secret_name, 142 | } 143 | 144 | cache_key = self.cache.compute_cache_key(CACHE_KEY_SINGLE_SECRET, **cache_params) 145 | self.cache.set(cache_key, result.data.secret) 146 | 147 | # Invalidates all list secret cache 148 | self.cache.invalidate_operation(CACHE_KEY_LIST_SECRETS) 149 | 150 | return result.data.secret 151 | 152 | def update_secret_by_name( 153 | self, 154 | current_secret_name: str, 155 | project_id: str, 156 | secret_path: str, 157 | environment_slug: str, 158 | secret_value: str = None, 159 | secret_comment: str = None, 160 | skip_multiline_encoding: bool = False, 161 | secret_reminder_repeat_days: Union[float, int] = None, 162 | secret_reminder_note: str = None, 163 | new_secret_name: str = None) -> BaseSecret: 164 | 165 | requestBody = { 166 | "workspaceId": project_id, 167 | "environment": environment_slug, 168 | "secretPath": secret_path, 169 | "secretValue": secret_value, 170 | "secretComment": secret_comment, 171 | "newSecretName": new_secret_name, 172 | "tagIds": None, 173 | "skipMultilineEncoding": skip_multiline_encoding, 174 | "type": "shared", 175 | "secretReminderRepeatDays": secret_reminder_repeat_days, 176 | "secretReminderNote": secret_reminder_note 177 | } 178 | 179 | result = self.requests.patch( 180 | path=f"/api/v3/secrets/raw/{current_secret_name}", 181 | json=requestBody, 182 | model=SingleSecretResponse 183 | ) 184 | 185 | if self.cache.enabled: 186 | cache_params = { 187 | "project_id": project_id, 188 | "environment_slug": environment_slug, 189 | "secret_path": secret_path, 190 | "secret_name": current_secret_name, 191 | } 192 | 193 | cache_key = self.cache.compute_cache_key(CACHE_KEY_SINGLE_SECRET, **cache_params) 194 | self.cache.unset(cache_key) 195 | 196 | # Invalidates all list secret cache 197 | self.cache.invalidate_operation(CACHE_KEY_LIST_SECRETS) 198 | 199 | return result.data.secret 200 | 201 | def delete_secret_by_name( 202 | self, 203 | secret_name: str, 204 | project_id: str, 205 | secret_path: str, 206 | environment_slug: str) -> BaseSecret: 207 | 208 | requestBody = { 209 | "workspaceId": project_id, 210 | "environment": environment_slug, 211 | "secretPath": secret_path, 212 | "type": "shared", 213 | } 214 | 215 | result = self.requests.delete( 216 | path=f"/api/v3/secrets/raw/{secret_name}", 217 | json=requestBody, 218 | model=SingleSecretResponse 219 | ) 220 | 221 | if self.cache.enabled: 222 | cache_params = { 223 | "project_id": project_id, 224 | "environment_slug": environment_slug, 225 | "secret_path": secret_path, 226 | "secret_name": secret_name, 227 | } 228 | 229 | cache_key = self.cache.compute_cache_key(CACHE_KEY_SINGLE_SECRET, **cache_params) 230 | self.cache.unset(cache_key) 231 | 232 | # Invalidates all list secret cache 233 | self.cache.invalidate_operation(CACHE_KEY_LIST_SECRETS) 234 | 235 | return result.data.secret -------------------------------------------------------------------------------- /infisical_sdk/util/__init__.py: -------------------------------------------------------------------------------- 1 | from .secrets_cache import SecretsCache -------------------------------------------------------------------------------- /infisical_sdk/util/secrets_cache.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, Tuple, Any 2 | 3 | from infisical_sdk.api_types import BaseSecret 4 | import json 5 | import time 6 | import threading 7 | from hashlib import sha256 8 | import pickle 9 | 10 | MAX_CACHE_SIZE = 1000 11 | 12 | class SecretsCache: 13 | def __init__(self, ttl_seconds: int = 60) -> None: 14 | if ttl_seconds is None or ttl_seconds <= 0: 15 | self.enabled = False 16 | return 17 | 18 | self.enabled = True 19 | self.ttl = ttl_seconds 20 | self.cleanup_interval = 60 21 | 22 | self.cache: Dict[str, Tuple[bytes, float]] = {} 23 | 24 | self.lock = threading.RLock() 25 | 26 | self.stop_cleanup_thread = False 27 | self.cleanup_thread = threading.Thread(target=self._cleanup_worker, daemon=True) 28 | self.cleanup_thread.start() 29 | 30 | def compute_cache_key(self, operation_name: str, **kwargs) -> str: 31 | sorted_kwargs = sorted(kwargs.items()) 32 | json_str = json.dumps(sorted_kwargs) 33 | 34 | return f"{operation_name}-{sha256(json_str.encode()).hexdigest()}" 35 | 36 | def get(self, cache_key: str) -> Any: 37 | if not self.enabled: 38 | return None 39 | 40 | with self.lock: 41 | if cache_key in self.cache: 42 | serialized_value, timestamp = self.cache[cache_key] 43 | if time.time() - timestamp <= self.ttl: 44 | return pickle.loads(serialized_value) 45 | else: 46 | self.cache.pop(cache_key, None) 47 | return None 48 | else: 49 | return None 50 | 51 | 52 | def set(self, cache_key: str, value: Any) -> None: 53 | if not self.enabled: 54 | return 55 | 56 | with self.lock: 57 | serialized_value = pickle.dumps(value) 58 | self.cache[cache_key] = (serialized_value, time.time()) 59 | 60 | if len(self.cache) > MAX_CACHE_SIZE: 61 | oldest_key = min(self.cache.keys(), key=lambda k: self.cache[k][1]) # oldest key based on timestamp 62 | self.cache.pop(oldest_key) 63 | 64 | 65 | 66 | def unset(self, cache_key: str) -> None: 67 | if not self.enabled: 68 | return 69 | 70 | with self.lock: 71 | self.cache.pop(cache_key, None) 72 | 73 | def invalidate_operation(self, operation_name: str) -> None: 74 | if not self.enabled: 75 | return 76 | 77 | with self.lock: 78 | for key in list(self.cache.keys()): 79 | if key.startswith(operation_name): 80 | self.cache.pop(key, None) 81 | 82 | 83 | def _cleanup_expired_items(self) -> None: 84 | """Remove all expired items from the cache.""" 85 | current_time = time.time() 86 | with self.lock: 87 | expired_keys = [ 88 | key for key, (_, timestamp) in self.cache.items() 89 | if current_time - timestamp > self.ttl 90 | ] 91 | for key in expired_keys: 92 | self.cache.pop(key, None) 93 | 94 | def _cleanup_worker(self) -> None: 95 | """Background worker that periodically cleans up expired items.""" 96 | while not self.stop_cleanup_thread: 97 | time.sleep(self.cleanup_interval) 98 | self._cleanup_expired_items() 99 | 100 | def __del__(self) -> None: 101 | """Ensure thread is properly stopped when the object is garbage collected.""" 102 | self.stop_cleanup_thread = True 103 | if self.enabled and self.cleanup_thread.is_alive(): 104 | self.cleanup_thread.join(timeout=1.0) 105 | 106 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "infisicalapi_client" 3 | version = "1.0.0" 4 | description = "Infisical API" 5 | authors = ["OpenAPI Generator Community "] 6 | license = "NoLicense" 7 | readme = "README.md" 8 | repository = "https://github.com/GIT_USER_ID/GIT_REPO_ID" 9 | keywords = ["OpenAPI", "OpenAPI-Generator", "Infisical API"] 10 | include = ["infisicalapi_client/py.typed"] 11 | 12 | [tool.poetry.dependencies] 13 | python = "^3.7" 14 | 15 | python-dateutil = ">=2.8.2" 16 | aenum = ">=3.1.11" 17 | 18 | [tool.poetry.dev-dependencies] 19 | pytest = ">=7.2.1" 20 | tox = ">=3.9.0" 21 | flake8 = ">=4.0.0" 22 | 23 | [build-system] 24 | requires = ["setuptools"] 25 | build-backend = "setuptools.build_meta" -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | python_dateutil >= 2.5.3 2 | setuptools >= 21.0.0 3 | aenum >= 3.1.11 4 | requests >= 2.31.0 5 | boto3 >= 1.33.8 6 | botocore >= 1.33.8 7 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length=99 3 | exclude = .venv,test.py,sink -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | """ 4 | Infisical SDK 5 | 6 | List of all available APIs that can be consumed 7 | """ # noqa: E501 8 | 9 | from setuptools import setup, find_packages # noqa: H301 10 | 11 | # To install the library, run the following 12 | # 13 | # python setup.py install 14 | # 15 | # prerequisite: setuptools 16 | # http://pypi.python.org/pypi/setuptools 17 | NAME = "infisicalsdk" 18 | VERSION = "1.0.1" 19 | PYTHON_REQUIRES = ">=3.8" 20 | REQUIRES = [ 21 | "python-dateutil", 22 | "aenum", 23 | "requests~=2.32", 24 | "boto3~=1.35", 25 | "botocore~=1.35", 26 | ] 27 | 28 | setup( 29 | name=NAME, 30 | version=VERSION, 31 | description="Infisical API Client", 32 | author="Infisical", 33 | author_email="support@infisical.com", 34 | url="https://github.com/Infisical/python-sdk-official", 35 | keywords=["Infisical", "Infisical API", "Infisical SDK"], 36 | install_requires=REQUIRES, 37 | packages=find_packages(exclude=["test", "tests"]), 38 | include_package_data=True, 39 | long_description_content_type='text/markdown', 40 | long_description="""\ 41 | Infisical SDK client for Python. To view documentation, please visit https://github.com/Infisical/python-sdk-official 42 | """, # noqa: E501 43 | package_data={"infisicalapi_client": ["py.typed"]}, 44 | ) 45 | -------------------------------------------------------------------------------- /sink/.env.example: -------------------------------------------------------------------------------- 1 | SECRETS_PROJECT_ID= 2 | SECRETS_ENVIRONMENT_SLUG= 3 | MACHINE_IDENTITY_UNIVERSAL_AUTH_CLIENT_ID= 4 | MACHINE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET= 5 | SITE_URL= 6 | KMS_PROJECT_ID= -------------------------------------------------------------------------------- /sink/README.md: -------------------------------------------------------------------------------- 1 | **Developer note:** 2 | 3 | To run the sink files, you must first run `pip install -e .` from inside the project root, to install the SDK in development mode. -------------------------------------------------------------------------------- /sink/cache_deletion_test.py: -------------------------------------------------------------------------------- 1 | from infisical_sdk import InfisicalSDKClient 2 | import time 3 | import os 4 | import random 5 | import string 6 | 7 | 8 | def loadEnvVarsFromFileIntoEnv(): 9 | d = dict() 10 | with open("./.env", "r") as fp: 11 | for line in fp: 12 | line = line.strip() 13 | if line and not line.startswith("#"): 14 | line = line.split("=", 1) 15 | d[line[0]] = line[1] 16 | 17 | for key, value in d.items(): 18 | os.environ[key] = value 19 | 20 | loadEnvVarsFromFileIntoEnv() 21 | 22 | SECRETS_PROJECT_ID = os.getenv("SECRETS_PROJECT_ID") 23 | SECRETS_ENVIRONMENT_SLUG = os.getenv("SECRETS_ENVIRONMENT_SLUG") 24 | 25 | MACHINE_IDENTITY_UNIVERSAL_AUTH_CLIENT_ID = os.getenv("MACHINE_IDENTITY_UNIVERSAL_AUTH_CLIENT_ID") 26 | MACHINE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET = os.getenv("MACHINE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET") 27 | SITE_URL = os.getenv("SITE_URL") 28 | 29 | cache_enabled_client = InfisicalSDKClient(host=SITE_URL, cache_ttl=10) 30 | cache_enabled_client.auth.universal_auth.login(MACHINE_IDENTITY_UNIVERSAL_AUTH_CLIENT_ID, MACHINE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET) 31 | 32 | 33 | time_start_cache_disabled = time.time() 34 | 35 | def randomStringNoSpecialChars(length: int = 10) -> str: 36 | return ''.join(random.choices(string.ascii_letters + string.digits, k=length)) 37 | 38 | created_sec = cache_enabled_client.secrets.create_secret_by_name( 39 | secret_name=f"TEST_{randomStringNoSpecialChars()}", 40 | project_id=SECRETS_PROJECT_ID, 41 | environment_slug=SECRETS_ENVIRONMENT_SLUG, 42 | secret_path="/", 43 | secret_value=f"secret_value_{randomStringNoSpecialChars()}", 44 | ) 45 | 46 | 47 | single_secret_cached = cache_enabled_client.secrets.get_secret_by_name( 48 | secret_name=created_sec.secretKey, 49 | project_id=SECRETS_PROJECT_ID, 50 | environment_slug=SECRETS_ENVIRONMENT_SLUG, 51 | secret_path="/", 52 | expand_secret_references=True, 53 | include_imports=True) 54 | 55 | print(single_secret_cached) 56 | 57 | 58 | deleted_secret = cache_enabled_client.secrets.delete_secret_by_name( 59 | secret_name=created_sec.secretKey, 60 | project_id=SECRETS_PROJECT_ID, 61 | environment_slug=SECRETS_ENVIRONMENT_SLUG, 62 | secret_path="/", 63 | ) 64 | 65 | print(deleted_secret) 66 | 67 | # Should error 68 | try: 69 | single_secret_cached = cache_enabled_client.secrets.get_secret_by_name( 70 | secret_name=created_sec.secretKey, 71 | project_id=SECRETS_PROJECT_ID, 72 | environment_slug=SECRETS_ENVIRONMENT_SLUG, 73 | secret_path="/", 74 | expand_secret_references=True, 75 | include_imports=True) 76 | except Exception as e: 77 | print(e) 78 | print("Good, we errored as expected!") 79 | -------------------------------------------------------------------------------- /sink/cache_expire_test.py: -------------------------------------------------------------------------------- 1 | from infisical_sdk import InfisicalSDKClient 2 | 3 | import time 4 | import os 5 | 6 | def loadEnvVarsFromFileIntoEnv(): 7 | d = dict() 8 | with open("./.env", "r") as fp: 9 | for line in fp: 10 | line = line.strip() 11 | if line and not line.startswith("#"): 12 | line = line.split("=", 1) 13 | d[line[0]] = line[1] 14 | 15 | for key, value in d.items(): 16 | os.environ[key] = value 17 | 18 | loadEnvVarsFromFileIntoEnv() 19 | 20 | SECRETS_PROJECT_ID = os.getenv("SECRETS_PROJECT_ID") 21 | SECRETS_ENVIRONMENT_SLUG = os.getenv("SECRETS_ENVIRONMENT_SLUG") 22 | 23 | MACHINE_IDENTITY_UNIVERSAL_AUTH_CLIENT_ID = os.getenv("MACHINE_IDENTITY_UNIVERSAL_AUTH_CLIENT_ID") 24 | MACHINE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET = os.getenv("MACHINE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET") 25 | SITE_URL = os.getenv("SITE_URL") 26 | 27 | cache_enabled_client = InfisicalSDKClient(host=SITE_URL, cache_ttl=10) 28 | cache_enabled_client.auth.universal_auth.login(MACHINE_IDENTITY_UNIVERSAL_AUTH_CLIENT_ID, MACHINE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET) 29 | 30 | 31 | 32 | single_secret_cached = cache_enabled_client.secrets.get_secret_by_name( 33 | secret_name="TEST", 34 | project_id=SECRETS_PROJECT_ID, 35 | environment_slug=SECRETS_ENVIRONMENT_SLUG, 36 | secret_path="/", 37 | expand_secret_references=True, 38 | include_imports=True) 39 | 40 | 41 | time_start_cache_enabled = time.time() 42 | # Running in loop 10 times or the time is so small that python messes up the print (which is a great sign for us!) 43 | for i in range(10): 44 | single_secret_cached = cache_enabled_client.secrets.get_secret_by_name( 45 | secret_name="TEST", 46 | project_id=SECRETS_PROJECT_ID, 47 | environment_slug=SECRETS_ENVIRONMENT_SLUG, 48 | secret_path="/", 49 | expand_secret_references=True, 50 | include_imports=True) 51 | time_end_cache_enabled = time.time() 52 | print(f"[CACHE ENABLED] Time taken: {time_end_cache_enabled - time_start_cache_enabled} seconds") 53 | 54 | 55 | print("Sleeping for 10 seconds") 56 | time.sleep(10) 57 | 58 | 59 | print("Getting secret again") 60 | time_start_cache_enabled = time.time() 61 | single_secret_cached = cache_enabled_client.secrets.get_secret_by_name( 62 | secret_name="TEST", 63 | project_id=SECRETS_PROJECT_ID, 64 | environment_slug=SECRETS_ENVIRONMENT_SLUG, 65 | secret_path="/", 66 | expand_secret_references=True, 67 | include_imports=True) 68 | time_end_cache_enabled = time.time() 69 | print(f"[CACHE EXPIRED] Time taken: {time_end_cache_enabled - time_start_cache_enabled} seconds") 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /sink/cache_test.py: -------------------------------------------------------------------------------- 1 | from infisical_sdk import InfisicalSDKClient 2 | import time 3 | import os 4 | 5 | 6 | def loadEnvVarsFromFileIntoEnv(): 7 | d = dict() 8 | with open("./.env", "r") as fp: 9 | for line in fp: 10 | line = line.strip() 11 | if line and not line.startswith("#"): 12 | line = line.split("=", 1) 13 | d[line[0]] = line[1] 14 | 15 | for key, value in d.items(): 16 | os.environ[key] = value 17 | 18 | loadEnvVarsFromFileIntoEnv() 19 | 20 | SECRETS_PROJECT_ID = os.getenv("SECRETS_PROJECT_ID") 21 | SECRETS_ENVIRONMENT_SLUG = os.getenv("SECRETS_ENVIRONMENT_SLUG") 22 | 23 | MACHINE_IDENTITY_UNIVERSAL_AUTH_CLIENT_ID = os.getenv("MACHINE_IDENTITY_UNIVERSAL_AUTH_CLIENT_ID") 24 | MACHINE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET = os.getenv("MACHINE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET") 25 | SITE_URL = os.getenv("SITE_URL") 26 | 27 | cache_disabled_client = InfisicalSDKClient(host=SITE_URL, cache_ttl=None) 28 | cache_disabled_client.auth.universal_auth.login(MACHINE_IDENTITY_UNIVERSAL_AUTH_CLIENT_ID, MACHINE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET) 29 | 30 | cache_enabled_client = InfisicalSDKClient(host=SITE_URL, cache_ttl=10) 31 | cache_enabled_client.auth.universal_auth.login(MACHINE_IDENTITY_UNIVERSAL_AUTH_CLIENT_ID, MACHINE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET) 32 | 33 | 34 | time_start_cache_disabled = time.time() 35 | 36 | 37 | for i in range(100): 38 | all_secrets = cache_disabled_client.secrets.list_secrets( 39 | project_id=SECRETS_PROJECT_ID, 40 | environment_slug=SECRETS_ENVIRONMENT_SLUG, 41 | secret_path="/", 42 | expand_secret_references=True, 43 | include_imports=True 44 | ) 45 | 46 | time_end_cache_disabled = time.time() 47 | 48 | print(f"[CACHE DISABLED] Time taken: {time_end_cache_disabled - time_start_cache_disabled} seconds") 49 | 50 | 51 | time_start_cache_enabled = time.time() 52 | 53 | for i in range(100): 54 | all_secrets = cache_enabled_client.secrets.list_secrets( 55 | project_id=SECRETS_PROJECT_ID, 56 | environment_slug=SECRETS_ENVIRONMENT_SLUG, 57 | secret_path="/", 58 | expand_secret_references=True, 59 | include_imports=True 60 | ) 61 | 62 | time_end_cache_enabled = time.time() 63 | 64 | print(f"[CACHE ENABLED] Time taken: {time_end_cache_enabled - time_start_cache_enabled} seconds") 65 | 66 | 67 | 68 | single_secret_cached = cache_enabled_client.secrets.get_secret_by_name( 69 | secret_name="TEST", 70 | project_id=SECRETS_PROJECT_ID, 71 | environment_slug=SECRETS_ENVIRONMENT_SLUG, 72 | secret_path="/", 73 | expand_secret_references=True, 74 | include_imports=True) 75 | 76 | single_secret_cached = cache_enabled_client.secrets.get_secret_by_name( 77 | secret_name="TEST", 78 | project_id=SECRETS_PROJECT_ID, 79 | environment_slug=SECRETS_ENVIRONMENT_SLUG, 80 | secret_path="/", 81 | expand_secret_references=False, 82 | include_imports=False) 83 | 84 | 85 | single_secret_cached = cache_enabled_client.secrets.get_secret_by_name( 86 | secret_name="TEST", 87 | project_id=SECRETS_PROJECT_ID, 88 | environment_slug=SECRETS_ENVIRONMENT_SLUG, 89 | secret_path="/", 90 | expand_secret_references=True, 91 | include_imports=True) -------------------------------------------------------------------------------- /sink/example.py: -------------------------------------------------------------------------------- 1 | from infisical_sdk import InfisicalSDKClient, SymmetricEncryption 2 | 3 | import random 4 | import base64 5 | import os 6 | import string 7 | 8 | def loadEnvVarsFromFileIntoEnv(): 9 | d = dict() 10 | with open("./.env", "r") as fp: 11 | for line in fp: 12 | line = line.strip() 13 | if line and not line.startswith("#"): 14 | line = line.split("=", 1) 15 | d[line[0]] = line[1] 16 | 17 | for key, value in d.items(): 18 | os.environ[key] = value 19 | 20 | loadEnvVarsFromFileIntoEnv() 21 | 22 | sdkInstance = InfisicalSDKClient(host=os.getenv("SITE_URL")) 23 | 24 | 25 | SECRETS_PROJECT_ID = os.getenv("SECRETS_PROJECT_ID") 26 | KMS_PROJECT_ID = os.getenv("KMS_PROJECT_ID") 27 | SECRETS_ENVIRONMENT_SLUG = os.getenv("SECRETS_ENVIRONMENT_SLUG") 28 | 29 | MACHINE_IDENTITY_UNIVERSAL_AUTH_CLIENT_ID = os.getenv("MACHINE_IDENTITY_UNIVERSAL_AUTH_CLIENT_ID") 30 | MACHINE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET = os.getenv("MACHINE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET") 31 | SITE_URL = os.getenv("SITE_URL") 32 | 33 | 34 | sdkInstance.auth.universal_auth.login(MACHINE_IDENTITY_UNIVERSAL_AUTH_CLIENT_ID, MACHINE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET) 35 | 36 | 37 | def random_string(length: int = 10) -> str: 38 | # Use only lowercase letters, numbers, and hyphens 39 | allowed_chars = string.ascii_lowercase + string.digits + '-' 40 | return ''.join(random.choices(allowed_chars, k=length)) 41 | 42 | 43 | 44 | 45 | ################################################# SECRET TESTS ################################################# 46 | 47 | new_secret = sdkInstance.secrets.create_secret_by_name( 48 | secret_name=f"TEST_{random_string()}", 49 | project_id=SECRETS_PROJECT_ID, 50 | secret_path="/", 51 | environment_slug=SECRETS_ENVIRONMENT_SLUG, 52 | secret_value=f"secret_value_{random_string()}", 53 | secret_comment=f"Optional comment_{random_string()}", 54 | skip_multiline_encoding=False, 55 | secret_reminder_repeat_days=30, # Optional 56 | secret_reminder_note=f"Remember to update this secret_{random_string()}" # Optional 57 | ) 58 | 59 | print(f"Created secret: [key={new_secret.secretKey}] | [value={new_secret.secretValue}]") 60 | 61 | updated_secret = sdkInstance.secrets.update_secret_by_name( 62 | current_secret_name=new_secret.secretKey, 63 | project_id=SECRETS_PROJECT_ID, 64 | secret_path="/", 65 | environment_slug=SECRETS_ENVIRONMENT_SLUG, 66 | secret_value=f"new_secret_value_{random_string()}", 67 | secret_comment=f"Updated comment_{random_string()}", # Optional 68 | skip_multiline_encoding=False, 69 | secret_reminder_repeat_days=10, # Optional 70 | secret_reminder_note=f"Updated reminder note_{random_string()}", # Optional 71 | new_secret_name=f"NEW_NAME_{random_string()}" # Optional 72 | ) 73 | 74 | print(f"Updated secret: [key={updated_secret.secretKey}] | [value={updated_secret.secretValue}]") 75 | secret = sdkInstance.secrets.get_secret_by_name( 76 | secret_name=updated_secret.secretKey, 77 | project_id=SECRETS_PROJECT_ID, 78 | environment_slug=SECRETS_ENVIRONMENT_SLUG, 79 | secret_path="/", 80 | expand_secret_references=True, 81 | include_imports=True, 82 | version=None # Optional 83 | ) 84 | 85 | print(f"Retrieved secret: [key={secret.secretKey}] | [value={secret.secretValue}]") 86 | 87 | 88 | all_secrets = sdkInstance.secrets.list_secrets( 89 | project_id=SECRETS_PROJECT_ID, 90 | environment_slug=SECRETS_ENVIRONMENT_SLUG, 91 | secret_path="/", 92 | expand_secret_references=True, 93 | include_imports=True 94 | ) 95 | 96 | 97 | all_secrets.secrets = [secret for secret in all_secrets.secrets if secret.secretKey != "TEST"] 98 | if len(all_secrets.secrets) != 1: 99 | raise Exception("Expected 1 secret, got {}".format(len(all_secrets.secrets))) 100 | 101 | 102 | # Print all secret keys 103 | for idx, secret in enumerate(all_secrets.secrets): 104 | print(f"Listed secrets key {idx}: [{secret.secretKey}] | [value={secret.secretValue}]") 105 | 106 | deleted_secret = sdkInstance.secrets.delete_secret_by_name( 107 | secret_name=updated_secret.secretKey, 108 | project_id=SECRETS_PROJECT_ID, 109 | environment_slug=SECRETS_ENVIRONMENT_SLUG, 110 | secret_path="/" 111 | ) 112 | 113 | print(f"Deleted secret: [key={deleted_secret.secretKey}] | [value={deleted_secret.secretValue}]") 114 | 115 | ################################################# KMS TESTS ################################################# 116 | 117 | kms_key = sdkInstance.kms.create_key( 118 | name=f"test-key-{random_string()}", 119 | project_id=KMS_PROJECT_ID, 120 | encryption_algorithm=SymmetricEncryption.AES_GCM_256, 121 | description=f"Optional description_{random_string()}" 122 | ) 123 | 124 | print(f"Created KMS key: [key={kms_key.id}] | [name={kms_key.name}]") 125 | 126 | 127 | plantext = "Hello, world!" 128 | 129 | encrypted = sdkInstance.kms.encrypt_data( 130 | key_id=kms_key.id, 131 | base64EncodedPlaintext=base64.b64encode(plantext.encode()).decode() 132 | ) 133 | 134 | print(f"Encrypted: {encrypted}") 135 | 136 | decrypted = sdkInstance.kms.decrypt_data( 137 | key_id=kms_key.id, 138 | ciphertext=encrypted 139 | ) 140 | 141 | print(f"Decrypted: {base64.b64decode(decrypted.encode()).decode()}") 142 | 143 | key_by_id = sdkInstance.kms.get_key_by_id( 144 | key_id=kms_key.id 145 | ) 146 | 147 | print(f"Key by ID: {key_by_id}") 148 | 149 | key_by_name = sdkInstance.kms.get_key_by_name( 150 | key_name=kms_key.name, 151 | project_id=KMS_PROJECT_ID 152 | ) 153 | 154 | print(f"Key by Name: {key_by_name}") 155 | 156 | list_keys = sdkInstance.kms.list_keys( 157 | project_id=KMS_PROJECT_ID 158 | ) 159 | 160 | print(f"List keys: {list_keys}") 161 | 162 | 163 | deleted_key = sdkInstance.kms.delete_key( 164 | key_id=kms_key.id 165 | ) 166 | 167 | print(f"Deleted key: {deleted_key}") -------------------------------------------------------------------------------- /test-requirements.txt: -------------------------------------------------------------------------------- 1 | pytest~=7.1.3 2 | pytest-cov>=2.8.1 3 | pytest-randomly>=3.12.0 4 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py3 3 | 4 | [testenv] 5 | deps=-r{toxinidir}/requirements.txt 6 | -r{toxinidir}/test-requirements.txt 7 | 8 | commands= 9 | pytest --cov=infisicalapi_client 10 | --------------------------------------------------------------------------------