Skip to content

Signers#

A signer mediates between a STAC consumer and the three auth boundaries a cloud-hosted STAC archive can have: the search request, the returned Item (asset-href rewrite), and the asset read (GDAL env). Every signer satisfies the Signer protocol — name plus sign_request, sign_item, sign_href, and gdal_env — so a custom signer need not subclass anything.

Pick by where the credential lives:

Signer Credential boundary Needs
AnonymousSigner none (public catalogs)
AWSRequesterPaysSigner asset read (AWS_REQUEST_PAYER=requester)
BearerTokenSigner request + asset read (Authorization: Bearer) a token (or a callable)

Wire a signer into open_client(..., signer=...) (request + item), into DatasetCollection.from_stac(..., signer=...) / build_vrt_from_stac(..., signer=...) (href + read), or into load_asset(item, key, signer=...) (href + read).

Bearer tokens and cross-host redirects

BearerTokenSigner puts the token in GDAL's GDAL_HTTP_HEADERS, which GDAL forwards across HTTP redirects — including to a different host. For catalogs that redirect asset reads cross-host, prefer a URL-signing signer (one that rewrites the href via sign_href, e.g. a SAS token in the query string) so the credential is scoped to the signed URL.

Provider-specific signers live in earthlens

Signers that hardcode a single Earth-observation catalog — Microsoft Planetary Computer, NASA Earthdata, and Copernicus CDSE — implement this same Signer protocol but ship in earthlens (earthlens.stac), not pyramids. They drop into the same signer=... arguments.

pyramids.stac.signers #

Generic signing abstraction for STAC consumers.

