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

Add scope parsing to Scope object #642

Merged
merged 10 commits into from
Nov 15, 2022
21 changes: 21 additions & 0 deletions changelog.d/20221104_145601_sirosen_add_scope_parsing.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
* Enhance scope utilities with scope parsing, attached to
``globus_sdk.scopes.Scope`` (:pr:`NUMBER`)

* ``MutableScope`` has been renamed to ``Scope``. Both names remain available
for backwards compatibility, but the preferred name is now ``Scope``

* ``Scope.parse`` and ``Scope.deserialize`` can now be used to parse strings
into ``Scope``\s

* ``Scope(...).serialize()`` is added, and ``str(Scope(...))`` uses it

* ``Scope.add_dependency`` now supports ``Scope`` objects as inputs

* The ``optional`` argument to ``add_dependency`` is deprecated.
``Scope(...).add_dependency("*foo")`` can be used to add an optional
dependency as a string, or equivalently
``Scope(...).add_dependency(Scope("foo", optional=True))``

* ``ScopeBuilder.make_mutable`` now accepts a keyword argument ``optional``.
This allows, for example,
``TransferScopes.make_mutable("all", optional=True)``
145 changes: 121 additions & 24 deletions docs/scopes.rst
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
.. _scopes:

.. currentmodule:: globus_sdk.scopes

Scopes and ScopeBuilders
========================

Expand Down Expand Up @@ -76,16 +78,16 @@ To elaborate on the above example:
# data from the response
tokendata = token_response.by_resource_server[TransferScopes.resource_server]

MutableScope objects
--------------------
Scope objects
-------------

In order to support optional and dependent scopes, an additional type is
provided by ``globus_sdk.scopes``: the ``MutableScope`` class.
provided by ``globus_sdk.scopes``: the ``Scope`` class.

``MutableScope`` can be constructed directly, from full scope strings, or via a
``Scope`` can be constructed directly, from full scope strings, or via a
``ScopeBuilder``'s ``make_mutable`` method, given a scope's short name.

For example, one can create a ``MutableScope`` from the Groups "all" scope as
For example, one can create a ``Scope`` from the Groups "all" scope as
follows:

.. code-block:: python
Expand All @@ -94,31 +96,121 @@ follows:

scope = GroupsScopes.make_mutable("all")

MutableScopes provide the most value when handling scope dependencies. For
example, given a Globus Connect Server Mapped Collection, it may be desirable
to construct a "data_access" scope as an optional dependency for the Transfer
Scope. To do so, one first creates the mutable scope object, then adds the
dependency to it:
``Scope`` objects primarily provide three main pieces of functionality:
parsing from a string, dynamically building a scope tree, and serializing to a
string.

Scope Parsing
~~~~~~~~~~~~~

:meth:`Scope.parse` is the primary parsing method. Given a string, parsing may
produce a list of scopes. The reason for this is that a scope string being
requested may be a space-delimited set of scopes. For example, the following
parse is desirable:

.. code-block:: pycon

>>> Scope.parse("openid urn:globus:auth:scopes:transfer.api.globus.org:all")
[
Scope("openid"),
Scope("urn:globus:auth:scopes:transfer.api.globus.org:all"),
]

Additionally, scopes can be deserialized from strings with
:meth:`Scope.deserialize`. This is similar to ``parse``, but it must return
exactly one scope. For example,

.. code-block:: pycon

>>> Scope.deserialize("urn:globus:auth:scopes:transfer.api.globus.org:all")
Scope("urn:globus:auth:scopes:transfer.api.globus.org:all")

Parsing supports scopes with dependencies and optional scopes denoted by the
``*`` marker. Therefore, the following is also a valid parse:

.. code-block:: pycon

>>> transfer_scope = "urn:globus:auth:scopes:transfer.api.globus.org:all"
>>> collection_scope = (
... "https://auth.globus.org/scopes/c855676f-7840-4630-9b16-ef260aaf02c3/data_access"
... )
>>> Scope.deserialize(f"{transfer_scope}[*{collection_scope}]")
Scope(
"urn:globus:auth:scopes:transfer.api.globus.org:all",
dependencies=[
Scope(
"https://auth.globus.org/scopes/c855676f-7840-4630-9b16-ef260aaf02c3/data_access",
optional=True
)
]
)

Dynamic Scope Construction
~~~~~~~~~~~~~~~~~~~~~~~~~~

In the parsing example above, a scope string was constructed as a format string
which was then parsed into a complex dependent scope structure. This can be
done directly, without needing to encode the scope as a string beforehand.

For example, the same transfer scope dependent upon a collection scope may be
constructed by means of ``Scope`` methods and the ``make_mutable`` method of
scope builders:

