Skip to content

Commit

Permalink
Merge pull request #768 from globus/an/add-globus-session-error-support
Browse files Browse the repository at this point in the history
Add support for Globus Auth Requirements Error responses
  • Loading branch information
ada-globus authored Aug 7, 2023
2 parents 8bf6c44 + 70cd929 commit bfa4e97
Show file tree
Hide file tree
Showing 10 changed files with 1,200 additions and 0 deletions.
3 changes: 3 additions & 0 deletions changelog.d/20230629_093104_ada_add_session_error_support.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
* Add a new class ``GlobusAuthRequirementsError`` and utility functions to the
experimental subpackage to support handling of the Globus Auth Requirements Error
response format (:pr:`NUMBER`)
139 changes: 139 additions & 0 deletions docs/experimental/auth_requirements_errors.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
Auth Requirements Errors
========================

Globus Auth Requirements Error is a response format that conveys to a client any
modifications to a session (i.e., "boosting") that will be required
to complete a particular request.

The ``globus_sdk.experimental.auth_requirements_error`` module provides a
number of tools to make it easier to identify and handle these errors when they occur.

GlobusAuthRequirementsError
---------------------------

The ``GlobusAuthRequirementsError`` class provides a model for working with Globus
Auth Requirements Error responses.

Services in the Globus ecosystem may need to communicate authorization requirements
to their consumers. For example, a service may need to instruct clients to have the user
consent to an additional scope, ``"foo"``. In such a case, ``GlobusAuthRequirementsError``
can provide serialization into the well-known Globus Auth Requirements Error format:

.. code-block:: python
from globus_sdk.experimental.auth_requirements_error import GlobusAuthRequirementsError
error = GlobusAuthRequirementsError(
code="ConsentRequired",
authorization_parameters=GlobusAuthorizationParameters(
required_scopes=["foo"],
session_message="Missing required 'foo' consent",
),
)
# Render a strict dictionary
error.to_dict()
If non-canonical fields are needed, the ``extra`` argument can be used to
supply a dictionary of additional fields to include. Non-canonical fields present
in the provided dictionary when calling ``from_dict()`` are stored similarly.
You can include these fields in the rendered output dictionary
by specifying ``include_extra=True`` when calling ``to_dict()``.

.. code-block:: python
from globus_sdk.experimental.auth_requirements_error import GlobusAuthRequirementsError
error = GlobusAuthRequirementsError(
code="ConsentRequired",
authorization_parameters=GlobusAuthorizationParameters(
required_scopes=["foo"],
session_message="Missing required 'foo' consent",
),
extra={
"message": "Missing required 'foo' consent",
"request_id": "WmMV97A1w",
"required_scopes": ["foo"],
"resource": "/transfer",
},
)
# Render a dictionary with extra fields
error.to_dict(include_extra=True)
These fields are stored by both the ``GlobusAuthRequirementsError`` and
``GlobusAuthenticationParameters`` classes in an ``extra`` attribute.

.. note::

Non-canonical fields in a Globus Auth Requirements Error are primarily intended
to make it easier for services to provide backward-compatibile error responses
to clients that have not adopted the Globus Auth Requirements Error format. Avoid
using non-canonical fields for any data that should be generically understood by
a consumer of the error response.

Parsing Responses
-----------------

If you are writing a client to a Globus API, the ``auth_requirements_error`` subpackage
provides utilities to detect legacy Globus Auth requirements error response
formats and normalize them.

To detect if a ``GlobusAPIError``, ``ErrorSubdocument``, or JSON response
dictionary represents an error that can be converted to a Globus Auth
Requirements Error, you can use, e.g.,:

.. code-block:: python
from globus_sdk.experimental import auth_requirements_error
error_dict = {
"code": "ConsentRequired",
"message": "Missing required foo consent",
}
# The dict is not a Globus Auth Requirements Error, so `False` is returned.
auth_requirements_error.utils.is_auth_requirements_error(error_dict)
# The dict is not a Globus Auth Requirements Error and cannot be converted.
auth_requirements_error.utils.to_auth_requirements_error(error_dict) # None
error_dict = {
"code": "ConsentRequired",
"message": "Missing required foo consent",
"required_scopes": ["urn:globus:auth:scope:transfer.api.globus.org:all[*foo]"],
}
auth_requirements_error.utils.is_auth_requirements_error(error_dict) # True
auth_requirements_error.utils.to_auth_requirements_error(
error_dict
) # GlobusAuthRequirementsError
.. note::

If a ``GlobusAPIError`` represents multiple errors that were returned in an
array, ``to_auth_requirements_error()`` only returns the first error in that
array that can be converted to the Globus Auth Requirements Error response format.
In this case (and in general) it's preferable to use
``to_auth_requirements_errors()`` (which also accepts a list of
``GlobusAPIError``\ s, ``ErrorSubdocument``\ s, and JSON response dictionaries):