A signer mediates between a STAC consumer (an external client or pyramids' own from_stac) and the three distinct auth boundaries a cloud-hosted STAC archive can have:

  1. search-time — the outgoing GET/POST /search HTTP request may need credentials (a bearer token, a signed header).
  2. item-rewrite — returned STAC Items' asset hrefs may need a token grafted on or the URL rewritten.
  3. asset-read — when GDAL opens the asset, it may need extra environment (AWS_REQUEST_PAYER=requester, an Authorization header).

This module ships the generic, dependency-light signers (:class:AnonymousSigner, :class:AWSRequesterPaysSigner, :class:BearerTokenSigner). Provider-specific signers that hardcode a single Earth-observation catalog or need a remote-sensing SDK (Microsoft Planetary Computer, NASA Earthdata, Copernicus CDSE) live in earthlens, which implements this :class:Signer protocol downstream; for any other provider, implement the protocol yourself or pass a token callable to :class:BearerTokenSigner.

Signer #

Bases: Protocol

Three-boundary signing protocol for STAC consumers.

Any object exposing name plus the four methods below satisfies the protocol structurally — concrete signers need not subclass it.

Source code in src/pyramids/stac/signers.py
@runtime_checkable
class Signer(Protocol):
    """Three-boundary signing protocol for STAC consumers.

    Any object exposing `name` plus the four methods below satisfies the
    protocol structurally — concrete signers need not subclass it.
    """

    name: str

    def sign_request(self, request: Any) -> Any | None:
        """Modify an outgoing STAC-API HTTP request before it is sent.

        Matches `pystac_client.Client.open(request_modifier=...)`. Return
        `None` to signal "mutated in place, send as-is", or return the
        (modified) request.
        """
        ...

    def sign_item(self, item: Any) -> None:
        """Mutate a returned STAC Item / ItemCollection in place.

        Matches `pystac_client.Client.open(modifier=...)` and must return
        `None` (pystac-client warns on a non-None return).
        """
        ...

    def sign_href(self, href: str) -> str:
        """Rewrite a single asset href (used by `from_stac(patch_url=...)`)."""
        ...

    def gdal_env(self) -> dict[str, str]:
        """GDAL config options for asset reads (fed into `CloudConfig.extra`)."""
        ...
sign_request(request) #

Modify an outgoing STAC-API HTTP request before it is sent.

Matches pystac_client.Client.open(request_modifier=...). Return None to signal "mutated in place, send as-is", or return the (modified) request.

Source code in src/pyramids/stac/signers.py
def sign_request(self, request: Any) -> Any | None:
    """Modify an outgoing STAC-API HTTP request before it is sent.

    Matches `pystac_client.Client.open(request_modifier=...)`. Return
    `None` to signal "mutated in place, send as-is", or return the
    (modified) request.
    """
    ...
sign_item(item) #

Mutate a returned STAC Item / ItemCollection in place.

Matches pystac_client.Client.open(modifier=...) and must return None (pystac-client warns on a non-None return).

Source code in src/pyramids/stac/signers.py
def sign_item(self, item: Any) -> None:
    """Mutate a returned STAC Item / ItemCollection in place.

    Matches `pystac_client.Client.open(modifier=...)` and must return
    `None` (pystac-client warns on a non-None return).
    """
    ...
sign_href(href) #

Rewrite a single asset href (used by from_stac(patch_url=...)).

Source code in src/pyramids/stac/signers.py
def sign_href(self, href: str) -> str:
    """Rewrite a single asset href (used by `from_stac(patch_url=...)`)."""
    ...
gdal_env() #

GDAL config options for asset reads (fed into CloudConfig.extra).

Source code in src/pyramids/stac/signers.py
def gdal_env(self) -> dict[str, str]:
    """GDAL config options for asset reads (fed into `CloudConfig.extra`)."""
    ...

AnonymousSigner #

Bases: _BaseSigner

Signer that adds no credentials anywhere — for public catalogs.

Examples:

  • It is a complete no-op across every boundary:
    >>> signer = AnonymousSigner()
    >>> signer.name
    'anonymous'
    >>> signer.gdal_env()
    {}
    >>> signer.sign_href("https://example.com/a.tif")
    'https://example.com/a.tif'
    
Source code in src/pyramids/stac/signers.py
class AnonymousSigner(_BaseSigner):
    """Signer that adds no credentials anywhere — for public catalogs.

    Examples:
        - It is a complete no-op across every boundary:
            ```python
            >>> signer = AnonymousSigner()
            >>> signer.name
            'anonymous'
            >>> signer.gdal_env()
            {}
            >>> signer.sign_href("https://example.com/a.tif")
            'https://example.com/a.tif'

            ```
    """

    name = "anonymous"

AWSRequesterPaysSigner #

Bases: _BaseSigner

Signer for assets in AWS Requester-Pays buckets.

Adds only the GDAL environment needed to read from buckets such as s3://usgs-landsat or s3://sentinel-1-grd; no request or href rewrite is required.

Parameters:

Name Type Description Default
region str | None

Optional AWS region of the bucket. Stored for callers that wire their own boto3/s3fs handles; pin it to avoid cross-region egress.

None

Examples:

  • The GDAL env opts into Requester-Pays and trims redundant calls:
    >>> signer = AWSRequesterPaysSigner(region="us-west-2")
    >>> signer.gdal_env()["AWS_REQUEST_PAYER"]
    'requester'
    >>> signer.region
    'us-west-2'
    
Source code in src/pyramids/stac/signers.py
class AWSRequesterPaysSigner(_BaseSigner):
    """Signer for assets in AWS Requester-Pays buckets.

    Adds only the GDAL environment needed to read from buckets such as
    `s3://usgs-landsat` or `s3://sentinel-1-grd`; no request or href rewrite
    is required.

    Args:
        region: Optional AWS region of the bucket. Stored for callers that wire
            their own boto3/s3fs handles; pin it to avoid cross-region egress.

    Examples:
        - The GDAL env opts into Requester-Pays and trims redundant calls:
            ```python
            >>> signer = AWSRequesterPaysSigner(region="us-west-2")
            >>> signer.gdal_env()["AWS_REQUEST_PAYER"]
            'requester'
            >>> signer.region
            'us-west-2'

            ```
    """

    name = "aws-requester-pays"

    def __init__(self, region: str | None = None) -> None:
        """Store the optional bucket region.

        Args:
            region: AWS region of the Requester-Pays bucket, or `None`.
        """
        self.region = region

    def gdal_env(self) -> dict[str, str]:
        """Return the GDAL config that opts into Requester-Pays reads.

        Returns:
            A mapping setting `AWS_REQUEST_PAYER=requester` plus the standard
            cloud-read knobs that avoid extra billable HEAD/list calls.
        """
        return {
            "AWS_REQUEST_PAYER": "requester",
            "GDAL_DISABLE_READDIR_ON_OPEN": "EMPTY_DIR",
            "CPL_VSIL_CURL_USE_HEAD": "NO",
        }
__init__(region=None) #

Store the optional bucket region.

Parameters:

Name Type Description Default
region str | None

AWS region of the Requester-Pays bucket, or None.

None
Source code in src/pyramids/stac/signers.py
def __init__(self, region: str | None = None) -> None:
    """Store the optional bucket region.

    Args:
        region: AWS region of the Requester-Pays bucket, or `None`.
    """
    self.region = region
gdal_env() #

Return the GDAL config that opts into Requester-Pays reads.

Returns:

Type Description
dict[str, str]

A mapping setting AWS_REQUEST_PAYER=requester plus the standard

dict[str, str]

cloud-read knobs that avoid extra billable HEAD/list calls.

Source code in src/pyramids/stac/signers.py
def gdal_env(self) -> dict[str, str]:
    """Return the GDAL config that opts into Requester-Pays reads.

    Returns:
        A mapping setting `AWS_REQUEST_PAYER=requester` plus the standard
        cloud-read knobs that avoid extra billable HEAD/list calls.
    """
    return {
        "AWS_REQUEST_PAYER": "requester",
        "GDAL_DISABLE_READDIR_ON_OPEN": "EMPTY_DIR",
        "CPL_VSIL_CURL_USE_HEAD": "NO",
    }

BearerTokenSigner #

Bases: _BaseSigner

Signer that injects an Authorization: Bearer header.

The token may be a static string or a zero-argument callable resolved on every use — pass a callable to plug in a provider-specific token cache / refresh routine without coupling pyramids to that provider's SDK.

Security note

gdal_env() carries the token in GDAL's process-wide GDAL_HTTP_HEADERS config. :func:pyramids.stac.load_asset installs it only for the duration of the asset open (via CloudConfig) and tears it down afterwards, so it does not persist globally. However, GDAL forwards that Authorization header across HTTP redirects, including redirects to a different host (common with signed-URL / blob-storage STAC assets) — so the token can be sent to the redirect target. Prefer a URL-signing signer (rewriting the href via sign_href, e.g. a SAS token in the query string) for catalogs that redirect cross-host, and reserve this signer for catalogs that authenticate the asset host directly with a bearer header.

Parameters:

Name Type Description Default
token str | Callable[[], str]

A bearer token string, or a callable returning one.

required

Examples:

  • A static token is injected into the request and the GDAL env:
    >>> from types import SimpleNamespace
    >>> signer = BearerTokenSigner("abc123")
    >>> request = SimpleNamespace(headers={})
    >>> _ = signer.sign_request(request)
    >>> request.headers["Authorization"]
    'Bearer abc123'
    >>> signer.gdal_env()["GDAL_HTTP_HEADERS"]
    'Authorization: Bearer abc123'
    
  • A callable token is resolved on each use (e.g. an auto-refresher):
    >>> signer = BearerTokenSigner(lambda: "fresh-token")
    >>> signer.gdal_env()["GDAL_HTTP_HEADERS"]
    'Authorization: Bearer fresh-token'
    
Source code in src/pyramids/stac/signers.py
class BearerTokenSigner(_BaseSigner):
    """Signer that injects an `Authorization: Bearer` header.

    The token may be a static string or a zero-argument callable resolved on
    every use — pass a callable to plug in a provider-specific token cache /
    refresh routine without coupling pyramids to that provider's SDK.

    Security note:
        `gdal_env()` carries the token in GDAL's process-wide
        `GDAL_HTTP_HEADERS` config. :func:`pyramids.stac.load_asset` installs
        it only for the duration of the asset open (via `CloudConfig`) and
        tears it down afterwards, so it does not persist globally. However, GDAL
        forwards that `Authorization` header across HTTP redirects, including
        redirects to a *different host* (common with signed-URL / blob-storage
        STAC assets) — so the token can be sent to the redirect target. Prefer a
        URL-signing signer (rewriting the href via `sign_href`, e.g. a SAS
        token in the query string) for catalogs that redirect cross-host, and
        reserve this signer for catalogs that authenticate the asset host
        directly with a bearer header.

    Args:
        token: A bearer token string, or a callable returning one.

    Examples:
        - A static token is injected into the request and the GDAL env:
            ```python
            >>> from types import SimpleNamespace
            >>> signer = BearerTokenSigner("abc123")
            >>> request = SimpleNamespace(headers={})
            >>> _ = signer.sign_request(request)
            >>> request.headers["Authorization"]
            'Bearer abc123'
            >>> signer.gdal_env()["GDAL_HTTP_HEADERS"]
            'Authorization: Bearer abc123'

            ```
        - A callable token is resolved on each use (e.g. an auto-refresher):
            ```python
            >>> signer = BearerTokenSigner(lambda: "fresh-token")
            >>> signer.gdal_env()["GDAL_HTTP_HEADERS"]
            'Authorization: Bearer fresh-token'

            ```
    """

    name = "bearer"

    def __init__(self, token: str | Callable[[], str]) -> None:
        """Store the bearer token or token-provider callable.

        Args:
            token: A static token string or a zero-arg callable returning one.
        """
        self._token = token

    def _resolve(self) -> str:
        """Return the current token, calling the provider if one was given.

        Returns:
            The resolved bearer-token string.

        Raises:
            ValueError: The token (or the callable's return value) is not a
                non-empty string — guards against silently sending the literal
                credential `Bearer None`.
        """
        token = self._token() if callable(self._token) else self._token
        if not isinstance(token, str) or not token:
            raise ValueError(
                f"bearer token must resolve to a non-empty string, got {token!r}."
            )
        return token

    def sign_request(self, request: Any) -> Any:
        """Set the `Authorization` header on an outgoing request.

        Args:
            request: An object with a mutable `headers` mapping (e.g. a
                :class:`requests.Request`).

        Returns:
            The same `request`, with the bearer header set.
        """
        request.headers["Authorization"] = f"Bearer {self._resolve()}"
        return request

    def gdal_env(self) -> dict[str, str]:
        """Return the GDAL config carrying the bearer header for asset reads.

        Returns:
            A mapping with `GDAL_HTTP_HEADERS` set to the bearer header.
        """
        return {"GDAL_HTTP_HEADERS": f"Authorization: Bearer {self._resolve()}"}
__init__(token) #

Store the bearer token or token-provider callable.

Parameters:

Name Type Description Default
token str | Callable[[], str]

A static token string or a zero-arg callable returning one.

required
Source code in src/pyramids/stac/signers.py
def __init__(self, token: str | Callable[[], str]) -> None:
    """Store the bearer token or token-provider callable.

    Args:
        token: A static token string or a zero-arg callable returning one.
    """
    self._token = token
sign_request(request) #

Set the Authorization header on an outgoing request.

Parameters:

Name Type Description Default
request Any

An object with a mutable headers mapping (e.g. a :class:requests.Request).

required

Returns:

Type Description
Any

The same request, with the bearer header set.

Source code in src/pyramids/stac/signers.py
def sign_request(self, request: Any) -> Any:
    """Set the `Authorization` header on an outgoing request.

    Args:
        request: An object with a mutable `headers` mapping (e.g. a
            :class:`requests.Request`).

    Returns:
        The same `request`, with the bearer header set.
    """
    request.headers["Authorization"] = f"Bearer {self._resolve()}"
    return request
gdal_env() #

Return the GDAL config carrying the bearer header for asset reads.

Returns:

Type Description
dict[str, str]

A mapping with GDAL_HTTP_HEADERS set to the bearer header.

Source code in src/pyramids/stac/signers.py
def gdal_env(self) -> dict[str, str]:
    """Return the GDAL config carrying the bearer header for asset reads.

    Returns:
        A mapping with `GDAL_HTTP_HEADERS` set to the bearer header.
    """
    return {"GDAL_HTTP_HEADERS": f"Authorization: Bearer {self._resolve()}"}