Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Deprecate LoginManager and AuthorizerLoginManager #1791

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
New Functionality
^^^^^^^^^^^^^^^^^

- Added an optional ``authorizer`` parameter to the ``Client`` initializer to support
using a ``GlobusAuthorizer`` for authentication. This parameter is mutually exclusive
with ``app``.

Deprecated
^^^^^^^^^^

- The ``LoginManager`` and ``AuthorizerLoginManager`` classes are now deprecated. Use
`GlobusApp <https://globus-compute.readthedocs.io/en/stable/sdk.html#globusapps>`_
objects from the Globus SDK instead.

- The ``Client.login_manager`` attribute is now deprecated.
46 changes: 40 additions & 6 deletions compute_sdk/globus_compute_sdk/sdk/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ def __init__(
data_serialization_strategy: SerializationStrategy | None = None,
login_manager: LoginManagerProtocol | None = None,
app: globus_sdk.GlobusApp | None = None,
authorizer: globus_sdk.authorizers.GlobusAuthorizer | None = None,
**kwargs,
):
"""
Expand All @@ -99,14 +100,19 @@ def __init__(
Strategy to use when serializing function arguments. If None,
globus_compute_sdk.serialize.DEFAULT_STRATEGY_DATA will be used.

login_manager: LoginManagerProtocol
login_manager: LoginManagerProtocol [Deprecated]
Allows login logic to be overridden for specific use cases. If None,
a ``GlobusApp`` will be used. Mutually exclusive with ``app``.
a ``GlobusApp`` will be used. Mutually exclusive with ``app`` and
``authorizer``.

app: GlobusApp
A ``GlobusApp`` that will handle authorization and storing and validating
tokens. If None, a standard ``GlobusApp`` will be used. Mutually exclusive
with ``login_manager``.
with ``authorizer`` and ``login_manager``.

authorizer: GlobusAuthorizer
A ``GlobusAuthorizer`` that will generate authorization headers. Mutually
exclusive with ``app`` and ``login_manager``.
"""
for arg_name in kwargs:
msg = (
Expand All @@ -122,12 +128,20 @@ def __init__(
self._task_status_table: dict[str, dict] = {}

self.app: globus_sdk.GlobusApp | None = None
self.login_manager: LoginManagerProtocol | None = None
self.authorizer: globus_sdk.authorizers.GlobusAuthorizer | None = None
self._login_manager: LoginManagerProtocol | None = None
self._web_client: WebClient | None = None
self._auth_client: globus_sdk.AuthClient | None = None

if app and login_manager:
raise ValueError("'app' and 'login_manager' are mutually exclusive.")
if sum(bool(x) for x in (app, authorizer, login_manager)) > 1:
raise ValueError(
"'app', 'authorizer' and 'login_manager' are mutually exclusive."
)
elif authorizer:
self.authorizer = authorizer
self._compute_web_client = _ComputeWebClient(
base_url=self.web_service_address, authorizer=self.authorizer
)
elif login_manager:
self.login_manager = login_manager
self._compute_web_client = _ComputeWebClient(
Expand All @@ -149,6 +163,19 @@ def __init__(
if do_version_check:
self.version_check()

@property
def login_manager(self):
warnings.warn(
"The 'Client.login_manager' attribute is deprecated.",
DeprecationWarning,
stacklevel=2,
)
return self._login_manager

@login_manager.setter
def login_manager(self, val: LoginManagerProtocol):
self._login_manager = val

@property
def auth_client(self):
warnings.warn(
Expand Down Expand Up @@ -210,6 +237,13 @@ def version_check(self, endpoint_version: str | None = None) -> None:

def logout(self):
"""Remove credentials from your local system"""
if self.authorizer:
logger.warning(
"Logout not supported when client initialized with"
f" authorizer: {type(self.authorizer).__name__}"
)
return

auth_obj = self.app or self.login_manager
if auth_obj:
auth_obj.logout()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

import logging
import warnings

import globus_sdk
from globus_compute_sdk.sdk.login_manager.manager import LoginManager
Expand All @@ -22,6 +23,13 @@ class AuthorizerLoginManager(LoginManagerProtocol):
"""

def __init__(self, authorizers: dict[str, globus_sdk.RefreshTokenAuthorizer]):
warnings.warn(
"The `AuthorizerLoginManager` is deprecated. Please use `GlobusApp` objects"
"from the Globus SDK instead:"
" https://globus-compute.readthedocs.io/en/stable/sdk.html#globusapps",
category=DeprecationWarning,
stacklevel=2,
)
self.authorizers = authorizers

def get_auth_client(self) -> globus_sdk.AuthClient:
Expand Down
8 changes: 8 additions & 0 deletions compute_sdk/globus_compute_sdk/sdk/login_manager/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import sys
import threading
import typing as t
import warnings

import globus_sdk
from globus_sdk.scopes import AuthScopes
Expand Down Expand Up @@ -40,6 +41,13 @@ class LoginManager:
}

def __init__(self, *, environment: str | None = None) -> None:
warnings.warn(
"The `LoginManager` is deprecated. Please use `GlobusApp` objects"
"from the Globus SDK instead:"
" https://globus-compute.readthedocs.io/en/stable/sdk.html#globusapps",
category=DeprecationWarning,
stacklevel=2,
)
self._token_storage = get_token_storage_adapter(environment=environment)
self._access_lock = threading.Lock()

Expand Down
7 changes: 7 additions & 0 deletions compute_sdk/tests/unit/test_authorizer_login_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,13 @@ def logman(mocker, tmp_path):
return AuthorizerLoginManager({})


def test_authorizer_login_manager_deprecated():
with pytest.warns(DeprecationWarning) as record:
AuthorizerLoginManager({})
msg = "The `AuthorizerLoginManager` is deprecated"
assert any(msg in str(r.message) for r in record)


def test_auth_client_requires_authorizer_openid_scope(mocker):
alm = AuthorizerLoginManager({})
with pytest.raises(KeyError) as pyt_exc:
Expand Down
57 changes: 52 additions & 5 deletions compute_sdk/tests/unit/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from globus_compute_sdk.serialize.concretes import SELECTABLE_STRATEGIES
from globus_sdk import ComputeClientV2, ComputeClientV3, UserApp
from globus_sdk import __version__ as __version_globus__
from globus_sdk.authorizers import GlobusAuthorizer
from pytest_mock import MockerFixture

_MOCK_BASE = "globus_compute_sdk.sdk.client."
Expand Down Expand Up @@ -692,12 +693,27 @@ def test_version_mismatch_only_warns_once_per_ep(mocker, gcc, mock_response, ep_
assert mock_warn.warn.call_count == len(set(ep_ids))


def test_client_globus_app_and_login_manager_mutually_exclusive():
app = mock.Mock(spec=UserApp)
login_manager = mock.Mock(spec=LoginManager)
@pytest.mark.parametrize(
"has_app, has_authz, has_lm",
[(True, True, False), (True, False, True), (False, True, True)],
)
def test_client_mutually_exclusive_auth_method(
has_app: bool, has_authz: bool, has_lm: bool
):
app = mock.Mock(spec=UserApp) if has_app else None
login_manager = mock.Mock(spec=LoginManager) if has_lm else None
authorizer = mock.Mock(spec=GlobusAuthorizer) if has_authz else None

with pytest.raises(ValueError) as excinfo:
gc.Client(do_version_check=False, login_manager=login_manager, app=app)
assert "'app' and 'login_manager' are mutually exclusive" in str(excinfo.value)
gc.Client(
do_version_check=False,
login_manager=login_manager,
app=app,
authorizer=authorizer,
)
assert "'app', 'authorizer' and 'login_manager' are mutually exclusive" in str(
excinfo.value
)


@pytest.mark.parametrize("custom_app", [True, False])
Expand Down Expand Up @@ -744,6 +760,19 @@ def test_client_handles_globus_app(
mock_auth_client.assert_called_once_with(app=mock_app)


def test_client_handles_authorizer(mocker: MockerFixture):
mock_compute_wc = mocker.patch(f"{_MOCK_BASE}_ComputeWebClient")
mock_authorizer = mock.Mock(spec=GlobusAuthorizer)

client = gc.Client(do_version_check=False, authorizer=mock_authorizer)

assert client.authorizer is mock_authorizer
assert client._compute_web_client is mock_compute_wc.return_value
mock_compute_wc.assert_called_once_with(
base_url=client.web_service_address, authorizer=mock_authorizer
)


def test_client_handles_login_manager():
mock_lm = mock.Mock(spec=LoginManager)
client = gc.Client(do_version_check=False, login_manager=mock_lm)
Expand All @@ -769,6 +798,16 @@ def test_client_logout_with_login_manager():
assert mock_lm.logout.called


def test_client_logout_with_authorizer_warning(mocker):
mock_log = mocker.patch(f"{_MOCK_BASE}logger")
mock_authorizer = mock.Mock(spec=GlobusAuthorizer)
client = gc.Client(do_version_check=False, authorizer=mock_authorizer)
client.logout()
a, _ = mock_log.warning.call_args
assert "Logout not supported" in a[0]
assert type(mock_authorizer).__name__ in a[0]


def test_web_client_deprecated():
gcc = gc.Client(do_version_check=False)
with pytest.warns(DeprecationWarning) as record:
Expand All @@ -783,3 +822,11 @@ def test_auth_client_deprecated():
assert gcc.auth_client, "Client.auth_client needed for backward compatibility"
msg = "'Client.auth_client' attribute is deprecated"
assert any(msg in str(r.message) for r in record)


def test_login_manager_deprecated():
gcc = gc.Client(do_version_check=False)
with pytest.warns(DeprecationWarning) as record:
gcc.login_manager, "Client.login_manager needed for backward compatibility"
msg = "'Client.login_manager' attribute is deprecated"
assert any(msg in str(r.message) for r in record)
7 changes: 7 additions & 0 deletions compute_sdk/tests/unit/test_login_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,13 @@ def logman(mocker, tmp_path):
return LoginManager()


def test_login_manager_deprecated():
with pytest.warns(DeprecationWarning) as record:
LoginManager()
msg = "The `LoginManager` is deprecated"
assert any(msg in str(r.message) for r in record)


def test_is_client_login():
env = {CID_KEY: "some_id", CSC_KEY: "some_secret"}
with mock.patch.dict(os.environ, env):
Expand Down
58 changes: 16 additions & 42 deletions docs/sdk.rst
Original file line number Diff line number Diff line change
Expand Up @@ -264,59 +264,31 @@ This also applies when starting a Globus Compute endpoint.
.. note:: Globus Compute clients and endpoints will use the client credentials if they are set, so it is important to ensure the client submitting requests has access to an endpoint.


.. _login manager:
.. _existing-token:

Using a Existing Tokens
Using an Existing Token
-----------------------

To programmatically create a Client from tokens and remove the need to perform a Native App login flow you can use the *AuthorizerLoginManager*.
The AuthorizerLoginManager is responsible for serving tokens to the Client as needed and can be instantiated using existing tokens.
To create a |Client|_ from an existing access token and skip the interactive login flow, you can pass an |AccessTokenAuthorizer|_
via the ``authorizer`` parameter:

The AuthorizerLoginManager can be used to simply return static tokens and enable programmatic use of the Client.

.. note::
Accessing the Globus Compute API requires the Globus Auth scope:

.. code-block:: text

https://auth.globus.org/scopes/facd7ccc-c5f4-42aa-916b-a0e270e2c2a9/all

This is also programmatically available as the ``FUNCX_SCOPE`` attribute on
the ``Client`` class:

.. code-block:: python

>>> from globus_compute_sdk import Client
>>> Client.FUNCX_SCOPE
'https://auth.globus.org/scopes/facd7ccc-c5f4-42aa-916b-a0e270e2c2a9/all'

More details on the Globus Compute login manager prototcol are available `here. <https://github.com/globus/globus-compute/blob/main/compute_sdk/globus_compute_sdk/sdk/login_manager/protocol.py>`_


.. code:: python
.. code-block:: python

import globus_sdk
from globus_sdk.scopes import AuthScopes

from globus_compute_sdk import Executor, Client
from globus_compute_sdk.sdk.login_manager import AuthorizerLoginManager
from globus_compute_sdk.sdk.login_manager.manager import ComputeScopeBuilder

ComputeScopes = ComputeScopeBuilder()
authorizer = globus_sdk.AccessTokenAuthorizer(access_token="...")
gcc = Client(authorizer=authorizer)
gce = Executor(endpoint_id="...", client=gcc)

# Create Authorizers from the Compute and Auth tokens
compute_auth = globus_sdk.AccessTokenAuthorizer(tokens[ComputeScopes.resource_server]['access_token'])
openid_auth = globus_sdk.AccessTokenAuthorizer(tokens[AuthScopes.openid]['access_token'])
.. note::
Accessing the Globus Compute API requires the Globus Compute scope:

# Create a Compute Client from these authorizers
compute_login_manager = AuthorizerLoginManager(
authorizers={ComputeScopes.resource_server: compute_auth,
AuthScopes.resource_server: openid_auth}
)
compute_login_manager.ensure_logged_in()
.. code-block:: python

gc = Client(login_manager=compute_login_manager)
gce = Executor(endpoint_id=tutorial_endpoint, client=gc)
>>> from globus_sdk.scopes import ComputeScopes
>>> ComputeScopes.all
'https://auth.globus.org/scopes/facd7ccc-c5f4-42aa-916b-a0e270e2c2a9/all'


.. _specifying-serde-strategy:
Expand Down Expand Up @@ -409,6 +381,8 @@ and serializes a fresh copy each time it is invoked.

.. |rarr| unicode:: 0x2192

.. |AccessTokenAuthorizer| replace:: ``AccessTokenAuthorizer``
.. _AccessTokenAuthorizer: https://globus-sdk-python.readthedocs.io/en/stable/authorization/globus_authorizers.html#globus_sdk.AccessTokenAuthorizer
.. |Client| replace:: ``Client``
.. _Client: reference/client.html
.. |Executor| replace:: ``Executor``
Expand Down
Loading