.. code-block:: python
auth_requirements_error.utils.to_auth_requirements_error(
other_error
) # GlobusAuthRequirementsError
auth_requirements_error.utils.to_auth_requirements_errors(
[other_error]
) # [GlobusAuthRequirementsError, ...]
Notes
-----

``GlobusAuthRequirementsError`` enforces types strictly when parsing a Globus
Auth Requirements Error response dictionary, and will raise a ``ValueError`` if a
supported field is supplied with a value of the wrong type.

``GlobusAuthRequirementsError`` does not attempt to mimic or itself enforce
any logic specific to the Globus Auth service with regard to what represents a valid
combination of fields (e.g., ``session_required_mfa`` requires either
``session_required_identities`` or ``session_required_single_domain``
in order to be properly handled).
1 change: 1 addition & 0 deletions docs/experimental/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,5 @@ Globus SDK Experimental Components
:caption: Contents
:maxdepth: 1

auth_requirements_errors
scope_parser
21 changes: 21 additions & 0 deletions src/globus_sdk/experimental/auth_requirements_error/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from ._auth_requirements_error import (
GlobusAuthorizationParameters,
GlobusAuthRequirementsError,
)
from ._functional_api import (
has_auth_requirements_errors,
is_auth_requirements_error,
to_auth_requirements_error,
to_auth_requirements_errors,
)
from ._validators import ValidationError

__all__ = [
"ValidationError",
"GlobusAuthRequirementsError",
"GlobusAuthorizationParameters",
"to_auth_requirements_error",
"to_auth_requirements_errors",
"is_auth_requirements_error",
"has_auth_requirements_errors",
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
from __future__ import annotations

import typing as t

from . import _serializable, _validators


class GlobusAuthorizationParameters(_serializable.Serializable):
"""
Represents authorization parameters that can be used to instruct a client
which additional authorizations are needed in order to complete a request.
:ivar session_message: A message to be displayed to the user.
:vartype session_message: str, optional
:ivar session_required_identities: A list of identities required for the
session.
:vartype session_required_identities: list of str, optional
:ivar session_required_policies: A list of policies required for the
session.
:vartype session_required_policies: list of str, optional
:ivar session_required_single_domain: A list of domains required for the
session.
:vartype session_required_single_domain: list of str, optional
:ivar session_required_mfa: Whether MFA is required for the session.
:vartype session_required_mfa: bool, optional
:ivar required_scopes: A list of scopes for which consent is required.
:vartype required_scopes: list of str, optional
:ivar extra: A dictionary of additional fields that were provided. May
be used for forward/backward compatibility.
:vartype extra: dict
"""

def __init__(
self,
*,
session_message: str | None = None,
session_required_identities: list[str] | None = None,
session_required_policies: list[str] | None = None,
session_required_single_domain: list[str] | None = None,
session_required_mfa: bool | None = None,
required_scopes: list[str] | None = None,
extra: dict[str, t.Any] | None = None,
):
self.session_message = _validators.opt_str("session_message", session_message)
self.session_required_identities = _validators.opt_str_list(
"session_required_identities", session_required_identities
)
self.session_required_policies = _validators.opt_str_list(
"session_required_policies", session_required_policies
)
self.session_required_single_domain = _validators.opt_str_list(
"session_required_single_domain", session_required_single_domain
)
self.session_required_mfa = _validators.opt_bool(
"session_required_mfa", session_required_mfa
)
self.required_scopes = _validators.opt_str_list(
"required_scopes", required_scopes
)
self.extra = extra or {}

# Enforce that the error contains at least one of the fields we expect
requires_at_least_one = [
name for name in self._supported_fields() if name != "session_message"
]
if all(
getattr(self, field_name) is None for field_name in requires_at_least_one
):
raise _validators.ValidationError(
"Must include at least one supported authorization parameter: "
+ ", ".join(requires_at_least_one)
)


class GlobusAuthRequirementsError(_serializable.Serializable):
"""
Represents a Globus Auth Requirements Error.
A Globus Auth Requirements Error is a class of error that is returned by Globus
services to indicate that additional authorization is required in order to complete
a request and contains information that can be used to request the appropriate
authorization.
:ivar code: The error code for this error.
:vartype code: str
:ivar authorization_parameters: The authorization parameters for this error.
:vartype authorization_parameters: GlobusAuthorizationParameters
:ivar extra: A dictionary of additional fields that were provided. May
be used for forward/backward compatibility.
:vartype extra: dict
"""

def __init__(
self,
code: str,
authorization_parameters: dict[str, t.Any] | GlobusAuthorizationParameters,
*,
extra: dict[str, t.Any] | None = None,
):
self.code = _validators.str_("code", code)
self.authorization_parameters = _validators.instance_or_dict(
"authorization_parameters",
authorization_parameters,
GlobusAuthorizationParameters,
)
self.extra = extra or {}
Loading

0 comments on commit bfa4e97

Please sign in to comment.