geeViz.eeAuth¶
Earth Engine multi-tenant auth proxy + client helpers.
A drop-in toolkit for running Earth Engine work behind a token-injecting HTTP proxy. Lets a single Python process talk to EE on behalf of many service accounts concurrently — bypassing the ee.Initialize() global- credential limitation that normally forces process-per-tenant designs.
What “by default” gets you¶
You usually don’t need to touch this module directly. Calling
Map.view() in geeViz.geeView auto-starts the proxy on first
use. The flow is:
Map.view()→eeCreds.ensure_started("attached")discover()finds whatever credentials are visible in the environment ($GOOGLE_APPLICATION_CREDENTIALS, the EE persistent file,gcloudADC, env-var SAs).If anything was discovered, a local
uvicornproxy is spawned in a daemon thread (or reused if already running).ee.Initializeis pointed at the proxy and EE traffic routes through it for the rest of the process.
Single-credential users get this for free. Multi-credential workflows
register additional creds with
addCreds() and switch with
use() / with eeCreds.use(...).
Every browser tab that Map.view() opens is pinned to the tenant
that was current at the time of the call — subsequent use()
switches in Python can’t drift open tabs to a different credential.
Layout¶
geeViz.eeAuth.eeCreds— high-leveleeCredssingleton & APIgeeViz.eeAuth.registry— lower-level env-var-driven SA cachegeeViz.eeAuth.tags— workload-tag construction (EE billing attribution)geeViz.eeAuth.client— initialize theeeSDK to route through a proxygeeViz.eeAuth.server— FastAPI proxy app (mountable in your ownFastAPI app OR runnable standalone)
Typical use — multi-credential (when you want it)¶
from geeViz.eeAuth import eeCreds
import ee
eeCreds.addCreds("/path/to/sa-prod.json", "prod")
eeCreds.addCreds("/path/to/sa-training.json", "training")
eeCreds.start() # spins up the local proxy
eeCreds.use("prod")
ee.Number(1).getInfo() # routes through prod SA
with eeCreds.use("training"):
ee.Number(2).getInfo() # routes through training SA
# back to prod here
Typical use — running the proxy standalone (Cloud Run, Docker, etc.)¶
python -m geeViz.eeAuth --port 8888
Embedded in your FastAPI app:
from fastapi import FastAPI
from geeViz.eeAuth.server import build_proxy_router
from geeViz.eeAuth import eeCreds
eeCreds.discover()
app = FastAPI()
app.include_router(
build_proxy_router(creds=eeCreds), prefix="/ee-api",
)
The proxy resolves which SA to use for each request from (in order):
X-geeViz-Credsrequest header?tenant=query string parameter/ee-api/t/<tenant>/path-prefix (used byMap.view()to pin each browser tab to its tenant)Default tenant (
GEE_SERVICE_ACCOUNT_B64env var, or the first registered credential ineeCreds)
Add SAs by setting env vars matching GEE_<NAME>_SERVICE_ACCOUNT
where NAME is a tenant id. The value is a base64-encoded SA JSON key.
Why this exists¶
The Earth Engine Python SDK stores credentials in module-level state via
ee.Initialize(). Two tenants can’t run concurrently in one process
without racing each other’s auth. By routing every REST call through a
local proxy that injects the right SA per request, you keep one Python
process, one ee.Initialize call, and get full multi-tenant
concurrency.
Functions
|
Module-level convenience: |
- class geeViz.eeAuth.EECreds[source]¶
Bases:
objectMulti-tenant credential registry + EE init lifecycle.
Usually used via the module-level singleton
eeCreds— but instantiable directly when you need multiple independent registries (e.g. tests, multi-tenant servers with isolated credential sets):from geeViz.eeAuth.eeCreds import EECreds creds = EECreds() creds.addCreds(...) creds.start()
- addADC(name: str = 'adc', project: str | None = None) EECreds[source]¶
Register the runtime’s Application Default Credentials as a tenant entry.
Use this when you want the proxy to route through ADC (attached SA on Cloud Run, WIF federation on AWS, gcloud user creds locally) — without holding any key material. Without this, ADC-only deployments still get EE working via
robust_init()but the proxy starts empty and the multi-tenant features are inaccessible.Tokens are minted by calling
google.auth.default()at mint time, then refreshing. Same code path as keyed SAs from the proxy’s perspective.
- addCreds(creds: str | dict | bytes, name: str, project: str | None = None) EECreds[source]¶
Register a set of credentials under
name.Accepts:
File path to a JSON key file (SA, OAuth, WIF, or impersonation config)
Base64-encoded JSON string (typical for env vars)
JSON string (literal contents)
Already-parsed dict
Bare service-account email string (
foo@bar.iam.gserviceaccount.com) → routed toaddImpersonation()so the proxy mints tokens via the IAM credentials API at request time (no key held)
Auto-detects which credential shape it is.
projectoverrides the default project — required for OAuth + impersonation, optional for SAs since their JSON hasproject_id.Returns self so chaining works:
eeCreds.addCreds(sa1, "a").addCreds(sa2, "b").start()
- addImpersonation(target_email: str, name: str, project: str | None = None) EECreds[source]¶
Register a tenant that mints tokens by impersonating
target_emailat request time.No key material is held. The runtime’s ADC source must hold
roles/iam.serviceAccountTokenCreatoron the target SA. On each token mint,google.auth.impersonated_credentialscalls the IAM credentials API to fetch a short-lived (1h) access token; google-auth refreshes it automatically when it expires.This is the multi-tenant keyless path: the proxy holds N SA emails, not N JSON keys. Works identically across GCP Cloud Run (source = attached SA), AWS via WIF (source = federated identity from STS), and local dev (source = gcloud user creds).
- Parameters:
target_email – The SA to impersonate (
foo@proj.iam.gserviceaccount.com).name – Tenant name for
eeCreds.use(name).project – Quota project for EE calls. If omitted, falls back to the runtime’s default ADC project.
- clearWorkloadTag() None[source]¶
Clear the SDK-side workload tag so subsequent EE calls fall back to the proxy’s default builder. Mapping in the store is preserved (so historical tags remain reversible).
- current() str[source]¶
Return the currently active tenant name. Falls back to the first registered name if no explicit
use()has been made yet and at least one credential is registered. Returns""if nothing’s registered.
- discover(*, overwrite: bool = False) list[str][source]¶
Scan the environment for credentials and register any found.
Lookups, in order — each one that produces a credential gets added under a stable name:
Source
Registered as
$GOOGLE_APPLICATION_CREDENTIALS(JSON path)"adc"~/.config/earthengine/credentials(EE persistent)"ee-persistent"gcloud ADC well-known file
"adc-default"$GEE_SERVICE_ACCOUNT_B64(legacy default SA)"env-default"$GEE_<NAME>_SERVICE_ACCOUNT(per-tenant SA keys)<name>$GEE_<NAME>_SA_EMAIL(per-tenant impersonation)<name>google.auth.default()fallback (Cloud Run / WIF)"adc"The fallback only fires when nothing else registered, so keyed deployments aren’t disturbed. On Cloud Run with an attached SA, on GKE with workload identity, or on AWS via Workload Identity Federation, this is the path that boots the proxy without any JSON key on disk.
Returns the list of names actually registered by this call. Existing names are not overwritten unless
overwrite=True.Safe to call multiple times; failing sources are logged but don’t raise — discovery is best-effort.
- ensure_started(*, mode: str = 'attached', proxy_port: int = 8889) dict[source]¶
Idempotent “I want the proxy running, please” helper used by
Map.view()and any other code that wants to ride the eeCreds proxy without forcing the user to calladdCreds+start.Modes:
"auto": try discovery + start (inline daemon thread). If anything fails, return a status dict withproxy_url=""— caller can fall back."proxy": try discovery + start (inline daemon thread). RAISE if nothing can be discovered or the proxy fails to bind."detached": attach to or spawn a long-lived background subprocess runningpython -m geeViz.eeAuth. Survives the calling script’s exit, so multi-Map.view()workflows and successive script invocations all share one proxy without needing the blockinginput()at the end of each. The subprocess is identified by a state file at<tmp>/.geeViz_eeauth_proxy.json; clients verify version + tenant fingerprint via/healthand respawn if anything drifted."legacy": do nothing. Returns immediately with"".
Returns
{proxy_url, tenants, current, mode, discovered}.proxy_url == ""means caller should fall back.
- getTagStore()[source]¶
Return the current tag store, constructing the default lazily on first call. See
setTagStore()for how to override.
- get_token(name: str | None = None, force_refresh: bool = False) dict[source]¶
Mint (or return cached) access token for
name. If no name is given, uses the currently active tenant.Returns
{access_token, project_id, client_email, tenant}— same shape asgeeViz.eeAuth.registry.SARegistry.get_tokensobuild_proxy_routercan accept either.
- info(name: str | None = None) dict[source]¶
Inspect a registered credential without exposing the secret key material. Returns
{name, type, project_id, client_email, source}.name=Nonereturns the currently active one.
- lookupWorkloadTag(tag: str) dict | None[source]¶
Recover the parts dict for a previously-minted tag. Returns
Noneif the tag isn’t in this process’s tag store (could be from another instance if not using shared storage, or an untagged / externally-tagged request).
- property proxy_url: str | None¶
URL of the in-process proxy (if one’s running). Useful for embedding into iframe URLs / Map exports so the JS side uses the same proxy.
- restart(*, mode: str = 'detached') dict[source]¶
Full
stop()followed byensure_started(mode=mode). Use after editing anygeeViz.eeAuthsource file so a fresh proxy loads the changes instead of the caller attaching to a still-running subprocess whose Python interpreter cached the old bytecode. Returns theensure_startedstatus dict.
- robust_init(*, verbose: bool = False, interactive: bool = True) dict[source]¶
Best-effort EE initialization with the simplest possible UX.
Decision tree (first hit wins, no prompts):
EE already initialized AND a test call works → return as-is.
eeAuth multi-tenant proxy via
ensure_started→ use it.ee.Initialize()with NO project arg → let EE’s own resolution chain (credentials’quota_project_id→ ADC → env vars) pick the project. This is the path that mirrors whatee.Initialize()would do if the user typed it themselves.Fallback:
ee.Authenticate(force=True, auth_mode='localhost')(interactive only) and re-run step 3.
No project-id prompts. If a user wants a specific project they can call
ee.Initialize(project='X')themselves before importing geeViz, or runearthengine set_project X/gcloud auth application-default set-quota-project X.Returns a status dict:
{"ok": bool, "source": "already-initialized" | "eeauth-proxy" | "ee-auto-init" | "interactive-auth", "project": "..."}
Raises
RuntimeErrorwhen no path completes — e.g. non-interactive environment with no creds, or no quota project discoverable after a fresh authenticate.- Parameters:
verbose – Print progress to stdout.
interactive – If False, skip the
ee.Authenticate()fallback and raise instead. Useful for daemons / CI where blocking on a browser would hang.
- router(**kwargs)[source]¶
Return a FastAPI
APIRouterthat proxies EE requests using these credentials. Mount it in your own FastAPI app:app.include_router(eeCreds.router(), prefix="/ee-api")
kwargspass through tobuild_proxy_router— customize the tenant header, resolver, workload-tag builder, etc.
- setTagSecret(secret: str) EECreds[source]¶
Set the secret used when minting workload tags. Defaults to
$WORKLOAD_TAG_SECRET(or a local-dev fallback if unset). Same secret is required to re-mint the same tag from the same parts across processes.
- setTagStore(store) EECreds[source]¶
Replace the default
TagStoreused for auto-minted workload tags. Pass anInMemoryTagStore(),SQLiteTagStore(path=...), or any object matching theTagStoreprotocol. Call this BEFOREstart()if you want the proxy’s fallback builder to write into your store. Returns self for chaining.
- setWorkloadTag(**parts) str[source]¶
Mint a tag from
parts, store the mapping, and set it on the EE Python SDK so every subsequent EE call carries it.Three things happen atomically:
tag = mint_workload_tag(parts, secret=…)— deterministic short hash (wl_<hex>).getTagStore().put(tag, parts)— solookupWorkloadTag(tag)can recoverpartslater, and any Cloud Monitoring row tagged withtagcan be joined back to identity.ee.data.setWorkloadTag(tag)— SDK-side setter. The tag is attached to every subsequent.getInfo(),.getMapId(), export, etc., AND baked into any tile URL returned bygetMapId, so browser tile fetches inherit it.
Since the proxy’s default builder honours client-set tags (
if request.query_params['workloadTag']: return it), the same tag round-trips through the proxy without any custom builder needed.Returns the minted tag so callers can log / correlate it.
- start(*, proxy_port: int = 8889, proxy_host: str = '127.0.0.1', ee_init: bool = True, launch_proxy: bool = True, workload_tag_builder=None) dict[source]¶
Initialize Earth Engine for multi-credential use.
Steps (each can be disabled via kwargs):
launch_proxy=True: start a background HTTP proxy that injects per-tenant SA / OAuth tokens. Required for switching credentials at runtime without re-initializingee.ee_init=True: callee.Initialize(url=proxy_url, ...)so the EE Python SDK routes all REST calls through the proxy.
Returns a status dict with
{started, proxy_url, tenants, ee_initialized}for inspection.Idempotent — calling
start()twice is safe; the second call returns the current state.
- stop() None[source]¶
Shut down the proxy — in-process AND any detached subprocess — and un-initialize the EE SDK so subsequent calls fail fast.
Without the SDK reset,
ee.Image(...).getInfo()afterstop()hangs on the socket timeout of the (now-dead) proxy URL before raising.ee.Reset()clears the SDK’s cached connection so the next call raises “Earth Engine client library not initialized” immediately.Prints one line describing what was torn down so the caller has a visible confirmation rather than a silent state change.
Safe to call when nothing is running.
- classmethod stop_detached() bool[source]¶
Public helper: kill the detached proxy (if any) and clear the state file. Returns
Trueif a process was actually terminated,Falseif there was nothing to kill.
- sync_oauth_project(project: str) int[source]¶
Update every OAuth entry’s
project_idtoprojectand invalidate the cached access token so the next mint includes the new project on thex-goog-user-projectheader.Used by
robustInitializerafter the legacyee.Initializefallback succeeds: discovery may have guessed a project the OAuth user can’t access (e.g.gcloud configpointing at a service-account-owned project), but legacy init knows what ACTUALLY works. Syncing that back to the OAuth entries means subsequentMap.view()calls route through the proxy with the correct project instead of repeating the 403.Service-account entries are NOT touched — their project_id came from the SA JSON and is authoritative.
- Parameters:
project – The known-good project ID.
- Returns:
Number of entries actually updated.
- use(name: str)[source]¶
Switch the active credential and return a context manager that restores the previous one on exit. Works as a statement OR a
withblock:eeCreds.use("acme") # switch and forget ee.Image(1).getInfo() # uses acme with eeCreds.use("ian"): # scoped ee.Image(2).getInfo() # uses ian # back to acme here
- geeViz.eeAuth.robust_init(*, verbose: bool = False, interactive: bool = True) dict[source]¶
Module-level convenience:
eeCreds.robust_init(...).Centralizes the “get EE up and running, prefer the proxy, never silently fall through to gcloud ADC without saying so” bootstrap that
geeViz.geeViewand external callers both want.
- class geeViz.eeAuth.SARegistry[source]¶
Bases:
objectPer-tenant service-account credentials + cached access tokens.
- geeViz.eeAuth.get_registry() SARegistry[source]¶
Return the process-wide SA registry, constructing it lazily on first access.
Thread-safe: the constructor grabs its own lock. Subsequent callers receive the same instance.
- Returns:
The singleton registry.
- Return type:
- geeViz.eeAuth.reset_registry() None[source]¶
Clear the singleton so the next
get_registry()re-reads the env.Used by tests that mutate GEEVIZ_SA_JSON_* env vars between cases — without a reset the cached registry would ignore the changes.
- geeViz.eeAuth.build_workload_tag(*parts: str) str[source]¶
Join sanitized parts with
__and clamp to EE’s 63-char limit.Empty / falsy parts are dropped. The final tag is guaranteed to satisfy EE’s regex:
[a-z0-9][a-z0-9_\-]{0,61}[a-z0-9]. Returns an empty string if everything was dropped — callers should treat empty as “no tag” and skip the workload-tag header / body field entirely.
- geeViz.eeAuth.sanitize_workload_tag_part(s: str) str[source]¶
Sanitize a single component of a workload tag.
Lowercases.
Replaces disallowed characters with
-.Collapses runs of
-to a single-.Collapses runs of
_to a single_so the__separator stays unambiguous when parts are joined.Strips leading/trailing
-and_(EE rejects tags that don’t begin and end with an alphanumeric).
- geeViz.eeAuth.initialize_via_proxy(proxy_url: str, tenant_header: str = 'X-geeViz-Creds', project: str | None = None) bool[source]¶
Initialize the Earth Engine SDK to route all REST calls through
proxy_url.Uses
AnonymousCredentialssince the proxy holds the real SA credentials. The SDK’s bearer-token header is stripped byTenantAwareHttpbefore reaching the proxy anyway.- Parameters:
proxy_url – Base URL of the EE proxy, e.g.
"http://localhost:8888/ee-api". No trailing slash.tenant_header – Header name the proxy expects for tenant routing. Default
X-geeViz-CredsmatchesgeeViz.eeAuth.server.project – Placeholder project id passed to
ee.Initialize(EE requires one but the proxy overrides per-tenant viax-goog-user-project). Default"ee-proxy-placeholder".
- Returns:
True on success, False if init failed (caller should fall back to direct
ee.Initializeor surface the error). Prints any underlying exception to stderr; doesn’t re-raise.
- geeViz.eeAuth.tenant_context(tenant: str)[source]¶
Scoped tenant switch.
with tenant_context("training"): ee.Image(1).getInfo() # back to previous tenant here
- geeViz.eeAuth.set_tenant(tenant: str)[source]¶
Set the current tenant for subsequent EE calls in this context.
Returns a token that can be passed to
reset_tenantto restore the previous value. Prefertenant_context()for scoped use.
- geeViz.eeAuth.reset_tenant(token) None[source]¶
Restore the tenant to what it was before the matching
set_tenantcall.- Parameters:
token – The token object returned by the paired
set_tenant()call. Passing a token from a different scope raisesValueError.
- class geeViz.eeAuth.TenantAwareHttp(tenant_header: str = 'X-geeViz-Creds')[source]¶
Bases:
objecthttplib2.Httpsubclass that stamps the tenant header on every outbound request and strips whatever Authorization the SDK injected.Subclassed at first instantiation so
httplib2is only imported when actually used — keeps unit tests that mock the EE init path from needing the dependency. The header name is set per-instance so you can run multiple proxies with different conventions in the same process if you really need to.Thread safety¶
httplib2.Httpis NOT thread-safe — its per-host connection cache (self.connections) is a plaindictmutated from insiderequest()andsocket.HTTPConnectionobjects hold per-instance socket state. When the EE SDK shares one transport across aThreadPoolExecutor(asMap.testLayers()does with 8 workers), concurrent threads tear down each other’s sockets mid-request, surfacing as'NoneType' object has no attribute 'close'and WindowsWinError 10038/10057socket errors.Workaround: route each thread’s
request()call to its OWNhttplib2.Httpinstance stored inthreading.local(). EE’s SDK only consults the transport forrequest()— it doesn’t reach intoself.connectionsdirectly — so per-thread instances are a safe drop-in.
- geeViz.eeAuth.build_proxy_router(creds=None, upstream: str = 'https://content-earthengine.googleapis.com', tenant_header: str = 'X-geeViz-Creds', tenant_query_param: str = 'tenant', tenant_resolver: Callable[[Request, str], str] | None = None, workload_tag_builder: Callable[[Request, str], str] | None = None) APIRouter[source]¶
Build a FastAPI
APIRouterthat handles{path:path}and proxies every request toupstreamwith the right SA token.- Parameters:
creds – Object exposing
get_token(tenant, force_refresh=False) -> {access_token, project_id, tenant, ...}. Accepts anEECredsinstance, anSARegistry, or any other object with the same interface.None(default) uses the process-wide env-var registry (legacy).upstream – Base URL of the real EE API.
content-earthengine.googleapis.comworks for both maps and compute.earthengine.googleapis.comis also accepted for most endpoints.tenant_header – Header name to read for routing. Default
X-geeViz-Creds. Must match the client side.tenant_query_param – Query string key to read for tenant routing (browser iframe pattern). Default
"tenant". Stripped from the outbound URL so EE never sees it.tenant_resolver – Custom function
(request) -> strto pick the tenant. Override for richer auth schemes (e.g. resolve via IAP email lookup). Default readstenant_headerthentenant_query_param.workload_tag_builder – Custom function
(request, tenant) -> strthat returns the workload tag for billing attribution. Returning""disables tagging on this request. Default buildsee-proxy__<tenant>.
Mount the returned router on whatever prefix you like — typically
/ee-api.
- geeViz.eeAuth.create_proxy_app(creds=None, upstream: str = 'https://content-earthengine.googleapis.com', tenant_header: str = 'X-geeViz-Creds', tenant_query_param: str = 'tenant', tenant_resolver: Callable[[Request, str], str] | None = None, workload_tag_builder: Callable[[Request, str], str] | None = None, prefix: str = '/ee-api', serve_geeview: bool = True) FastAPI[source]¶
Build a standalone FastAPI app with the proxy mounted at
prefix. Suitable for direct serving viauvicornor for testing.credsaccepts anEECreds/SARegistry-like object;Nonefalls back to the env-var registry. Seebuild_proxy_router()for the other parameters.Use
build_proxy_routerdirectly if you want to mount in an existing FastAPI app and share its middleware / lifecycle.- Parameters:
serve_geeview – When True (default for standalone runs), also mount the geeView frontend bundle at
/geeView/*. This makes the detached proxy the single long-lived server for both EE auth (/ee-api/*) andMap.view()HTML (/geeView/...). Same origin, same port — browser tabs survive script exits without a daemon-thread server inside each script. Set False to keep the proxy auth-only.
Modules
Client-side helpers for routing the Earth Engine Python SDK through a token-injecting proxy. |
|
Multi-tenant credential registry + EE init lifecycle. |
|
Earth Engine usage monitoring — Cloud Monitoring poller. |
|
Multi-tenant Earth Engine service-account registry. |
|
FastAPI proxy for Earth Engine that injects per-tenant SA tokens. |
|
Earth Engine workload-tag helpers. |
|