From d247194d0faade80547d2feb44940089f240ffd1 Mon Sep 17 00:00:00 2001 From: Andrew Lerman Date: Tue, 6 May 2025 14:35:49 -0400 Subject: [PATCH 01/11] feat: refactor credential classes and helper functions (#47) --- src/jamf_pro_sdk/clients/auth.py | 172 +++++++++++++++++++------------ 1 file changed, 106 insertions(+), 66 deletions(-) diff --git a/src/jamf_pro_sdk/clients/auth.py b/src/jamf_pro_sdk/clients/auth.py index d66e167..2dbd4d8 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 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,135 @@ 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. +def prompt_for_credentials(provider_type: Type[CredentialsProvider]) -> CredentialsProvider: + """Prompts the user for credentials based on the given provider type. - :param username: The Jamf Pro API username. - :type username: Optional[str] - """ - if username is None: - username = input("Jamf Pro Username: ") + Supports both user credentials (username/password) and API client credentials + (client_id/client_secret), prompting interactively as needed. + + :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: ") - super().__init__(username, 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}") -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. +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. - The ``SecretString`` is expected to be JSON string in this format: + The ``SecretString`` is expected to be JSON string in this format: - .. code-block:: json + .. code-block:: json - { - "username": "oscar", - "password": "*****" - } + // For UserCredentialsProvider: + { + "username": "oscar", + "password": "*****" + } - .. important:: + // For ApiClientCredentialsProvider: + { + "client_id": "abc123", + "client_secret": "xyz456" + } - This credentials provider requires the ``aws`` extra dependency. + .. important:: - :param secret_id: The ARN or name of the secret. - :type secret_id: str + This credentials provider requires the ``aws`` extra dependency. - :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 provider_type: The credentials provider class to instantiate using the loaded secret. + :type provider_type: Type[CredentialsProvider] - :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.") + :param secret_id: The ARN or name of the secret. + :type secret_id: str - secrets_client = boto3.client("secretsmanager") + :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 - kwargs = {"SecretId": secret_id} + :param version_stage: The staging label of the version of the secret to retrieve. + :type version_stage: str - if version_id: - kwargs["VersionId"] = version_id + :return: The ``CredentialsProvider`` object with necessary credentials. + :rtype: CredentialsProvider + """ + if not BOTO3_IS_INSTALLED: + raise ImportError("The 'aws' extra dependency is required.") - if version_stage: - kwargs["VersionStage"] = version_stage + 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) + secret_value = secrets_client.get_secret_value(**kwargs) + credentials = json.loads(secret_value["SecretString"]) + return provider_type(**credentials) - credentials = json.loads(secret_value["SecretString"]) - username = credentials["username"] - password = credentials["password"] - super().__init__(username, password) +def load_from_keychain( + provider_type: Type[CredentialsProvider], server: str +) -> CredentialsProvider: + """Load credentials from the macOS login keychain and return an instance of the + specified credentials provider. + The Jamf Pro API password or client credentials are stored in the keychain with + the ``service_name`` set to the Jamf Pro server name. -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. + Supports: + - ``UserCredentialsProvider``: retrieves password using provided username + - ``ApiClientCredentialsProvider``: retrieves Client ID and Client Secret using usernames of + "CLIENT_ID" and "CLIENT_SECRET" - .. important:: + .. important:: - This credentials provider requires the ``macOS`` extra dependency. + This credentials provider requires the ``macOS`` extra dependency. - :param server: The Jamf Pro server name. - :type server: str + :param provider_type: The credentials provider class to instantiate + :type provider_type: Type[CredentialsProvider] - :param username: The Jamf Pro API username. - :type username: str - """ - if not KEYRING_IS_INSTALLED: - raise ImportError("The 'macOS' extra dependency is required.") + :param server: The Jamf Pro server name. + :type server: str - username = username - password = keyring.get_password(service_name=server, username=username) + :return: An instantiated credentials provider using the keychain values. + :rtype: CredentialsProvider + """ + if not KEYRING_IS_INSTALLED: + raise ImportError("The 'macOS' extra dependency is required.") + if issubclass(provider_type, UserCredentialsProvider): + username = input("Jamf Pro 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}" ) - - super().__init__(username, password) + return provider_type(username, password) + elif issubclass(provider_type, ApiClientCredentialsProvider): + client_id = keyring.get_password(service_name=server, username="CLIENT_ID") + client_secret = keyring.get_password(service_name=server, username="CLIENT_SECRET") + if not client_id or not client_secret: + raise CredentialsError( + f"API Credentials were not found for server {server}. Please verify they are in the correct format." + ) + return provider_type(client_id, client_secret) + else: + raise TypeError(f"Unsupported credentials provider: {provider_type}") From 581730f0528fbf942b3ac3c4cb51ea13af9b73e5 Mon Sep 17 00:00:00 2001 From: Andrew Lerman Date: Tue, 6 May 2025 14:36:09 -0400 Subject: [PATCH 02/11] refactor: adjust top-level imports --- src/jamf_pro_sdk/__init__.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) 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", ] From 9b7f9e4055491648701728846636f57c25ab3a6b Mon Sep 17 00:00:00 2001 From: Andrew Lerman Date: Fri, 9 May 2025 15:51:39 -0400 Subject: [PATCH 03/11] docs: updated project docs with recent refactors to credential providers and usage. --- README.md | 4 +- docs/reference/credentials.rst | 39 ++++++++++----- docs/user/advanced.rst | 10 +++- docs/user/classic_api.rst | 4 +- docs/user/getting_started.rst | 89 +++++++++++++++++++++++++++++++--- docs/user/jcds2.rst | 8 +-- docs/user/pro_api.rst | 16 +++--- 7 files changed, 131 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index c4ab38a..d4bc1b7 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, UserCredentialsProvider client = JamfProClient( server="dummy.jamfcloud.com", - credentials=BasicAuthProvider("username", "password") + credentials=UserCredentialsProvider("username", "password") ) all_computers = client.pro_api.get_computer_inventory_v1() 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..f236e38 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, UserCredentialsProvider # The default concurrency setting is 10. client = JamfProClient( server="jamf.my.org", - credentials=BasicAuthProvider("oscar", "j@mf1234!") + credentials=UserCredentialsProvider("oscar", "j@mf1234!") ) # 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..eda58a0 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, UserCredentialsProvider >>> client = JamfProClient( ... server="jamf.my.org", - ... credentials=BasicAuthProvider("oscar", "j@mf1234!") + ... credentials=UserCredentialsProvider("oscar", "j@mf1234!") ... ) >>> diff --git a/docs/user/getting_started.rst b/docs/user/getting_started.rst index 5be1986..cd2cc0a 100644 --- a/docs/user/getting_started.rst +++ b/docs/user/getting_started.rst @@ -33,7 +33,9 @@ Create a Client Import the Jamf Pro client from the SDK: - >>> from jamf_pro_sdk import JamfProClient, BasicAuthProvider +.. code-block:: python + + >>> from jamf_pro_sdk import JamfProClient, UserCredentialsProvider Create a client object passing in your Jamf Pro server name and a username and password: @@ -41,14 +43,15 @@ Create a client object passing in your Jamf Pro server name and a username and p When passing your Jamf Pro server name, do not include the scheme (``https://``) as the SDK handles this automatically for you. - +.. code-block:: python + >>> client = JamfProClient( ... server="jamf.my.org", - ... credentials=BasicAuthProvider("oscar", "j@mf1234!") + ... credentials=UserCredentialsProvider("oscar", "j@mf1234!") ... ) >>> -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. +``UserCredentialsProvider`` class 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. To use an API Client for authentication (`Jamf Pro 10.49+ `_) use :class:`~jamf_pro_sdk.clients.auth.ApiClientCredentialsProvider`. @@ -56,12 +59,72 @@ There are a number of built-in :doc:`/reference/credentials` available. To learn .. 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. + **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. + +Prompting for Credentials +^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. code-block:: python + + >>> from jamf_pro_sdk import JamfProClient, UserCredentialsProvider, prompt_for_credentials + >>> client = JamfProClient( + ... server="jamf.my.org", + ... credentials=prompt_for_credentials( + ... provider_type=UserCredentialsProvider + ... ) + ... ) + Jamf Pro Username: oscar + Jamf Pro Password: + +Loading from AWS Secrets Manager +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. important:: + + The ``aws`` dependency is required for this function. + +.. 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:: + + This utility requires the ``keyring`` extra dependency. + + When using :class:`~jamf_pro_sdk.clients.auth.ApiClientCredentialsProvider`, the SDK expects the client ID and client secret to be stored using the format ``CLIENT_ID`` and ``CLIENT_SECRET`` respectively. For :class:`~jamf_pro_sdk.clients.auth.UserCredentialsProvider`, you will be prompted for a username. + +.. 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" + ... ) + ... ) 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. 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 +134,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 +162,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, UserCredentialsProvider, 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 +172,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 +181,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=UserCredentialsProvider("oscar", "j@mf1234!") ... session_config=config, ... ) >>> @@ -122,7 +191,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=UserCredentialsProvider("oscar", "j@mf1234!") ... session_config=config, ... ) >>> @@ -136,6 +205,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 +215,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..b7c0ac3 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, UserCredentialsProvider + >>> client = JamfProClient("dummy.jamfcloud.com", UserCredentialsProvider("demo", "tryitout")) >>> 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, UserCredentialsProvider + >>> client = JamfProClient("dummy.jamfcloud.com", UserCredentialsProvider("demo", "tryitout")) >>> 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..7a529e2 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, UserCredentialsProvider + >>> client = JamfProClient("dummy.jamfcloud.com", UserCredentialsProvider("demo", "tryitout")) >>> 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, UserCredentialsProvider >>> 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", UserCredentialsProvider("demo", "tryitout")) >>> 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, UserCredentialsProvider >>> from jamf_pro_sdk.clients.pro_api.pagination import FilterField, SortField >>> client = JamfProClient( ... server="dummy.jamfcloud.com", - ... credentials=BasicAuthProvider("demo", "tryitout") + ... credentials=UserCredentialsProvider("demo", "tryitout") ... ) >>> @@ -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, UserCredentialsProvider >>> from jamf_pro_sdk.models.pro.mdm import LogOutUserCommand - >>> client = JamfProClient("dummy.jamfcloud.com", BasicAuthProvider("demo", "tryitout")) + >>> client = JamfProClient("dummy.jamfcloud.com", UserCredentialsProvider("demo", "tryitout")) >>> response client.pro_api.send_mdm_command_preview( ... management_ids=["4eecc1fb-f52d-48c5-9560-c246b23601d3"], ... command=LogOutUserCommand() From 2140ffdc8c42043c6e6de19ff5bc7293c058fe77 Mon Sep 17 00:00:00 2001 From: Andrew Lerman Date: Sat, 10 May 2025 12:40:57 -0400 Subject: [PATCH 04/11] ci/cd: comment out experimental integration tests temporarily --- .github/workflows/main_pr_tests.yaml | 44 ++++++++++++++-------------- 1 file changed, 22 insertions(+), 22 deletions(-) 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 }} From e6168fa2abb5fdf10076aee8c5ec33bdabcf9f3d Mon Sep 17 00:00:00 2001 From: Andrew Lerman Date: Mon, 12 May 2025 18:45:53 -0400 Subject: [PATCH 05/11] docs: use ApiClientCredentialsProvider as default in usage docs, add admonitions of breaking changes --- README.md | 4 +-- docs/user/advanced.rst | 4 +-- docs/user/classic_api.rst | 4 +-- docs/user/getting_started.rst | 66 ++++++++++++++++++++++++----------- docs/user/jcds2.rst | 8 ++--- docs/user/pro_api.rst | 16 ++++----- 6 files changed, 63 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index d4bc1b7..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, UserCredentialsProvider +from jamf_pro_sdk import JamfProClient, ApiClientCredentialsProvider client = JamfProClient( server="dummy.jamfcloud.com", - credentials=UserCredentialsProvider("username", "password") + credentials=ApiClientCredentialsProvider("client_id", "client_secret") ) all_computers = client.pro_api.get_computer_inventory_v1() diff --git a/docs/user/advanced.rst b/docs/user/advanced.rst index f236e38..44a7ed9 100644 --- a/docs/user/advanced.rst +++ b/docs/user/advanced.rst @@ -65,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, UserCredentialsProvider + from jamf_pro_sdk import JamfProClient, ApiClientCredentialsProvider # The default concurrency setting is 10. client = JamfProClient( server="jamf.my.org", - credentials=UserCredentialsProvider("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 eda58a0..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, UserCredentialsProvider + >>> from jamf_pro_sdk import JamfProClient, ApiClientCredentialsProvider >>> client = JamfProClient( ... server="jamf.my.org", - ... credentials=UserCredentialsProvider("oscar", "j@mf1234!") + ... credentials=ApiClientCredentialsProvider("client_id", "client_secret") ... ) >>> diff --git a/docs/user/getting_started.rst b/docs/user/getting_started.rst index cd2cc0a..a83f6e4 100644 --- a/docs/user/getting_started.rst +++ b/docs/user/getting_started.rst @@ -31,32 +31,37 @@ 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: -.. code-block:: python - - >>> from jamf_pro_sdk import JamfProClient, UserCredentialsProvider - -Create a client object passing in your Jamf Pro server name and a username and password: +.. important:: -.. note:: + **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. - When passing your Jamf Pro server name, do not include the scheme (``https://``) as the SDK handles this automatically for you. + `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=UserCredentialsProvider("oscar", "j@mf1234!") + ... credentials=ApiClientCredentialsProvider("client_id", "client_secret") ... ) >>> -``UserCredentialsProvider`` class 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. +.. note:: + + When passing your Jamf Pro server name, do not include the scheme (``https://``) as the SDK handles this automatically for you. -To use an API Client for authentication (`Jamf Pro 10.49+ `_) use :class:`~jamf_pro_sdk.clients.auth.ApiClientCredentialsProvider`. +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. @@ -71,22 +76,38 @@ Prompting for Credentials .. code-block:: python - >>> from jamf_pro_sdk import JamfProClient, UserCredentialsProvider, prompt_for_credentials + >>> from jamf_pro_sdk import JamfProClient, ApiClientCredentialsProvider, prompt_for_credentials >>> client = JamfProClient( ... server="jamf.my.org", ... credentials=prompt_for_credentials( - ... provider_type=UserCredentialsProvider + ... provider_type=ApiClientCredentialsProvider ... ) ... ) - Jamf Pro Username: oscar - Jamf Pro Password: + API Client ID: 123456abcdef + API Client Secret: Loading from AWS Secrets Manager ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ .. important:: - The ``aws`` dependency is required for this function. + The ``aws`` dependency is required for this function and can be installed with ``% 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 @@ -104,7 +125,7 @@ Loading from Keychain .. important:: - This utility requires the ``keyring`` extra dependency. + This utility requires the ``keyring`` extra dependency, which can be installed via ``% python3 -m pip install 'jamf-pro-sdk[macOS]'``. When using :class:`~jamf_pro_sdk.clients.auth.ApiClientCredentialsProvider`, the SDK expects the client ID and client secret to be stored using the format ``CLIENT_ID`` and ``CLIENT_SECRET`` respectively. For :class:`~jamf_pro_sdk.clients.auth.UserCredentialsProvider`, you will be prompted for a username. @@ -119,7 +140,10 @@ Loading from Keychain ... ) ... ) -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. +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: @@ -164,7 +188,7 @@ The Jamf Pro client will create a default configuration if one is not provided. .. code-block:: python - >>> from jamf_pro_sdk import JamfProClient, UserCredentialsProvider, SessionConfig + >>> 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') @@ -181,7 +205,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=UserCredentialsProvider("oscar", "j@mf1234!") + ... credentials=ApiClientCredentialsProvider("client_id", "client_secret"), ... session_config=config, ... ) >>> @@ -191,7 +215,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=UserCredentialsProvider("oscar", "j@mf1234!") + ... credentials=ApiClientCredentialsProvider("client_id", "client_secret"), ... session_config=config, ... ) >>> diff --git a/docs/user/jcds2.rst b/docs/user/jcds2.rst index b7c0ac3..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, UserCredentialsProvider - >>> client = JamfProClient("dummy.jamfcloud.com", UserCredentialsProvider("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, UserCredentialsProvider - >>> client = JamfProClient("dummy.jamfcloud.com", UserCredentialsProvider("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 7a529e2..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, UserCredentialsProvider - >>> client = JamfProClient("dummy.jamfcloud.com", UserCredentialsProvider("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, UserCredentialsProvider + >>> 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", UserCredentialsProvider("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, UserCredentialsProvider + >>> 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=UserCredentialsProvider("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, UserCredentialsProvider + >>> from jamf_pro_sdk import JamfProClient, ApiClientCredentialsProvider >>> from jamf_pro_sdk.models.pro.mdm import LogOutUserCommand - >>> client = JamfProClient("dummy.jamfcloud.com", UserCredentialsProvider("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() From 6db92bf77c835582a0164b8996fe56d22dd220b4 Mon Sep 17 00:00:00 2001 From: Andrew Lerman Date: Thu, 15 May 2025 11:35:46 -0400 Subject: [PATCH 06/11] fix: normalize server URL to ensure it uses https scheme --- src/jamf_pro_sdk/clients/auth.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/jamf_pro_sdk/clients/auth.py b/src/jamf_pro_sdk/clients/auth.py index 2dbd4d8..d371006 100644 --- a/src/jamf_pro_sdk/clients/auth.py +++ b/src/jamf_pro_sdk/clients/auth.py @@ -340,6 +340,11 @@ def load_from_keychain( """ if not KEYRING_IS_INSTALLED: raise ImportError("The 'macOS' extra dependency is required.") + + if server.startswith("http://"): + server = "https://" + server[len("http://"):] + elif not server.startswith("https://"): + server = f"https://{server}" if issubclass(provider_type, UserCredentialsProvider): username = input("Jamf Pro Username: ") From da5aae0beb31b665161038e4514311c7383fa559 Mon Sep 17 00:00:00 2001 From: Andrew Lerman Date: Thu, 15 May 2025 11:37:02 -0400 Subject: [PATCH 07/11] docs: update admonition in load_from_keychain around server URL scheme omission --- docs/user/getting_started.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/user/getting_started.rst b/docs/user/getting_started.rst index a83f6e4..c15d060 100644 --- a/docs/user/getting_started.rst +++ b/docs/user/getting_started.rst @@ -48,6 +48,8 @@ Create a client object using an API Client ID and Client Secret - the **recommen ... ) >>> +.. _server_scheme: + .. note:: When passing your Jamf Pro server name, do not include the scheme (``https://``) as the SDK handles this automatically for you. @@ -128,6 +130,8 @@ Loading from Keychain This utility requires the ``keyring`` extra dependency, which can be installed via ``% python3 -m pip install 'jamf-pro-sdk[macOS]'``. When using :class:`~jamf_pro_sdk.clients.auth.ApiClientCredentialsProvider`, the SDK expects the client ID and client secret to be stored using the format ``CLIENT_ID`` and ``CLIENT_SECRET`` respectively. For :class:`~jamf_pro_sdk.clients.auth.UserCredentialsProvider`, you will be prompted for a username. + + Additionally, the :ref:`server scheme ` does not need to be passed to the ``server`` argument, as the SDK handles this for you. .. code-block:: python From 6dd374bff3e92c7d530120daabf559f4ad09c7d3 Mon Sep 17 00:00:00 2001 From: Andrew Lerman Date: Thu, 15 May 2025 11:37:28 -0400 Subject: [PATCH 08/11] chore: formatting --- src/jamf_pro_sdk/clients/auth.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/jamf_pro_sdk/clients/auth.py b/src/jamf_pro_sdk/clients/auth.py index d371006..e550c60 100644 --- a/src/jamf_pro_sdk/clients/auth.py +++ b/src/jamf_pro_sdk/clients/auth.py @@ -340,9 +340,9 @@ def load_from_keychain( """ if not KEYRING_IS_INSTALLED: raise ImportError("The 'macOS' extra dependency is required.") - + if server.startswith("http://"): - server = "https://" + server[len("http://"):] + server = "https://" + server[len("http://") :] elif not server.startswith("https://"): server = f"https://{server}" From 922585a157647d6ce6ceb18ff19a8661f33d165c Mon Sep 17 00:00:00 2001 From: Andrew Lerman Date: Thu, 15 May 2025 16:57:48 -0400 Subject: [PATCH 09/11] docs: leverage static path for screenshots --- docs/_static/api-keychain.png | Bin 0 -> 36590 bytes docs/_static/user-keychain.png | Bin 0 -> 35543 bytes docs/conf.py | 2 +- 3 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 docs/_static/api-keychain.png create mode 100644 docs/_static/user-keychain.png diff --git a/docs/_static/api-keychain.png b/docs/_static/api-keychain.png new file mode 100644 index 0000000000000000000000000000000000000000..c0a02606c1cac994072920e9e60688f6ff0933be GIT binary patch literal 36590 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%0A6X2;+0Rq3WRraQK(-ZwVL(pxF5Qxp-Uyx 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 literal 0 HcmV?d00001 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" From d9d3aa96bf07ca8465a3230f867ba79ed2d50915 Mon Sep 17 00:00:00 2001 From: Andrew Lerman Date: Thu, 15 May 2025 17:10:41 -0400 Subject: [PATCH 10/11] docs: add screenshots for clarity, update usage docs for credential provider utility functions --- docs/user/getting_started.rst | 64 ++++++++++++++++++++++++++++++++--- 1 file changed, 59 insertions(+), 5 deletions(-) diff --git a/docs/user/getting_started.rst b/docs/user/getting_started.rst index c15d060..28bc1f7 100644 --- a/docs/user/getting_started.rst +++ b/docs/user/getting_started.rst @@ -73,6 +73,11 @@ 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 ^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -93,7 +98,11 @@ Loading from AWS Secrets Manager .. important:: - The ``aws`` dependency is required for this function and can be installed with ``% python3 -m pip install 'jamf-pro-sdk[aws]'``. + 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: @@ -127,11 +136,38 @@ Loading from Keychain .. important:: - This utility requires the ``keyring`` extra dependency, which can be installed via ``% python3 -m pip install 'jamf-pro-sdk[macOS]'``. + 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 client ID and client secret to be stored using the format ``CLIENT_ID`` and ``CLIENT_SECRET`` respectively. For :class:`~jamf_pro_sdk.clients.auth.UserCredentialsProvider`, you will be prompted for a username. +When using :class:`~jamf_pro_sdk.clients.auth.ApiClientCredentialsProvider`, the SDK expects: - Additionally, the :ref:`server scheme ` does not need to be passed to the ``server`` argument, as the SDK handles this for you. +- 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 + +.. 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 @@ -140,10 +176,28 @@ Loading from Keychain ... server="jamf.my.org", ... credentials=load_from_keychain( ... provider_type=ApiClientCredentialsProvider, - ... server="jamf.my.org" + ... 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 ------------- From 57a3d40fc8c8afce6b5c751d56df21e3296cb834 Mon Sep 17 00:00:00 2001 From: Andrew Lerman Date: Thu, 15 May 2025 17:17:00 -0400 Subject: [PATCH 11/11] feat(auth): require keyword args for load_from_keychain identity resolution Refactored load_from_keychain to accept `client_id` or `username` as keyword arguments instead of prompting the user directly. Improves automation and unifies behavior across credential provider types. --- src/jamf_pro_sdk/clients/auth.py | 84 ++++++++++++++++++++++++-------- 1 file changed, 63 insertions(+), 21 deletions(-) diff --git a/src/jamf_pro_sdk/clients/auth.py b/src/jamf_pro_sdk/clients/auth.py index e550c60..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, Type +from typing import TYPE_CHECKING, Any, Dict, Optional, Type, overload try: import boto3 @@ -227,6 +227,18 @@ def _request_access_token(self) -> AccessToken: return AccessToken(type="user", **resp.json()) +@overload +def prompt_for_credentials( + provider_type: Type[UserCredentialsProvider], +) -> UserCredentialsProvider: ... + + +@overload +def prompt_for_credentials( + provider_type: Type[ApiClientCredentialsProvider], +) -> ApiClientCredentialsProvider: ... + + def prompt_for_credentials(provider_type: Type[CredentialsProvider]) -> CredentialsProvider: """Prompts the user for credentials based on the given provider type. @@ -311,23 +323,45 @@ def load_from_aws_secrets_manager( 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[CredentialsProvider], server: str + 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. Supports: - - ``UserCredentialsProvider``: retrieves password using provided username - - ``ApiClientCredentialsProvider``: retrieves Client ID and Client Secret using usernames of - "CLIENT_ID" and "CLIENT_SECRET" - - .. important:: - - This credentials provider requires the ``macOS`` extra dependency. + - ``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] @@ -335,6 +369,12 @@ def load_from_keychain( :param server: The Jamf Pro server name. :type server: str + :param client_id: The client ID used for ``ApiClientCredentialsProvider``. Required if ``provider_type`` is that provider. + :type client_id: Optional[str] + + :param username: The username used for ``UserCredentialsProvider``. Required if ``provider_type`` is that provider. + :type username: Optional[str] + :return: An instantiated credentials provider using the keychain values. :rtype: CredentialsProvider """ @@ -347,20 +387,22 @@ def load_from_keychain( server = f"https://{server}" if issubclass(provider_type, UserCredentialsProvider): - username = input("Jamf Pro 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 username is None: + raise ValueError( + "Username argument is required to create UserCredentialsProvider object." ) - return provider_type(username, password) + identity = username elif issubclass(provider_type, ApiClientCredentialsProvider): - client_id = keyring.get_password(service_name=server, username="CLIENT_ID") - client_secret = keyring.get_password(service_name=server, username="CLIENT_SECRET") - if not client_id or not client_secret: - raise CredentialsError( - f"API Credentials were not found for server {server}. Please verify they are in the correct format." + if client_id is None: + raise ValueError( + "API Client ID is required to instantiate ApiClientCredentialsProvider." ) - return provider_type(client_id, client_secret) + 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}") + + return provider_type(identity, password)