diff --git a/.github/workflows/main_pr_tests.yaml b/.github/workflows/main_pr_tests.yaml index 111fc5d..453bf3d 100644 --- a/.github/workflows/main_pr_tests.yaml +++ b/.github/workflows/main_pr_tests.yaml @@ -34,25 +34,25 @@ jobs: - name: Tests run: make test - exp-integration-tests: - continue-on-error: true - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v3 - - - name: Setup Python - uses: actions/setup-python@v4 - with: - python-version: '3.9' - cache: 'pip' - - - name: Install - run: make install - - - name: Run Integration Tests - run: make test-all - env: - JAMF_PRO_HOST: ${{ vars.JAMF_PRO_HOST }} - JAMF_PRO_CLIENT_ID: ${{ vars.JAMF_PRO_CLIENT_ID }} - JAMF_PRO_CLIENT_SECRET: ${{ vars.JAMF_PRO_CLIENT_SECRET }} + # exp-integration-tests: + # continue-on-error: true + # runs-on: ubuntu-latest + # steps: + # - name: Checkout + # uses: actions/checkout@v3 + + # - name: Setup Python + # uses: actions/setup-python@v4 + # with: + # python-version: '3.9' + # cache: 'pip' + + # - name: Install + # run: make install + + # - name: Run Integration Tests + # run: make test-all + # env: + # JAMF_PRO_HOST: ${{ vars.JAMF_PRO_HOST }} + # JAMF_PRO_CLIENT_ID: ${{ vars.JAMF_PRO_CLIENT_ID }} + # JAMF_PRO_CLIENT_SECRET: ${{ vars.JAMF_PRO_CLIENT_SECRET }} diff --git a/README.md b/README.md index c4ab38a..a02b33e 100644 --- a/README.md +++ b/README.md @@ -3,11 +3,11 @@ A client library for the Jamf Pro APIs and webhooks. ```python -from jamf_pro_sdk import JamfProClient, BasicAuthProvider +from jamf_pro_sdk import JamfProClient, ApiClientCredentialsProvider client = JamfProClient( server="dummy.jamfcloud.com", - credentials=BasicAuthProvider("username", "password") + credentials=ApiClientCredentialsProvider("client_id", "client_secret") ) all_computers = client.pro_api.get_computer_inventory_v1() diff --git a/docs/_static/api-keychain.png b/docs/_static/api-keychain.png new file mode 100644 index 0000000..c0a0260 Binary files /dev/null and b/docs/_static/api-keychain.png differ diff --git a/docs/_static/user-keychain.png b/docs/_static/user-keychain.png new file mode 100644 index 0000000..667287d Binary files /dev/null and b/docs/_static/user-keychain.png differ diff --git a/docs/conf.py b/docs/conf.py index d5af405..c018d42 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -56,7 +56,7 @@ pygments_style = "default" pygments_dark_style = "material" -# html_static_path = ["_static"] +html_static_path = ["_static"] html_theme = "furo" diff --git a/docs/reference/credentials.rst b/docs/reference/credentials.rst index 7f3f2b2..56b06d3 100644 --- a/docs/reference/credentials.rst +++ b/docs/reference/credentials.rst @@ -3,30 +3,43 @@ Credentials Providers ===================== -API Client Providers --------------------- +The Jamf Pro SDK has two primary types of credential providers: **API Client Credentials** and **User Credentials**. -These credentials providers use Jamf Pro API clients for API authentication. +API Client Credentials Provider +------------------------------- + +Use Jamf Pro `API clients `_ for API authentication. .. autoclass:: jamf_pro_sdk.clients.auth.ApiClientCredentialsProvider :members: -Basic Auth Providers --------------------- +User Credentials Provider +------------------------- -These credentials providers use a username and password for API authentication. +User credential providers use a username and password for API authentication. -.. autoclass:: jamf_pro_sdk.clients.auth.BasicAuthProvider +.. autoclass:: jamf_pro_sdk.clients.auth.UserCredentialsProvider :members: -.. autoclass:: jamf_pro_sdk.clients.auth.PromptForCredentials - :members: +Utilities for Credential Providers +---------------------------------- -.. autoclass:: jamf_pro_sdk.clients.auth.LoadFromKeychain - :members: +These functions return an instantiated credentials provider of the specified type. -.. autoclass:: jamf_pro_sdk.clients.auth.LoadFromAwsSecretsManager - :members: +Prompt for Credentials +^^^^^^^^^^^^^^^^^^^^^^ + +.. autofunction:: jamf_pro_sdk.clients.auth.prompt_for_credentials + +Load from AWS Secrets Manager +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. autofunction:: jamf_pro_sdk.clients.auth.load_from_aws_secrets_manager + +Load from Keychain +^^^^^^^^^^^^^^^^^^ + +.. autofunction:: jamf_pro_sdk.clients.auth.load_from_keychain Access Token ------------ diff --git a/docs/user/advanced.rst b/docs/user/advanced.rst index ea7e37d..44a7ed9 100644 --- a/docs/user/advanced.rst +++ b/docs/user/advanced.rst @@ -8,6 +8,8 @@ A :class:`~jamf_pro_sdk.clients.auth.CredentialsProvider` is an interface for th The following example does not accept a username or password and retrieves a token from a DynamoDB table in an AWS account (it is assumed an external process is managing this table entry). +.. code-block:: python + >>> import boto3 >>> from jamf_pro_sdk.clients.auth import CredentialsProvider >>> from jamf_pro_sdk.models.client import AccessToken @@ -35,6 +37,8 @@ The SDK's clients provide curated methods to a large number of Jamf Pro APIs. No Here is the built-in method for getting a computer from the Classic API: +.. code-block:: python + >>> computer = client.classic_api.get_computer_by_id(1) >>> type(computer) @@ -42,6 +46,8 @@ Here is the built-in method for getting a computer from the Classic API: The same operation can be performed by using the :meth:`~jamf_pro_sdk.clients.JamfProClient.classic_api_request` method directly: +.. code-block:: python + >>> response = client.classic_api_request(method='get', resource_path='computers/id/1') >>> type(response) @@ -59,12 +65,12 @@ Here is a code example using :meth:`~jamf_pro_sdk.clients.JamfProClient.concurre .. code-block:: python - from jamf_pro_sdk import JamfProClient, BasicAuthProvider + from jamf_pro_sdk import JamfProClient, ApiClientCredentialsProvider # The default concurrency setting is 10. client = JamfProClient( server="jamf.my.org", - credentials=BasicAuthProvider("oscar", "j@mf1234!") + credentials=ApiClientCredentialsProvider("client_id", "client_secret") ) # Get a list of all computers, and then their IDs. diff --git a/docs/user/classic_api.rst b/docs/user/classic_api.rst index 2f57faa..47e2a0d 100644 --- a/docs/user/classic_api.rst +++ b/docs/user/classic_api.rst @@ -114,10 +114,10 @@ Assume this client has been instantiated for the examples shown below. .. code-block:: python - >>> from jamf_pro_sdk import JamfProClient, BasicAuthProvider + >>> from jamf_pro_sdk import JamfProClient, ApiClientCredentialsProvider >>> client = JamfProClient( ... server="jamf.my.org", - ... credentials=BasicAuthProvider("oscar", "j@mf1234!") + ... credentials=ApiClientCredentialsProvider("client_id", "client_secret") ... ) >>> diff --git a/docs/user/getting_started.rst b/docs/user/getting_started.rst index 5be1986..28bc1f7 100644 --- a/docs/user/getting_started.rst +++ b/docs/user/getting_started.rst @@ -31,37 +31,182 @@ When running ``pip freeze`` the SDK will appear with a filepath to the source in Create a Client --------------- -Import the Jamf Pro client from the SDK: +Create a client object using an API Client ID and Client Secret - the **recommended** method for authentication: - >>> from jamf_pro_sdk import JamfProClient, BasicAuthProvider +.. important:: + + **Breaking Change**: As of version ``0.8a1``, the SDK no longer uses ``BasicAuthProvider`` objects. Use :class:`~jamf_pro_sdk.clients.auth.ApiClientCredentialsProvider` as the new default. + + `Basic authentication is now disabled by default `_ in Jamf Pro. To authenticate securely and ensure compatibility with future Jamf Pro versions, use an API Client for access tokens instead. + +.. code-block:: python + + >>> from jamf_pro_sdk import JamfProClient, ApiClientCredentialsProvider + >>> client = JamfProClient( + ... server="jamf.my.org", + ... credentials=ApiClientCredentialsProvider("client_id", "client_secret") + ... ) + >>> -Create a client object passing in your Jamf Pro server name and a username and password: +.. _server_scheme: .. note:: When passing your Jamf Pro server name, do not include the scheme (``https://``) as the SDK handles this automatically for you. +Choosing a Credential Provider +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +There are a number of built-in :doc:`/reference/credentials` available. To learn how to implement your own visit :ref:`user/advanced:Custom Credentials Providers`. + +**We recommend using API Clients** for most cases. Basic authentication via username and password is now considered a legacy method and is **disabled by default** in Jamf Pro versions ≥ 10.49. + +- Use :class:`~jamf_pro_sdk.clients.auth.ApiClientCredentialsProvider` for API Clients. +- Use :class:`~jamf_pro_sdk.clients.auth.UserCredentialsProvider` if enabled in your Jamf environment. + +.. important:: + + **Do not use plaintext secrets (passwords, clients secrets, etc.) in scripts or the console.** The use of the base ``UserCredentialsProvider`` class in this guide is for demonstration purposes. + +Credential Provider Utility Functions +------------------------------------- +The SDK contains three helper functions that will *return* an instantiated credential provider of the specified type. When leveraging these functions, ensure you have the required extra dependencies installed. + +When using ``load_from_keychain``, **you must provide the identity keyword argument** required by the specified provider: + +- ``username=`` for ``UserCredentialsProvider`` +- ``client_id=`` for ``ApiClientCredentialsProvider`` + +Prompting for Credentials +^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: python + + >>> from jamf_pro_sdk import JamfProClient, ApiClientCredentialsProvider, prompt_for_credentials >>> client = JamfProClient( ... server="jamf.my.org", - ... credentials=BasicAuthProvider("oscar", "j@mf1234!") + ... credentials=prompt_for_credentials( + ... provider_type=ApiClientCredentialsProvider + ... ) ... ) - >>> + API Client ID: 123456abcdef + API Client Secret: -The ``BasicAuthProvider`` is a credentials provider. These objects are interfaces for authenticating for access tokens to the Jamf Pro APIs. Basic auth credentials providers use a username and password for authentication when requesting a new token. +Loading from AWS Secrets Manager +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -To use an API Client for authentication (`Jamf Pro 10.49+ `_) use :class:`~jamf_pro_sdk.clients.auth.ApiClientCredentialsProvider`. +.. important:: -There are a number of built-in :doc:`/reference/credentials` available. To learn how to implement your own visit :ref:`user/advanced:Custom Credentials Providers`. + The ``aws`` dependency is required for this function and can be installed via: + + .. code-block:: console + + % python3 -m pip install 'jamf-pro-sdk[aws]' + +The ``SecretString`` is expected to be a JSON string in the following format: + +.. code-block:: json + + // For UserCredentialsProvider: + { + "username": "oscar", + "password": "******" + } + + // For ApiClientCredentialsProvider: + { + "client_id": "abc123", + "client_secret": "xyz456" + } + +.. code-block:: python + + >>> from jamf_pro_sdk import JamfProClient, ApiClientCredentialsProvider, load_from_aws_secrets_manager + >>> client = JamfProClient( + ... server="jamf.my.org", + ... credentials=load_from_aws_secrets_manager( + ... provider_type=ApiClientCredentialsProvider, + ... secret_id="arn:aws:secretsmanager:us-west-2:111122223333:secret:aes128-1a2b3c" + ... ) + ... ) + +Loading from Keychain +^^^^^^^^^^^^^^^^^^^^^ .. important:: - **Do not plaintext secrets (passwords, clients secrets, etc.) in scripts or the console.** The use of the base ``BasicAuthProvider`` class in this guide is for demonstration purposes. + This utility requires the ``keyring`` extra dependency, which can be installed via: + + .. code-block:: console + + % python3 -m pip install 'jamf-pro-sdk[macOS]' + +When using :class:`~jamf_pro_sdk.clients.auth.ApiClientCredentialsProvider`, the SDK expects: + +- The API **client ID** to be stored in the keychain under your Jamf Pro server name (as the *service_name*) with the client ID as the *username*, and its associated secret as the *password*. + +.. image:: ../_static/api-keychain.png + :alt: Example macOS Keychain entry for API credentials (client_id) + :align: center + :width: 400px + +When using :class:`~jamf_pro_sdk.clients.auth.UserCredentialsProvider`, the SDK expects: + +- A **username** to be passed, and the password to be retrieved from the keychain under the same server name and username. + +.. image:: ../_static/user-keychain.png + :alt: Example keychain entry for User credentials + :align: center + :width: 400px -On the first request made the client will retrieve and cache an access token. This token will be used for all requests up until it nears expiration. At that point the client will refresh the token. If the token has expired the client will basic auth for a new one. +.. note:: + + The ``server`` argument should not include the :ref:`scheme `. The SDK normalizes this internally. + +Use the appropriate keyword argument depending on the credential provider class: + +- Use ``client_id=`` when using :class:`~jamf_pro_sdk.clients.auth.ApiClientCredentialsProvider`. +- Use ``username=`` when using :class:`~jamf_pro_sdk.clients.auth.UserCredentialsProvider`. + +.. code-block:: python + + >>> from jamf_pro_sdk import JamfProClient, ApiClientCredentialsProvider, load_from_keychain + >>> client = JamfProClient( + ... server="jamf.my.org", + ... credentials=load_from_keychain( + ... provider_type=ApiClientCredentialsProvider, + ... server="jamf.my.org", + ... client_id="" # Required keyword + ... ) + ... ) + +.. code-block:: python + + >>> from jamf_pro_sdk import JamfProClient, UserCredentialsProvider, load_from_keychain + >>> client = JamfProClient( + ... server="jamf.my.org", + ... credentials=load_from_keychain( + ... provider_type=UserCredentialsProvider, + ... server="jamf.my.org", + ... username="" # Required keyword + ... ) + ... ) + +.. tip:: + + You can manage entries using the **Keychain Access** app on macOS. See: `Apple's Keychain User Guide `_. + + +Access Tokens +------------- + +On the first request made the client will retrieve and cache an access token. This token will be used for all requests up until it nears expiration. At that point the client will refresh the token. If the token has expired, the client will use the configured credentials provider to request a new one. You can retrieve the current token at any time: +.. code-block:: python + >>> access_token = client.get_access_token() >>> access_token AccessToken(type='user', token='eyJhbGciOiJIUzI1NiJ9...', expires=datetime.datetime(2023, 8, 21, 16, 57, 1, 113000, tzinfo=datetime.timezone.utc), scope=None) @@ -71,6 +216,8 @@ You can retrieve the current token at any time: Both the Classic and Pro APIs are exposed through two interfaces: +.. code-block:: python + >>> client.classic_api >>> client.pro_api @@ -97,7 +244,9 @@ Some aspects of the Jamf Pro client can be configured at instantiation. These in The Jamf Pro client will create a default configuration if one is not provided. - >>> from jamf_pro_sdk import JamfProClient, BasicAuthProvider, SessionConfig +.. code-block:: python + + >>> from jamf_pro_sdk import JamfProClient, ApiClientCredentialsProvider, SessionConfig >>> config = SessionConfig() >>> config SessionConfig(timeout=None, max_retries=0, max_concurrency=5, verify=True, cookie=None, ca_cert_bundle=None, scheme='https') @@ -105,6 +254,8 @@ The Jamf Pro client will create a default configuration if one is not provided. Here are two examples on how to use a ``SessionConfig`` with the client to disable TLS verification and set a 30 second timeout: +.. code-block:: python + >>> config = SessionConfig() >>> config.verify = False >>> config.timeout = 30 @@ -112,7 +263,7 @@ Here are two examples on how to use a ``SessionConfig`` with the client to disab SessionConfig(timeout=30, max_retries=0, max_concurrency=5, verify=False, cookie=None, ca_cert_bundle=None, scheme='https') >>> client = JamfProClient( ... server="jamf.my.org", - ... credentials=BasicAuthProvider("oscar", "j@mf1234!") + ... credentials=ApiClientCredentialsProvider("client_id", "client_secret"), ... session_config=config, ... ) >>> @@ -122,7 +273,7 @@ Here are two examples on how to use a ``SessionConfig`` with the client to disab SessionConfig(timeout=30, max_retries=0, max_concurrency=5, verify=False, cookie=None, ca_cert_bundle=None, scheme='https') >>> client = JamfProClient( ... server="jamf.my.org", - ... credentials=BasicAuthProvider("oscar", "j@mf1234!") + ... credentials=ApiClientCredentialsProvider("client_id", "client_secret"), ... session_config=config, ... ) >>> @@ -136,6 +287,8 @@ Logging You can quickly setup console logging using the provided :func:`~jamf_pro_sdk.helpers.logger_quick_setup` function. +.. code-block:: python + >>> import logging >>> from jamf_pro_sdk.helpers import logger_quick_setup >>> logger_quick_setup(level=logging.DEBUG) @@ -144,5 +297,7 @@ When set to ``DEBUG`` the stream handler and level will also be applied to ``url If you require different handlers or formatting you may configure the SDK's logger manually. +.. code-block:: python + >>> import logging >>> sdk_logger = logging.getLogger("jamf_pro_sdk") diff --git a/docs/user/jcds2.rst b/docs/user/jcds2.rst index 2c19223..e52c80b 100644 --- a/docs/user/jcds2.rst +++ b/docs/user/jcds2.rst @@ -58,8 +58,8 @@ The file upload operation performs multiple API requests in sequence. .. code-block:: python - >>> from jamf_pro_sdk import JamfProClient, BasicAuthProvider - >>> client = JamfProClient("dummy.jamfcloud.com", BasicAuthProvider("demo", "tryitout")) + >>> from jamf_pro_sdk import JamfProClient, ApiClientCredentialsProvider + >>> client = JamfProClient("dummy.jamfcloud.com", ApiClientCredentialsProvider("client_id", "client_secret")) >>> client.jcds2.upload_file(file_path="/path/to/my.pkg") >>> @@ -70,8 +70,8 @@ File downloads will retrieve the download URL to the requested JCDS file and the .. code-block:: python - >>> from jamf_pro_sdk import JamfProClient, BasicAuthProvider - >>> client = JamfProClient("dummy.jamfcloud.com", BasicAuthProvider("demo", "tryitout")) + >>> from jamf_pro_sdk import JamfProClient, ApiClientCredentialsProvider + >>> client = JamfProClient("dummy.jamfcloud.com", ApiClientCredentialsProvider("client_id", "client_secret")) >>> client.jcds2.download_file(file_name="/path/to/my.pkg", download_path="/path/to/downloads/") >>> diff --git a/docs/user/pro_api.rst b/docs/user/pro_api.rst index 4531caa..71686be 100644 --- a/docs/user/pro_api.rst +++ b/docs/user/pro_api.rst @@ -22,8 +22,8 @@ The curated methods will return all results by default. Each operation that supp .. code-block:: python - >>> from jamf_pro_sdk import JamfProClient, BasicAuthProvider - >>> client = JamfProClient("dummy.jamfcloud.com", BasicAuthProvider("demo", "tryitout")) + >>> from jamf_pro_sdk import JamfProClient, ApiClientCredentialsProvider + >>> client = JamfProClient("dummy.jamfcloud.com", ApiClientCredentialsProvider("client_id", "client_secret")) >>> response = client.pro_api.get_computer_inventory_v1() >>> response @@ -49,10 +49,10 @@ The paginator object itself will return the generator by default. This can be ov .. code-block:: python - >>> from jamf_pro_sdk import JamfProClient, BasicAuthProvider + >>> from jamf_pro_sdk import JamfProClient, ApiClientCredentialsProvider >>> from jamf_pro_sdk.clients.pro_api.pagination import Paginator >>> from jamf_pro_sdk.models.pro.computers import Computer - >>> client = JamfProClient("dummy.jamfcloud.com", BasicAuthProvider("demo", "tryitout")) + >>> client = JamfProClient("dummy.jamfcloud.com", ApiClientCredentialsProvider("client_id", "client_secret")) >>> paginator = Paginator(api_client=client.pro_api, resource_path="v1/computers-inventory", return_model=Computer) >>> paginator() @@ -138,12 +138,12 @@ Here is an example of a paginated request using the SDK with the sorting and fil .. code-block:: python - >>> from jamf_pro_sdk import JamfProClient, BasicAuthProvider + >>> from jamf_pro_sdk import JamfProClient, ApiClientCredentialsProvider >>> from jamf_pro_sdk.clients.pro_api.pagination import FilterField, SortField >>> client = JamfProClient( ... server="dummy.jamfcloud.com", - ... credentials=BasicAuthProvider("demo", "tryitout") + ... credentials=ApiClientCredentialsProvider("client_id", "client_secret") ... ) >>> @@ -162,9 +162,9 @@ The SDK provides MDM commands in the form of models that are passed to the :meth .. code-block:: python - >>> from jamf_pro_sdk import JamfProClient, BasicAuthProvider + >>> from jamf_pro_sdk import JamfProClient, ApiClientCredentialsProvider >>> from jamf_pro_sdk.models.pro.mdm import LogOutUserCommand - >>> client = JamfProClient("dummy.jamfcloud.com", BasicAuthProvider("demo", "tryitout")) + >>> client = JamfProClient("dummy.jamfcloud.com", ApiClientCredentialsProvider("client_id", "client_secret")) >>> response client.pro_api.send_mdm_command_preview( ... management_ids=["4eecc1fb-f52d-48c5-9560-c246b23601d3"], ... command=LogOutUserCommand() diff --git a/src/jamf_pro_sdk/__init__.py b/src/jamf_pro_sdk/__init__.py index 56fe92a..01ff4c6 100644 --- a/src/jamf_pro_sdk/__init__.py +++ b/src/jamf_pro_sdk/__init__.py @@ -1,10 +1,11 @@ from .__about__ import __title__, __version__ from .clients import JamfProClient from .clients.auth import ( - BasicAuthProvider, - LoadFromAwsSecretsManager, - LoadFromKeychain, - PromptForCredentials, + ApiClientCredentialsProvider, + UserCredentialsProvider, + load_from_aws_secrets_manager, + load_from_keychain, + prompt_for_credentials, ) from .helpers import logger_quick_setup from .models.client import SessionConfig @@ -13,10 +14,11 @@ "__title__", "__version__", "JamfProClient", - "BasicAuthProvider", - "LoadFromAwsSecretsManager", - "LoadFromKeychain", - "PromptForCredentials", + "ApiClientCredentialsProvider", + "UserCredentialsProvider", + "load_from_aws_secrets_manager", + "load_from_keychain", + "prompt_for_credentials", "logger_quick_setup", "SessionConfig", ] diff --git a/src/jamf_pro_sdk/clients/auth.py b/src/jamf_pro_sdk/clients/auth.py index d66e167..2f0079d 100644 --- a/src/jamf_pro_sdk/clients/auth.py +++ b/src/jamf_pro_sdk/clients/auth.py @@ -3,7 +3,7 @@ from datetime import datetime, timedelta, timezone from getpass import getpass from threading import Lock -from typing import TYPE_CHECKING, Any, Dict, Optional +from typing import TYPE_CHECKING, Any, Dict, Optional, Type, overload try: import boto3 @@ -60,7 +60,7 @@ def _request_access_token(self) -> AccessToken: """This internal method requests a new Jamf Pro access token. Custom credentials providers should override this method. Refer to the ``ApiClientProvider`` - and ``BasicAuthProvider`` classes for example implementations. + and ``UserCredentialsProvider`` classes for example implementations. This method must always return an :class:`~jamf_pro_sdk.models.client.AccessToken` object. @@ -104,7 +104,7 @@ def _refresh_access_token(self) -> None: seconds it will be returned. If the cached token's remaining time is greater than 5 seconds but less than 60 seconds the token will be refreshed using the ``keep-alive`` API. - For OAuth tokens, if the cached token's remaining tims is greater than or equal to 3 seconds + For OAuth tokens, if the cached token's remaining time is greater than or equal to 3 seconds it will be returned. If the above conditions are not met a new token will be requested. @@ -190,9 +190,9 @@ def _request_access_token(self) -> AccessToken: ) -class BasicAuthProvider(CredentialsProvider): +class UserCredentialsProvider(CredentialsProvider): def __init__(self, username: str, password: str): - """A basic auth credentials provider that uses a username and password for obtaining access + """Credentials provider that uses a username and password for obtaining access tokens. :param username: The Jamf Pro API username. @@ -227,95 +227,182 @@ def _request_access_token(self) -> AccessToken: return AccessToken(type="user", **resp.json()) -class PromptForCredentials(BasicAuthProvider): - def __init__(self, username: Optional[str] = None): - """A basic auth credentials provider for command-line uses cases. The user will be prompted - for their username (if not provided) and password. +@overload +def prompt_for_credentials( + provider_type: Type[UserCredentialsProvider], +) -> UserCredentialsProvider: ... - :param username: The Jamf Pro API username. - :type username: Optional[str] - """ - if username is None: - username = input("Jamf Pro Username: ") - password = getpass("Jamf Pro Password: ") - super().__init__(username, password) - - -class LoadFromAwsSecretsManager(BasicAuthProvider): - def __init__(self, secret_id: str, version_id: str = None, version_stage: str = None): - """A basic auth credentials provider for AWS Secrets Manager. - Requires an IAM role with the ``secretsmanager:GetSecretValue`` permission. May also require - ``kms:Decrypt`` if the secret is encrypted with a customer managed key. - The ``SecretString`` is expected to be JSON string in this format: +@overload +def prompt_for_credentials( + provider_type: Type[ApiClientCredentialsProvider], +) -> ApiClientCredentialsProvider: ... - .. code-block:: json - { - "username": "oscar", - "password": "*****" - } - - .. important:: - - This credentials provider requires the ``aws`` extra dependency. - - :param secret_id: The ARN or name of the secret. - :type secret_id: str - - :param version_id: The unique identifier of this version of the secret. If not - provided the latest version of the secret will be returned. - :type version_id: str - - :param version_stage: The staging label of the version of the secret to retrieve. - :type version_stage: str - """ - if not BOTO3_IS_INSTALLED: - raise ImportError("The 'aws' extra dependency is required.") +def prompt_for_credentials(provider_type: Type[CredentialsProvider]) -> CredentialsProvider: + """Prompts the user for credentials based on the given provider type. - secrets_client = boto3.client("secretsmanager") + Supports both user credentials (username/password) and API client credentials + (client_id/client_secret), prompting interactively as needed. - kwargs = {"SecretId": secret_id} - - if version_id: - kwargs["VersionId"] = version_id - - if version_stage: - kwargs["VersionStage"] = version_stage + :param provider_type: The credentials provider class to instantiate. + :type provider_type: Type[CredentialsProvider] + :return: The ``CredentialsProvider`` object. + :rtype: CredentialsProvider + """ + if issubclass(provider_type, UserCredentialsProvider): + username = input("Jamf Pro Username: ") + password = getpass("Jamf Pro Password: ") + return provider_type(username, password) + elif issubclass(provider_type, ApiClientCredentialsProvider): + client_id = input("API Client ID: ") + client_secret = getpass("API Client Secret: ") + return provider_type(client_id, client_secret) + else: + raise TypeError(f"Unsupported credentials provider: {provider_type}") + + +def load_from_aws_secrets_manager( + provider_type: Type[CredentialsProvider], + secret_id: str, + version_id: str = None, + version_stage: str = None, +) -> CredentialsProvider: + """A basic auth credentials provider for AWS Secrets Manager. + Requires an IAM role with the ``secretsmanager:GetSecretValue`` permission. May also require + ``kms:Decrypt`` if the secret is encrypted with a customer managed key. - secret_value = secrets_client.get_secret_value(**kwargs) + The ``SecretString`` is expected to be JSON string in this format: + + .. code-block:: json - credentials = json.loads(secret_value["SecretString"]) - username = credentials["username"] - password = credentials["password"] + // For UserCredentialsProvider: + { + "username": "oscar", + "password": "*****" + } + + // For ApiClientCredentialsProvider: + { + "client_id": "abc123", + "client_secret": "xyz456" + } + + .. important:: + + This credentials provider requires the ``aws`` extra dependency. + + :param provider_type: The credentials provider class to instantiate using the loaded secret. + :type provider_type: Type[CredentialsProvider] + + :param secret_id: The ARN or name of the secret. + :type secret_id: str + + :param version_id: The unique identifier of this version of the secret. If not + provided the latest version of the secret will be returned. + :type version_id: str + + :param version_stage: The staging label of the version of the secret to retrieve. + :type version_stage: str + + :return: The ``CredentialsProvider`` object with necessary credentials. + :rtype: CredentialsProvider + """ + if not BOTO3_IS_INSTALLED: + raise ImportError("The 'aws' extra dependency is required.") + + secrets_client = boto3.client("secretsmanager") + kwargs = {"SecretId": secret_id} + if version_id: + kwargs["VersionId"] = version_id + if version_stage: + kwargs["VersionStage"] = version_stage + + secret_value = secrets_client.get_secret_value(**kwargs) + credentials = json.loads(secret_value["SecretString"]) + return provider_type(**credentials) + + +@overload +def load_from_keychain( + provider_type: Type[UserCredentialsProvider], + server: str, + *, + username: str, + client_id: None = None, +) -> UserCredentialsProvider: ... + + +@overload +def load_from_keychain( + provider_type: Type[ApiClientCredentialsProvider], + server: str, + *, + client_id: str, + username: None = None, +) -> ApiClientCredentialsProvider: ... + + +def load_from_keychain( + provider_type: Type[CredentialsProvider], + server: str, + client_id: Optional[str] = None, + username: Optional[str] = None, +) -> CredentialsProvider: + """Load credentials from the macOS login keychain and return an instance of the + specified credentials provider. + + .. important:: + + This credentials provider requires the ``macOS`` extra dependency. + + The Jamf Pro API password or client credentials are stored in the keychain with + the ``service_name`` set to the Jamf Pro server name. - super().__init__(username, password) + Supports: + - ``UserCredentialsProvider``: Retrieves a password using the provided ``username``. + - ``ApiClientCredentialsProvider``: Retrieves the API client secret using the provided ``client_id``. + + :param provider_type: The credentials provider class to instantiate + :type provider_type: Type[CredentialsProvider] + :param server: The Jamf Pro server name. + :type server: str -class LoadFromKeychain(BasicAuthProvider): - def __init__(self, server: str, username: str): - """A credentials provider for the macOS login keychain. The API password is stored in a - keychain entry where the ``service_name`` is the server. + :param client_id: The client ID used for ``ApiClientCredentialsProvider``. Required if ``provider_type`` is that provider. + :type client_id: Optional[str] - .. important:: + :param username: The username used for ``UserCredentialsProvider``. Required if ``provider_type`` is that provider. + :type username: Optional[str] - This credentials provider requires the ``macOS`` extra dependency. + :return: An instantiated credentials provider using the keychain values. + :rtype: CredentialsProvider + """ + if not KEYRING_IS_INSTALLED: + raise ImportError("The 'macOS' extra dependency is required.") - :param server: The Jamf Pro server name. - :type server: str + if server.startswith("http://"): + server = "https://" + server[len("http://") :] + elif not server.startswith("https://"): + server = f"https://{server}" - :param username: The Jamf Pro API username. - :type username: str - """ - if not KEYRING_IS_INSTALLED: - raise ImportError("The 'macOS' extra dependency is required.") - - username = username - password = keyring.get_password(service_name=server, username=username) - - if password is None: - raise CredentialsError( - f"Password not found for server {server} and username {username}" + if issubclass(provider_type, UserCredentialsProvider): + if username is None: + raise ValueError( + "Username argument is required to create UserCredentialsProvider object." ) + identity = username + elif issubclass(provider_type, ApiClientCredentialsProvider): + if client_id is None: + raise ValueError( + "API Client ID is required to instantiate ApiClientCredentialsProvider." + ) + identity = client_id + else: + raise TypeError(f"Unsupported credentials provider: {provider_type}") + + password = keyring.get_password(service_name=server, username=identity) + if password is None: + raise CredentialsError(f"Password not found for server {server} and username {identity}") - super().__init__(username, password) + return provider_type(identity, password)