Skip to content

Commit

Permalink
Add AuthorizerLoginManager (#1287)
Browse files Browse the repository at this point in the history
* Add AuthorizerLoginManager (#1287)

Add a commonly requested class.  Documented expected use, and with unit tests.

[sc-24329]

* Add two additional tests

[sc-24329]

---------

Co-authored-by: Kevin Hunter Kesling <[email protected]>
  • Loading branch information
ryanchard and khk-globus authored Oct 27, 2023
1 parent 5895588 commit 8f5b590
Show file tree
Hide file tree
Showing 5 changed files with 147 additions and 39 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
New Functionality
^^^^^^^^^^^^^^^^^

- Added a new ``AuthorizerLoginManager`` to create a login_manager from
existing tokens. This removes the need to implement a custom login manager
to create a client from authorizers.
2 changes: 2 additions & 0 deletions compute_sdk/globus_compute_sdk/sdk/login_manager/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from .authorizer_login_manager import AuthorizerLoginManager
from .decorators import requires_login
from .manager import ComputeScopes, LoginManager
from .protocol import LoginManagerProtocol
Expand All @@ -7,4 +8,5 @@
"ComputeScopes",
"LoginManagerProtocol",
"requires_login",
"AuthorizerLoginManager",
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
from __future__ import annotations

import logging

import globus_sdk
from globus_compute_sdk.sdk.login_manager.manager import LoginManager
from globus_compute_sdk.sdk.login_manager.protocol import LoginManagerProtocol
from globus_compute_sdk.sdk.web_client import WebClient
from globus_sdk.scopes import AuthScopes

from .manager import ComputeScopeBuilder

log = logging.getLogger(__name__)

ComputeScopes = ComputeScopeBuilder()


class AuthorizerLoginManager(LoginManagerProtocol):
"""
Implements a LoginManager that can be instantiated with authorizers.
This manager can be used to create an Executor with authorizers created
from previously acquired tokens, rather than requiring a Native App login
flow or Client credentials.
"""

def __init__(self, authorizers: dict[str, globus_sdk.RefreshTokenAuthorizer]):
self.authorizers = authorizers

def get_auth_client(self) -> globus_sdk.AuthClient:
return globus_sdk.AuthClient(authorizer=self.authorizers[AuthScopes.openid])

def get_web_client(
self, *, base_url: str | None = None, app_name: str | None = None
) -> WebClient:
return WebClient(
base_url=base_url,
app_name=app_name,
authorizer=self.authorizers[ComputeScopes.resource_server],
)

def ensure_logged_in(self):
"""Ensure authorizers for each of the required scopes are present."""

for server in LoginManager.SCOPES:
if server not in self.authorizers:
log.error(f"Required authorizer for {server} is not present.")
raise LookupError(
f"{type(self).__name__} could not find authorizer for {server}"
)

def logout(self):
log.warning(f"Logout cannot be invoked from an {type(self).__name__}.")
67 changes: 67 additions & 0 deletions compute_sdk/tests/unit/test_authorizer_login_manager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
from itertools import chain, combinations

import pytest
from globus_compute_sdk.sdk.login_manager import AuthorizerLoginManager, LoginManager
from globus_sdk.scopes import AuthScopes

CID_KEY = "FUNCX_SDK_CLIENT_ID"
CSC_KEY = "FUNCX_SDK_CLIENT_SECRET"
_MOCK_BASE = "globus_compute_sdk.sdk.login_manager."


@pytest.fixture
def logman(mocker, tmp_path):
home = mocker.patch(f"{_MOCK_BASE}tokenstore._home")
home.return_value = tmp_path
return AuthorizerLoginManager({})


def test_auth_client_requires_authorizer_openid_scope(mocker):
alm = AuthorizerLoginManager({})
with pytest.raises(KeyError) as pyt_exc:
alm.get_auth_client()
assert AuthScopes.openid in str(pyt_exc)


def test_does_not_open_local_cred_storage(mocker, randomstring):
test_authorizer = randomstring()
mock_lm = mocker.patch(f"{_MOCK_BASE}authorizer_login_manager.LoginManager")
mock_lm.SCOPES = {test_authorizer}
with pytest.raises(LookupError):
AuthorizerLoginManager({}).ensure_logged_in()

alm = AuthorizerLoginManager({test_authorizer: "asdf"})
assert alm.ensure_logged_in() is None, "Test setup: verified SCOPES is checked"
assert not mock_lm.called, "Do not instantiate; do not create creds storage"


@pytest.mark.parametrize(
"missing_keys",
list(
chain(
combinations(LoginManager.SCOPES, 1),
combinations(LoginManager.SCOPES, 2),
[()],
)
),
)
def test_ensure_logged_in(mocker, logman, missing_keys):
_authorizers = dict(LoginManager.SCOPES)
for k in missing_keys:
_authorizers.pop(k, None)

logman.authorizers = _authorizers

if missing_keys:
with pytest.raises(LookupError) as err:
logman.ensure_logged_in()

assert f"could not find authorizer for {missing_keys[0]}" in err.value.args[0]


def test_warns_upon_logout_attempts(mocker):
mock_log = mocker.patch(f"{_MOCK_BASE}authorizer_login_manager.log")
alm = AuthorizerLoginManager({})
assert not mock_log.warning.called
alm.logout()
assert mock_log.warning.called
59 changes: 20 additions & 39 deletions docs/sdk.rst
Original file line number Diff line number Diff line change
Expand Up @@ -211,13 +211,13 @@ This also applies when starting a Globus Compute endpoint.

.. _login manager:

Using a Custom LoginManager
---------------------------
Using a Existing Tokens
-----------------------

To programmatically create a Client from tokens and remove the need to perform a Native App login flow you can use a custom *LoginManager*.
The LoginManager is responsible for serving tokens to the Client as needed. Typically, this would perform a Native App login flow, store tokens, and return them as needed.
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.

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

.. note::
To access the funcX API the scope that needs to be requested from
Expand All @@ -235,46 +235,27 @@ More details on the Globus Compute login manager prototcol are available `here.
import globus_sdk
from globus_sdk.scopes import AuthScopes
from globus_compute_sdk.sdk.login_manager import LoginManager
from globus_compute_sdk.sdk.web_client import WebClient
from globus_compute_sdk import Client
class LoginManager:
"""
Implements the globus_compute_sdk.sdk.login_manager.protocol.LoginManagerProtocol class.
"""
def __init__(self, authorizers: dict[str, globus_sdk.RefreshTokenAuthorizer]):
self.authorizers = authorizers
def get_auth_client(self) -> globus_sdk.AuthClient:
return globus_sdk.AuthClient(
authorizer=self.authorizers[AuthScopes.openid]
)
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
def get_web_client(self, *, base_url: str) -> WebClient:
return WebClient(
base_url=base_url,
authorizer=self.authorizers[Client.FUNCX_SCOPE],
)
ComputeScopes = ComputeScopeBuilder()
def ensure_logged_in(self):
return True
# 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'])
def logout(self):
log.warning("logout cannot be invoked from here!")
# Create authorizers from existing tokens
compute_auth = globus_sdk.AccessTokenAuthorizer(compute_token)
openid_auth = globus_sdk.AccessTokenAuthorizer(openid_token)
# Create a new login manager and use it to create a client
compute_login_manager = LoginManager(
authorizers={Client.FUNCX_SCOPE: compute_auth,
AuthScopes.openid: openid_auth}
# 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()
gc = Client(login_manager=compute_login_manager)
gce = Executor(endpoint_id=tutorial_endpoint, funcx_client=gc)
fx = Client(login_manager=compute_login_manager)
Specifying a Serialization Strategy
-----------------------------------
Expand Down

0 comments on commit 8f5b590

Please sign in to comment.