.. code-block:: python

from globus_sdk.scopes import GCSCollectionScopeBuilder, TransferScopes

MAPPED_COLLECTION_ID = "...ID HERE..."

# create the scopes with make_mutable
transfer_scope = TransferScopes.make_mutable("all")
transfer_scope.add_dependency(
GCSCollectionScopeBuilder(MAPPED_COLLECTION_ID).data_access, optional=True
data_access_scope = GCSCollectionScopeBuilder(MAPPED_COLLECTION_ID).make_mutable(
"data_access", optional=True
)
# add data_access as a dependency
transfer_scope.add_dependency(data_access_scope)

``MutableScope``\s can be used in most of the same locations where scope
``Scope``\s can be used in most of the same locations where scope
strings can be used, but you can also call ``str()`` on them to get a
stringified representation.

ScopeBuilder Types and Constants
--------------------------------
Serializing Scopes
~~~~~~~~~~~~~~~~~~

Whenever scopes are being sent to Globus services, they need to be encoded as
strings. All scope objects support this by means of their defined ``serialize``
method. Note that ``__str__`` for a ``Scope`` is just an alias for
``serialize``. For example, the following is valid usage to demonstrate
``str()``, ``repr()``, and ``serialize()``:

.. code-block:: pycon

>>> from globus_sdk.scopes import Scope
>>> foo = Scope("foo")
>>> bar = Scope("bar")
>>> bar.add_dependency("baz")
>>> foo.add_dependency(bar)
>>> print(str(Scope("foo")))
foo[bar *baz]
>>> print(bar.serialize())
bar[baz]
>>> alpha = Scope("alpha")
>>> alpha.add_dependency("*beta")
>>> print(repr(alpha))
Scope("alpha", dependencies=[Scope("beta", optional=True)])

Scope Reference
~~~~~~~~~~~~~~~

.. autoclass:: Scope
:members:
:show-inheritance:

ScopeBuilders
-------------

.. module:: globus_sdk.scopes
ScopeBuilder Types
~~~~~~~~~~~~~~~~~~

.. autoclass:: ScopeBuilder
:members:
Expand All @@ -132,21 +224,26 @@ ScopeBuilder Types and Constants
:members:
:show-inheritance:

.. autoclass:: MutableScope
:members:
:show-inheritance:
ScopeBuilder Constants
~~~~~~~~~~~~~~~~~~~~~~

.. autodata:: globus_sdk.scopes.data.AuthScopes
:annotation:

.. autodata:: globus_sdk.scopes.data.FlowsScopes
:annotation:

.. autodata:: AuthScopes
.. autodata:: globus_sdk.scopes.data.GroupsScopes
:annotation:

.. autodata:: GroupsScopes
.. autodata:: globus_sdk.scopes.data.NexusScopes
:annotation:

.. autodata:: NexusScopes
.. autodata:: globus_sdk.scopes.data.SearchScopes
:annotation:

.. autodata:: SearchScopes
.. autodata:: globus_sdk.scopes.data.TimerScopes
:annotation:

.. autodata:: globus_sdk.scopes.TransferScopes
.. autodata:: globus_sdk.scopes.data.TransferScopes
:annotation:
12 changes: 12 additions & 0 deletions src/globus_sdk/_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,18 @@
import typing as t
import uuid

if t.TYPE_CHECKING:
from globus_sdk.scopes import Scope

# these types are aliases meant for internal use
IntLike = t.Union[int, str]
UUIDLike = t.Union[uuid.UUID, str]
DateLike = t.Union[str, datetime.datetime]

ScopeCollectionType = t.Union[
str,
"Scope",
t.Iterable[str],
t.Iterable["Scope"],
t.Iterable[t.Union[str, "Scope"]],
]
5 changes: 3 additions & 2 deletions src/globus_sdk/authorizers/client_credentials.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import logging
import typing as t

from globus_sdk.scopes import MutableScope, _ScopeCollectionType
from globus_sdk._types import ScopeCollectionType
from globus_sdk.scopes import MutableScope

if t.TYPE_CHECKING:
from globus_sdk.services.auth import ConfidentialAppAuthClient, OAuthTokenResponse
Expand Down Expand Up @@ -61,7 +62,7 @@ class ClientCredentialsAuthorizer(RenewingAuthorizer):
def __init__(
self,
confidential_client: "ConfidentialAppAuthClient",
scopes: _ScopeCollectionType,
scopes: ScopeCollectionType,
*,
access_token: t.Optional[str] = None,
expires_at: t.Optional[int] = None,
Expand Down
Loading